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)
{