#include "richtext.h" #include #include #include #include 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(""); auto end = html.find(""); 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 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, "", 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