Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor TextBuffer::GenHTML #2038

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 125 additions & 187 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
#include "textBuffer.hpp"
#include "CharRow.hpp"

#include "../types/inc/utils.hpp"
#include "../types/inc/convert.hpp"
#include <regex>
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved

#pragma hdrstop

using namespace Microsoft::Console;
using namespace Microsoft::Console::Types;

// Routine Description:
Expand Down Expand Up @@ -1033,238 +1036,173 @@ const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSe
// - Generates a CF_HTML compliant structure based on the passed in text and color data
// Arguments:
// - rows - the text and color data we will format & encapsulate
// - iFontHeightPoints - the unscaled font height
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - htmlTitle - value used in title tag of html header. Used to name the application
// Return Value:
// - string containing the generated HTML
std::string TextBuffer::GenHTML(const TextAndColor& rows, const int iFontHeightPoints, const PCWCHAR fontFaceName)
std::string TextBuffer::GenHTML(const TextAndColor& rows, const int fontHeightPoints, const PCWCHAR fontFaceName, const std::string htmlTitle)
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
{
std::string szClipboard; // we will build the data going back in this string buffer

try
{
std::string const szHtmlClipFormat =
"Version:0.9\r\n"
"StartHTML:%010d\r\n"
"EndHTML:%010d\r\n"
"StartFragment:%010d\r\n"
"EndFragment:%010d\r\n"
"StartSelection:%010d\r\n"
"EndSelection:%010d\r\n";

// measure clip header
size_t const cbHeader = 157; // when formats are expanded, there will be 157 bytes in the header.

std::string const szHtmlHeader =
"<!DOCTYPE><HTML><HEAD><TITLE>Windows Console Host</TITLE></HEAD><BODY>";
size_t const cbHtmlHeader = szHtmlHeader.size();

std::string const szHtmlFragStart = "<!--StartFragment -->";
std::string const szHtmlFragEnd = "<!--EndFragment -->";
std::string const szHtmlFooter = "</BODY></HTML>";
size_t const cbHtmlFooter = szHtmlFooter.size();

std::string const szDivOuterBackgroundPattern = R"X(<DIV STYLE="background-color:#%02x%02x%02x;white-space:pre;">)X";

size_t const cbDivOuter = 55;
std::string szDivOuter;
szDivOuter.reserve(cbDivOuter);

std::string const szSpanFontSizePattern = R"X(<SPAN STYLE="font-size: %dpt">)X";

size_t const cbSpanFontSize = 28 + (iFontHeightPoints / 10) + 1;

std::string szSpanFontSize;
szSpanFontSize.resize(cbSpanFontSize + 1); // reserve space for null after string for sprintf
sprintf_s(szSpanFontSize.data(), cbSpanFontSize + 1, szSpanFontSizePattern.data(), iFontHeightPoints);
szSpanFontSize.resize(cbSpanFontSize); //chop off null at end

std::string const szSpanStartPattern = R"X(<SPAN STYLE="color:#%02x%02x%02x;background-color:#%02x%02x%02x">)X";

size_t const cbSpanStart = 53; // when format is expanded, there will be 53 bytes per color pattern.
std::string szSpanStart;
szSpanStart.resize(cbSpanStart + 1); // +1 for null terminator
std::ostringstream htmlBuilder;

std::string const szSpanStartFontPattern = R"X(<SPAN STYLE="font-family: '%s', monospace">)X";
size_t const cbSpanStartFontPattern = 41;

std::string const szSpanStartFontConstant = R"X(<SPAN STYLE="font-family: monospace">)X";
size_t const cbSpanStartFontConstant = 37;
// First we have to add some standard
// HTML boiler plate required for CF_HTML
// as part of the HTML Clipboard format
const std::string htmlHeader =
"<!DOCTYPE><HTML><HEAD><TITLE>" + htmlTitle + "</TITLE></HEAD><BODY>";
htmlBuilder << htmlHeader;

std::string szSpanStartFont;
size_t cbSpanStartFont;
bool fDeleteSpanStartFont = false;
htmlBuilder << "<!--StartFragment -->";

std::wstring const wszFontFaceName = fontFaceName;
size_t const cchFontFaceName = wszFontFaceName.size();
if (cchFontFaceName > 0)
// apply global style in div element
{
// measure and create buffer to convert face name to UTF8
int const cbNeeded = WideCharToMultiByte(CP_UTF8, 0, wszFontFaceName.data(), static_cast<int>(cchFontFaceName), nullptr, 0, nullptr, nullptr);
std::string szBuffer;
szBuffer.resize(cbNeeded);
htmlBuilder << "<DIV STYLE=\"";
htmlBuilder << "display:inline-block";
htmlBuilder << "width:400px;";
htmlBuilder << "white-space:pre;";

// fixme: this is only walkaround for filling background after last char of row.
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
// It is based on first char of first row, not the actual char at correct position.
htmlBuilder << "background-color:";
const COLORREF globalBgColor = rows.BkAttr.at(0).at(0);
htmlBuilder << Utils::ColorToHexString(globalBgColor);
htmlBuilder << ";";

htmlBuilder << "font-family:'";
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
if (fontFaceName[0] != '\0')
{
htmlBuilder << ConvertToA(CP_UTF8, fontFaceName);
htmlBuilder << "',";
}
// even with different font, add monospace as fallback
htmlBuilder << "monospace;";

// do conversion
WideCharToMultiByte(CP_UTF8, 0, wszFontFaceName.data(), static_cast<int>(cchFontFaceName), szBuffer.data(), cbNeeded, nullptr, nullptr);
htmlBuilder << "font-size:";
htmlBuilder << fontHeightPoints;
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
htmlBuilder << "pt;";

// format converted font name into pattern
std::string const szFinalFontPattern = R"X(<SPAN STYLE="font-family: ')X" + szBuffer + R"X(', monospace\">)X";
size_t const cbBytesNeeded = szFinalFontPattern.length();
// note: MS Word doesn't support padding (in this way at least)
htmlBuilder << "padding:";
htmlBuilder << 4; // todo: customizable padding
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
htmlBuilder << "px;";

fDeleteSpanStartFont = true;
szSpanStartFont = szFinalFontPattern;
cbSpanStartFont = cbBytesNeeded;
}
else
{
szSpanStartFont = szSpanStartFontConstant;
cbSpanStartFont = cbSpanStartFontConstant;
htmlBuilder << "\">";
}

std::string const szSpanEnd = "</SPAN>";
std::string const szDivEnd = "</DIV>";

// Start building the HTML formated string to return
// First we have to add the required header and then
// some standard HTML boiler plate required for CF_HTML
// as part of the HTML Clipboard format
szClipboard.append(cbHeader, 'H'); // reserve space for a header we fill in later
szClipboard.append(szHtmlHeader);
szClipboard.append(szHtmlFragStart);

COLORREF iBgColor = rows.BkAttr.at(0).at(0);

szDivOuter.resize(cbDivOuter + 1);
sprintf_s(szDivOuter.data(), cbDivOuter + 1, szDivOuterBackgroundPattern.data(), GetRValue(iBgColor), GetGValue(iBgColor), GetBValue(iBgColor));
szDivOuter.resize(cbDivOuter);
szClipboard.append(szDivOuter);

// copy font face start
szClipboard.append(szSpanStartFont);

// copy font size start
szClipboard.append(szSpanFontSize);

bool bColorFound = false;

// copy all text into the final clipboard data handle. There should be no nulls between rows of
// characters, but there should be a \0 at the end.
COLORREF const Blackness = RGB(0x00, 0x00, 0x00);
for (UINT iRow = 0; iRow < rows.text.size(); iRow++)
// copy text and info color from buffer
bool hasWrittenAnyText = false;
std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (UINT row = 0; row < rows.text.size(); row++)
{
size_t cbStartOffset = 0;
size_t cchCharsToPrint = 0;
size_t startOffset = 0;

COLORREF fgColor = Blackness;
COLORREF bkColor = Blackness;

for (UINT iCol = 0; iCol < rows.text.at(iRow).length(); iCol++)
if (row != 0)
{
bool fColorDelta = false;
htmlBuilder << "<BR>";
}

if (!bColorFound)
for (UINT col = 0; col < rows.text[row].length(); col++)
{
// do not include \r nor \n as they don't have attributes
// and are not HTML friendly. For line break use '<BR>' instead.
bool isLastCharInRow =
col == rows.text[row].length() - 1 ||
rows.text[row][col + 1] == '\r' ||
rows.text[row][col + 1] == '\n';

bool colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr[row][col] != fgColor.value())
{
fgColor = rows.FgAttr.at(iRow).at(iCol);
bkColor = rows.BkAttr.at(iRow).at(iCol);
bColorFound = true;
fColorDelta = true;
fgColor = rows.FgAttr[row][col];
colorChanged = true;
}
else if ((rows.FgAttr.at(iRow).at(iCol) != fgColor) || (rows.BkAttr.at(iRow).at(iCol) != bkColor))

if (!bkColor.has_value() || rows.BkAttr[row][col] != bkColor.value())
{
fgColor = rows.FgAttr.at(iRow).at(iCol);
bkColor = rows.BkAttr.at(iRow).at(iCol);
fColorDelta = true;
bkColor = rows.BkAttr[row][col];
colorChanged = true;
}

if (fColorDelta)
{
if (cchCharsToPrint > 0)
const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col > startOffset)
{
// write accumulated characters to stream ....
std::string TempBuff;
int const cbTempCharsNeeded = WideCharToMultiByte(CP_UTF8, 0, rows.text[iRow].data() + cbStartOffset, static_cast<int>(cchCharsToPrint), nullptr, 0, nullptr, nullptr);
TempBuff.resize(cbTempCharsNeeded);
WideCharToMultiByte(CP_UTF8, 0, rows.text[iRow].data() + cbStartOffset, static_cast<int>(cchCharsToPrint), TempBuff.data(), cbTempCharsNeeded, nullptr, nullptr);
szClipboard.append(TempBuff);
cbStartOffset += cchCharsToPrint;
cchCharsToPrint = 0;

// close previous span
szClipboard += szSpanEnd;
// note: this should be escaped (for '<', '>', and '&'),
// however MS Word doesn't appear to support HTML entities
htmlBuilder << ConvertToA(CP_UTF8, std::wstring_view(rows.text[row].data() + startOffset, col - startOffset + includeCurrent));
startOffset = col;
}
};

// start new span
if (colorChanged)
{
writeAccumulatedChars(false);

// format with color then copy formatted string
szSpanStart.resize(cbSpanStart + 1); // add room for null
sprintf_s(szSpanStart.data(), cbSpanStart + 1, szSpanStartPattern.data(), GetRValue(fgColor), GetGValue(fgColor), GetBValue(fgColor), GetRValue(bkColor), GetGValue(bkColor), GetBValue(bkColor));
szSpanStart.resize(cbSpanStart); // chop null from sprintf
szClipboard.append(szSpanStart);
}
if (hasWrittenAnyText)
{
htmlBuilder << "</SPAN>";
}

// accumulate 1 character
cchCharsToPrint++;
}
htmlBuilder << "<SPAN STYLE=\"";
htmlBuilder << "color:";
htmlBuilder << Utils::ColorToHexString(fgColor.value());
htmlBuilder << ";";
htmlBuilder << "background-color:";
htmlBuilder << Utils::ColorToHexString(bkColor.value());
htmlBuilder << ";";
htmlBuilder << "\">";
}

PCWCHAR pwchAccumulateStart = rows.text.at(iRow).data() + cbStartOffset;
hasWrittenAnyText = true;

// write accumulated characters to stream
std::string CharsConverted;
int cbCharsConverted = WideCharToMultiByte(CP_UTF8, 0, pwchAccumulateStart, static_cast<int>(cchCharsToPrint), nullptr, 0, nullptr, nullptr);
CharsConverted.resize(cbCharsConverted);
WideCharToMultiByte(CP_UTF8, 0, pwchAccumulateStart, static_cast<int>(cchCharsToPrint), CharsConverted.data(), cbCharsConverted, nullptr, nullptr);
szClipboard.append(CharsConverted);
if (isLastCharInRow)
{
writeAccumulatedChars(true);
break;
}
}
}

if (bColorFound)
if (hasWrittenAnyText)
{
// copy end span
szClipboard.append(szSpanEnd);
// last opened span wasn't closed in loop above, so close it now
htmlBuilder << "</SPAN>";
}

// after we have copied all text we must wrap up
// with a standard set of HTML boilerplate required
// by CF_HTML

// copy end font size span
szClipboard.append(szSpanEnd);

// copy end font face span
szClipboard.append(szSpanEnd);
htmlBuilder << "</DIV>";

// copy end background color span
szClipboard.append(szDivEnd);
htmlBuilder << "<!--EndFragment -->";

// copy HTML end fragment
szClipboard.append(szHtmlFragEnd);
constexpr std::string_view HtmlFooter = "</BODY></HTML>";
htmlBuilder << HtmlFooter;

// copy HTML footer
szClipboard.append(szHtmlFooter);
// once filled with values, there will be exactly 157 bytes in the clipboard header
constexpr size_t ClipboardHeaderSize = 157;
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved

// null terminate the clipboard data
szClipboard += '\0';
// these values are byte offsets from start of clipboard
const size_t htmlStartPos = ClipboardHeaderSize;
const size_t htmlEndPos = ClipboardHeaderSize + htmlBuilder.tellp();
const size_t fragStartPos = ClipboardHeaderSize + htmlHeader.length();
const size_t fragEndPos = htmlEndPos - HtmlFooter.length();

// we are done generating formating & building HTML for the selection
// prepare the header text with the byte counts now that we know them
size_t const cbHtmlStart = cbHeader; // bytecount to start of HTML context
size_t const cbHtmlEnd = szClipboard.size() - 1; // don't count the null at the end
size_t const cbFragStart = cbHeader + cbHtmlHeader; // bytecount to start of selection fragment
size_t const cbFragEnd = cbHtmlEnd - cbHtmlFooter;
// header required by HTML 0.9 format
std::ostringstream clipHeaderBuilder;
clipHeaderBuilder << "Version:0.9\r\n";
clipHeaderBuilder << std::setfill('0');
clipHeaderBuilder << "StartHTML:" << std::setw(10) << htmlStartPos << "\r\n";
clipHeaderBuilder << "EndHTML:" << std::setw(10) << htmlEndPos << "\r\n";
clipHeaderBuilder << "StartFragment:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndFragment:" << std::setw(10) << fragEndPos << "\r\n";
clipHeaderBuilder << "StartSelection:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndSelection:" << std::setw(10) << fragEndPos << "\r\n";

// push the values into the required HTML 0.9 header format
std::string szHtmlClipHeaderFinal;
szHtmlClipHeaderFinal.resize(cbHeader + 1); // add room for a null
sprintf_s(szHtmlClipHeaderFinal.data(), cbHeader + 1, szHtmlClipFormat.data(), cbHtmlStart, cbHtmlEnd, cbFragStart, cbFragEnd, cbFragStart, cbFragEnd);
szHtmlClipHeaderFinal.resize(cbHeader); // chop off the null

// overwrite the reserved space with the actual header & offsets we calculated
szClipboard.replace(0, cbHeader, szHtmlClipHeaderFinal.data());
return clipHeaderBuilder.str() + htmlBuilder.str();
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
szClipboard.clear(); // dont return a partial html fragment...
return "";
mcpiroman marked this conversation as resolved.
Show resolved Hide resolved
}

return szClipboard;
}
5 changes: 3 additions & 2 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ class TextBuffer final
std::function<COLORREF(TextAttribute&)> GetBackgroundColor) const;

static std::string GenHTML(const TextAndColor& rows,
const int iFontHeightPoints,
const PCWCHAR fontFaceName);
const int fontHeightPoints,
const PCWCHAR fontFaceName,
const std::string htmlTitle);

private:
std::deque<ROW> _storage;
Expand Down
4 changes: 2 additions & 2 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1194,8 +1194,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}

// convert text to HTML format
const auto htmlData = TextBuffer::GenHTML(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName());

const auto htmlData = TextBuffer::GenHTML(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName(), "Windows Terminal");
_terminal->ClearSelection();

// send data up for clipboard
Expand Down
Loading