From fa89e53fa665515728548813f0d76a6639d16846 Mon Sep 17 00:00:00 2001 From: mcpiroman <38111589+mcpiroman@users.noreply.github.com> Date: Tue, 6 Aug 2019 20:34:30 +0200 Subject: [PATCH] Refactor TextBuffer::GenHTML (#2038) Fixes #1846. --- src/buffer/out/textBuffer.cpp | 311 ++++++++----------- src/buffer/out/textBuffer.hpp | 5 +- src/cascadia/TerminalControl/TermControl.cpp | 4 +- src/interactivity/win32/Clipboard.cpp | 2 +- 4 files changed, 130 insertions(+), 192 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index e1e0ecfd3a6..9164282d99a 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -6,10 +6,12 @@ #include "textBuffer.hpp" #include "CharRow.hpp" +#include "../types/inc/utils.hpp" #include "../types/inc/convert.hpp" #pragma hdrstop +using namespace Microsoft::Console; using namespace Microsoft::Console::Types; // Routine Description: @@ -1033,238 +1035,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) { - 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 = - "Windows Console Host"; - size_t const cbHtmlHeader = szHtmlHeader.size(); - - std::string const szHtmlFragStart = ""; - std::string const szHtmlFragEnd = ""; - std::string const szHtmlFooter = ""; - size_t const cbHtmlFooter = szHtmlFooter.size(); - - std::string const szDivOuterBackgroundPattern = R"X(
)X"; - - size_t const cbDivOuter = 55; - std::string szDivOuter; - szDivOuter.reserve(cbDivOuter); - - std::string const szSpanFontSizePattern = R"X()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()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()X"; - size_t const cbSpanStartFontPattern = 41; - - std::string const szSpanStartFontConstant = R"X()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 = + "" + htmlTitle + ""; + htmlBuilder << htmlHeader; - std::string szSpanStartFont; - size_t cbSpanStartFont; - bool fDeleteSpanStartFont = false; + htmlBuilder << ""; - 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(cchFontFaceName), nullptr, 0, nullptr, nullptr); - std::string szBuffer; - szBuffer.resize(cbNeeded); + htmlBuilder << "
(cchFontFaceName), szBuffer.data(), cbNeeded, nullptr, nullptr); + htmlBuilder << "font-size:"; + htmlBuilder << fontHeightPoints; + htmlBuilder << "pt;"; - // format converted font name into pattern - std::string const szFinalFontPattern = R"X()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 + htmlBuilder << "px;"; - fDeleteSpanStartFont = true; - szSpanStartFont = szFinalFontPattern; - cbSpanStartFont = cbBytesNeeded; - } - else - { - szSpanStartFont = szSpanStartFontConstant; - cbSpanStartFont = cbSpanStartFontConstant; + htmlBuilder << "\">"; } - std::string const szSpanEnd = ""; - std::string const szDivEnd = "
"; - - // 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 fgColor = std::nullopt; + std::optional 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 << "
"; + } - 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 '
' 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(cchCharsToPrint), nullptr, 0, nullptr, nullptr); - TempBuff.resize(cbTempCharsNeeded); - WideCharToMultiByte(CP_UTF8, 0, rows.text[iRow].data() + cbStartOffset, static_cast(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 << "
"; + } - // accumulate 1 character - cchCharsToPrint++; - } + 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(cchCharsToPrint), nullptr, 0, nullptr, nullptr); - CharsConverted.resize(cbCharsConverted); - WideCharToMultiByte(CP_UTF8, 0, pwchAccumulateStart, static_cast(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 << ""; } - // 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 << "
"; - // copy end background color span - szClipboard.append(szDivEnd); + htmlBuilder << ""; - // copy HTML end fragment - szClipboard.append(szHtmlFragEnd); + constexpr std::string_view HtmlFooter = ""; + 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; - // 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 {}; } - - return szClipboard; } diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 78025086b0e..0143f2b88f7 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -145,8 +145,9 @@ class TextBuffer final std::function 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 _storage; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 28bc0562759..955fd05b2dd 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -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 diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp index ea58120b096..54a48b4048a 100644 --- a/src/interactivity/win32/Clipboard.cpp +++ b/src/interactivity/win32/Clipboard.cpp @@ -277,7 +277,7 @@ void Clipboard::CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, { const auto& fontData = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetCurrentFont(); int const iFontHeightPoints = fontData.GetUnscaledSize().Y * 72 / ServiceLocator::LocateGlobals().dpi; - std::string HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName()); + std::string HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), "Windows Console Host"); const size_t cbNeededHTML = HTMLToPlaceOnClip.size(); if (cbNeededHTML) {