305 lines
9.5 KiB
C++
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
|