Files
hrynco-kswitcher/src/richtext.cpp
T

305 lines
9.5 KiB
C++

#include "richtext.h"
#include <algorithm>
#include <cctype>
#include <richedit.h>
#include <string_view>
namespace ks
{
namespace
{
std::string escape(const std::wstring &text)
{
std::string out;
for (wchar_t c : text)
{
if (c == '\r')
continue;
if (c == '\n')
out += "\\line ";
else if (c == '\t')
out += "\\tab ";
else if (c == '\\' || c == '{' || c == '}')
{
out += '\\';
out += char(c);
}
else if (c >= 32 && c < 127)
out += char(c);
else
out += "\\u" + std::to_string((int)(short)c) + "?";
}
return out;
}
const std::string header = "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Segoe UI;}}\\f0\\fs21\\uc1 ";
std::wstring entities(std::string_view input)
{
std::wstring out;
for (size_t i = 0; i < input.size();)
{
if (input[i] != '&')
{
auto end = input.find('&', i);
if (end == input.npos)
end = input.size();
out += wide(std::string(input.substr(i, end - i)));
i = end;
continue;
}
auto end = input.find(';', i + 1);
if (end != input.npos && end - i < 14)
{
auto name = std::string(input.substr(i + 1, end - i - 1));
uint32_t c = 0;
if (name == "amp")
c = '&';
else if (name == "lt")
c = '<';
else if (name == "gt")
c = '>';
else if (name == "quot")
c = '"';
else if (name == "apos")
c = '\'';
else if (name == "nbsp")
c = 160;
else if (name.starts_with('#'))
try
{
size_t used{};
bool hex = name.size() > 1 && (name[1] == 'x' || name[1] == 'X');
auto digits = name.substr(hex ? 2 : 1);
auto n = std::stoul(digits, &used, hex ? 16 : 10);
if (used == digits.size() && n <= 0x10ffff && !(n >= 0xd800 && n <= 0xdfff))
c = (uint32_t)n;
}
catch (...)
{
}
if (c)
{
if (c > 0xffff)
{
c -= 0x10000;
out += wchar_t(0xd800 + (c >> 10));
out += wchar_t(0xdc00 + (c & 1023));
}
else
out += wchar_t(c);
i = end + 1;
continue;
}
}
out += L'&';
++i;
}
return out;
}
std::string htmlRtf(std::string html)
{
auto start = html.find("<!--StartFragment-->");
auto end = html.find("<!--EndFragment-->");
if (start != html.npos && end != html.npos && end >= start + 20)
html = html.substr(start + 20, end - start - 20);
else
{
auto tag = html.find('<');
if (tag == html.npos)
return {};
html.erase(0, tag);
}
std::string out = header;
std::vector<std::string> stack;
std::string skip;
size_t chars = 0;
bool any = false;
for (size_t i = 0; i < html.size() && chars < 4096 && out.size() < 55000;)
{
if (html[i] != '<')
{
auto next = html.find('<', i);
if (next == html.npos)
next = html.size();
if (skip.empty())
{
auto text = entities(std::string_view(html).substr(i, next - i));
text.resize(std::min(text.size(), 4096 - chars));
chars += text.size();
out += escape(text);
any |= !text.empty();
}
i = next;
continue;
}
if (html.compare(i, 4, "<!--") == 0)
{
auto e = html.find("-->", i + 4);
if (e == html.npos)
break;
i = e + 3;
continue;
}
size_t e = i + 1;
char quote = 0;
for (; e < html.size(); ++e)
{
char c = html[e];
if (quote)
{
if (c == quote)
quote = 0;
}
else if (c == '\'' || c == '"')
quote = c;
else if (c == '>')
break;
}
if (e == html.size())
break;
auto tag = html.substr(i + 1, e - i - 1);
i = e + 1;
std::transform(tag.begin(), tag.end(), tag.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
size_t p = 0;
while (p < tag.size() && isspace((unsigned char)tag[p]))
++p;
bool close = p < tag.size() && tag[p] == '/';
if (close)
++p;
size_t begin = p;
while (p < tag.size() && (isalnum((unsigned char)tag[p]) || tag[p] == '-'))
++p;
auto name = tag.substr(begin, p - begin);
if (!skip.empty())
{
if (close && name == skip)
skip.clear();
continue;
}
if (name == "script" || name == "style" || name == "head")
{
if (!close)
skip = name;
continue;
}
if (name.empty())
continue;
bool block = name == "p" || name == "div" || name == "li" || name == "tr" || name == "blockquote";
if (close)
{
auto it = std::find(stack.rbegin(), stack.rend(), name);
if (it != stack.rend())
{
size_t n = std::distance(stack.rbegin(), it) + 1;
out.append(n, '}');
stack.resize(stack.size() - n);
}
if (block && any)
out += "\\line ";
continue;
}
if (name == "br" || name == "hr")
{
out += "\\line ";
continue;
}
if (name == "img" || name == "meta" || name == "link" || name == "input" || tag.ends_with('/'))
continue;
if (stack.size() >= 128)
return {};
if (block && any && !out.ends_with("\\line "))
out += "\\line ";
out += '{';
stack.push_back(name);
std::string compact = tag;
std::erase_if(compact, [](unsigned char c) { return isspace(c); });
if (name == "b" || name == "strong" || compact.find("font-weight:bold") != compact.npos ||
compact.find("font-weight:700") != compact.npos ||
compact.find("font-weight:600") != compact.npos)
out += "\\b ";
if (name == "i" || name == "em" || compact.find("font-style:italic") != compact.npos)
out += "\\i ";
if (name == "u" || compact.find("text-decoration:underline") != compact.npos ||
compact.find("text-decoration-line:underline") != compact.npos)
out += "\\ul ";
if (name == "s" || name == "strike" || name == "del" || compact.find("line-through") != compact.npos)
out += "\\strike ";
if (compact.find("font-weight:normal") != compact.npos ||
compact.find("font-weight:400") != compact.npos)
out += "\\b0 ";
if (compact.find("font-style:normal") != compact.npos)
out += "\\i0 ";
if (name == "li")
out += "\\bullet\\tab ";
}
if (!any)
return {};
out.append(stack.size(), '}');
out += '}';
return out;
}
struct Stream
{
const std::string &text;
size_t at{};
};
DWORD CALLBACK readStream(DWORD_PTR cookie, LPBYTE buffer, LONG size, LONG *read)
{
auto &s = *(Stream *)cookie;
*read = (LONG)std::min(size_t(size), s.text.size() - s.at);
memcpy(buffer, s.text.data() + s.at, *read);
s.at += *read;
return 0;
}
} // namespace
std::string richPreview(const Clip &clip)
{
for (const auto &f : clip.formats)
if (f.name == L"Rich Text Format" && f.data.size() <= 65536)
{
std::string rtf(f.data.begin(), f.data.end());
while (!rtf.empty() && rtf.back() == 0)
rtf.pop_back();
if (rtf.starts_with("{\\rtf"))
return rtf;
}
for (const auto &f : clip.formats)
if (f.name == L"HTML Format" && f.data.size() <= 1024 * 1024)
{
try
{
auto rtf = htmlRtf(std::string(f.data.begin(), f.data.end()));
if (!rtf.empty())
return rtf;
}
catch (...)
{
}
}
auto text = clip.text();
if (text.size() > 4096)
text.resize(4096);
return header + escape(text) + '}';
}
HWND createRichText(HWND parent, int id, bool visible)
{
static HMODULE rich = LoadLibraryExW(L"Msftedit.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!rich)
return nullptr;
return CreateWindowExW(0, L"RICHEDIT50W", L"",
WS_CHILD | ES_MULTILINE | ES_READONLY |
(visible ? (WS_VISIBLE | WS_VSCROLL | ES_AUTOVSCROLL) : 0),
0, 0, 1, 1, parent, (HMENU)(INT_PTR)id, GetModuleHandleW(nullptr), nullptr);
}
bool setRichText(HWND window, const std::string &text)
{
if (!window)
return false;
Stream data{text};
EDITSTREAM stream{(DWORD_PTR)&data, 0, readStream};
SendMessageW(window, EM_STREAMIN, SF_RTF, (LPARAM)&stream);
SendMessageW(window, EM_EMPTYUNDOBUFFER, 0, 0);
return stream.dwError == 0;
}
} // namespace ks