24 using HelpClock = std::chrono::steady_clock;
29 return {
reinterpret_cast<const char*
>(
text.data()),
text.size()};
33 std::string
fileUrl(
const std::filesystem::path&
path)
35 const auto generic =
path.generic_u8string();
37 std::string
url =
text.starts_with(
"//") ?
"file:" :
38 text.starts_with(
'/') ?
"file://" :
"file:///";
39 constexpr char hex[] =
"0123456789ABCDEF";
40 for (
const unsigned char byte :
text)
42 if ((
byte >=
'a' &&
byte <=
'z') || (
byte >=
'A' &&
byte <=
'Z') ||
43 (
byte >=
'0' &&
byte <=
'9') ||
byte ==
'/' ||
byte ==
':' ||
44 byte ==
'-' ||
byte ==
'_' ||
byte ==
'.' ||
byte ==
'~')
45 url.push_back(
static_cast<char>(
byte));
49 url.push_back(
hex[
byte >> 4]);
50 url.push_back(
hex[
byte & 15]);
60 std::atomic<bool> completed{
false};
74 return std::unexpected(
"Cannot initialize Full Help browser dispatch: " +
75 std::to_string(
static_cast<unsigned long>(initialized)));
77 request.cbSize =
sizeof(request);
79 request.lpVerb =
L"open";
85 if (!
opened)
return std::unexpected(
"Cannot open Full Help in the default browser: " +
86 std::to_string(
error));
103 std::function<std::expected<void, std::string>(
const std::filesystem::path&)>
openDocument;
137 std::array<char, 2048>
buffer{};
138 for (
int read = 0; read < 8; ++read)
144 return std::unexpected(std::string(
"Full Help exporter pipe: ") +
SDL_GetError());
145 if (
bytes == 0)
break;
158 return std::unexpected(std::string(
"Full Help cache directory: ") +
SDL_GetError());
159 const auto root = std::filesystem::u8path(
raw) /
"full-help";
161 std::error_code
error;
162 std::filesystem::create_directories(
root,
error);
163 if (
error)
return std::unexpected(
"Cannot create Full Help cache: " +
error.message());
164 const auto stamp = HelpClock::now().time_since_epoch().count();
176 return std::unexpected(
"Cannot create Full Help export: " +
error.message());
179 return std::unexpected(
"Cannot allocate a unique Full Help export directory");
189 0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
190 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0,
191 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
192 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178};
194 result.reserve(
bytes.size());
195 for (
const unsigned char byte :
bytes)
198 return std::unexpected(
"retail quick-help text contains an embedded NUL");
199 const std::uint16_t code =
byte >= 0x80 &&
byte <= 0x9F
202 return std::unexpected(
"retail quick-help text contains an undefined Windows-1252 byte");
203 if (code < 0x80) result.push_back(
static_cast<char>(code));
204 else if (code < 0x800)
206 result.push_back(
static_cast<char>(0xC0 | (code >> 6)));
207 result.push_back(
static_cast<char>(0x80 | (code & 0x3F)));
211 result.push_back(
static_cast<char>(0xE0 | (code >> 12)));
212 result.push_back(
static_cast<char>(0x80 | ((code >> 6) & 0x3F)));
213 result.push_back(
static_cast<char>(0x80 | (code & 0x3F)));
222 std::snprintf(
name,
sizeof(
name),
fullHelp ?
"mono%02u.hlp" :
"qhelp%02u.txt",
223 static_cast<unsigned>(language));
230 const auto path = resources.paths().resolve(
helpFileName(resources.context().language,
false));
231 if (!
path)
return std::unexpected(
path.error().detail);
232 std::ifstream file(*
path, std::ios::binary);
233 if (!file)
return std::unexpected(
"cannot open retail quick-help file");
234 std::string
text((std::istreambuf_iterator<char>(file)), {});
235 if (file.bad())
return std::unexpected(
"cannot read retail quick-help file");
239 state.quickHelpLines.clear();
240 state.quickHelpFirstLine = 0;
241 state.quickHelpPageOffsets.clear();
242 state.quickHelpPageIndex = 0;
243 state.quickHelpInitialPage =
true;
244 state.quickHelpVisible =
true;
251 if (
fullHelpJob)
return std::unexpected(
"A Full Help export is already running");
256 return std::unexpected(
"A previous Full Help browser request is still pending");
261 return std::unexpected(
"Invalid Full Help export limits");
262 const auto path = resources.paths().resolve(
helpFileName(resources.context().language,
true));
263 if (!
path)
return std::unexpected(
path.error().detail);
266 std::ifstream
input(*
path, std::ios::binary);
267 std::array<unsigned char, 4>
signature{};
269 if (!
input ||
signature != std::array<unsigned char, 4>{0x3F, 0x5F, 0x03, 0x00})
270 return std::unexpected(
"Full Help resource is not a readable WinHelp HLP file");
272 auto job = std::make_unique<FullHelpJob>();
282 const auto* configured =
SDL_getenv(
"MONOPOLY_WINHLP");
283 exporter = configured && *configured ? configured :
"winhlp";
285 if (
exporter.find(
'\0') != std::string::npos)
286 return std::unexpected(
"Full Help exporter executable contains an embedded NUL");
287 const std::array<std::string, 6>
arguments{
289 std::array<const char*, 7>
argv{};
294 return std::unexpected(std::string(
"Full Help process properties: ") +
SDL_GetError());
308 return std::unexpected(
"Cannot start the Full Help exporter; install winhlp with HTML/image "
309 "support or set MONOPOLY_WINHLP to its executable: " +
processError);
316 if (!
job->statusHandle)
317 return std::unexpected(
"Cannot retain Full Help exporter exit status: " +
322 if (!
job->errors)
return std::unexpected(
"Full Help exporter diagnostic pipe unavailable");
331 for(std::size_t
i=0;
i<
html.size();)
333 const auto first=
static_cast<unsigned char>(
html[
i++]);
334 if(
first<0x80)
continue;
335 unsigned count{},code{},minimum{};
343 const auto next=
static_cast<unsigned char>(
html[
i++]);
344 if((
next&0xC0)!=0x80)
return {};
345 code=(code<<6)|(
next&63);
350 std::transform(
lower.begin(),
lower.end(),
lower.begin(),[](
unsigned char c){return char(c>=
'A' && c<=
'Z'?c+32:c);});
353 lower.find(
"<!doctype html>")==std::string::npos ||
lower.find(
"<html")==std::string::npos ||
lower.find(
"</html>")==std::string::npos ||
354 lower.find(
"<meta charset=\"utf-8\">",
head)>=
endHead ||
lower.find(
"<nav>",body)==std::string::npos ||
355 lower.find(
"</nav>",body)==std::string::npos ||
lower.find(
"<section id=\"topic-",body)==std::string::npos ||
356 lower.find(
"</section>",body)==std::string::npos ||
lower.find(
"</body>",body)==std::string::npos ||
357 lower.find(
"monopoly-modern-help")!=std::string::npos)
return {};
358 constexpr std::string_view
theme=R
"HELP(
359<meta name="viewport" content="width=device-width, initial-scale=1">
360<style id="monopoly-modern-help">
361html{background:#102e30;color:#203536;color-scheme:light;scroll-behavior:auto}
362body{font-family:"Segoe UI",Arial,sans-serif;max-width:72rem;margin:0 auto;padding:clamp(1rem,4vw,3.5rem);line-height:1.65}
363body>h1{color:#f4efdf;font-size:clamp(1.7rem,3vw,2.3rem);font-weight:600;letter-spacing:.02em;margin:0 0 1.4rem;padding:.2rem 0 .2rem 1rem;border-left:4px solid #bc9c59}
364nav{font-size:.95rem;line-height:1.35;background:#163c3e;border:1px solid #887344;border-radius:14px;padding:clamp(1rem,3vw,2rem);margin-bottom:2rem;box-shadow:0 12px 32px #0003}
365nav ul{columns:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.05rem 1.5rem;list-style:none;padding:0;margin:0}
366nav a{display:block;color:#f1e9d3;padding:.12rem .65rem;border-radius:5px;text-decoration:none;overflow-wrap:anywhere}
367nav a:hover{background:#244e4e;color:#fff}
368a{color:#215c60;text-underline-offset:.18em;overflow-wrap:anywhere}
369a:hover{color:#103d40}
370a:focus-visible{outline:3px solid #bc9c59;outline-offset:3px;border-radius:3px}
371section{background:#f5f0e2;border:1px solid #c2ad7a;border-radius:14px;padding:clamp(1rem,3vw,2rem);margin-top:2rem;box-shadow:0 12px 32px #0003;scroll-margin-top:1rem;overflow-wrap:anywhere}
372section:target{border-color:#d4b367}
373.topic-meta{color:#566665;font-size:.85rem}
374.nonscroll{background:#ebe3cf;border-left:3px solid #b79754;border-radius:5px;padding:.65rem 1rem}
375.f0,.f3,.f4,.f6,.f9{font-family:"Segoe UI",Arial,sans-serif}
376.f1,.f2,.f5,.f7,.f8{font-family:"Segoe UI",Arial,sans-serif}
377section p{line-height:1.65;margin-block:.65rem!important}
378table{max-width:100%;border-collapse:collapse}td{border-color:#baa97f;padding:.5rem .7rem}img{max-width:100%;height:auto}
379@media(max-width:640px){nav a{padding:.5rem .65rem}nav ul{grid-template-columns:1fr}body{padding:1rem}section,nav{border-radius:10px;padding:1rem}.f0{font-size:1.6rem}.f3{font-size:1.35rem}}
380@media print{html{background:white}body{max-width:none;padding:0}body>h1{color:#203536}nav,section{box-shadow:none;border-color:#999}a{color:inherit}}
394 auto fail = [](std::string message) -> std::expected<bool, std::string>
397 return std::unexpected(std::move(message));
401 if (
job.browserDispatch)
403 if (!
job.browserDispatch->completed.load(std::memory_order_acquire))
405 if (HelpClock::now() >=
job.deadline)
406 return fail(
"Full Help browser dispatch timed out; the Windows request may still complete");
409 const auto error =
job.browserDispatch->error;
412 if (!
error.empty())
return std::unexpected(
error);
420 const auto size = std::filesystem::file_size(
job.document,
sizeError);
422 return fail(
"Full Help HTML exceeds the configured size limit");
425 if (HelpClock::now() >=
job.deadline)
426 return fail(
"Full Help export timed out: " +
job.diagnostics);
432 return fail(
"Cannot query Full Help exporter exit status: " + std::to_string(
GetLastError()));
437 return fail(
"Full Help export failed (exit " + std::to_string(
exitCode) +
"): " +
job.diagnostics);
439 return fail(
"Full Help exporter did not produce a readable HTML document");
442 std::ifstream file(
job.document, std::ios::binary);
443 std::string
html(
static_cast<std::size_t
>(size),
'\0');
444 file.read(
html.data(),
static_cast<std::streamsize
>(
html.size()));
445 if (!file)
return fail(
"Cannot read the completed Full Help HTML export");
448 std::transform(
html.begin(),
html.end(),
html.begin(), [](
unsigned char ch)
450 return static_cast<char>(ch >=
'A' && ch <=
'Z' ? ch + (
'a' -
'A') : ch);
452 if (
html.find(
"<html") == std::string::npos ||
html.find(
"</html>") == std::string::npos)
453 return fail(
"Full Help exporter produced an incomplete HTML document");
460 std::error_code
error;
461 if(!
output){std::filesystem::remove(
temporary,
error);
return fail(
"Cannot write the themed Full Help export");}
468 if(!
replaced){std::filesystem::remove(
temporary,
error);
return fail(
"Cannot publish the themed Full Help export");}
471 auto dispatch = std::make_shared<BrowserDispatch>();
480 const auto opened = opener ? opener(document) : openNativeDocument(document);
481 if (!opened) dispatch->error = opened.error();
484 catch (...) {
dispatch->error =
"Full Help browser dispatch failed"; }
485 dispatch->completed.store(
true, std::memory_order_release);
488 catch (
const std::exception&
error)
490 return fail(std::string(
"Cannot start Full Help browser dispatch: ") +
error.what());
493 job.retainDocument =
true;
496 job.deadline = HelpClock::now() + std::chrono::milliseconds(
job.timeoutMilliseconds);
500 const auto opened =
job.openDocument ?
job.openDocument(
job.document) :
502 std::expected<void, std::string>{std::unexpected(
503 std::string(
"Cannot open Full Help in the default browser: ") +
SDL_GetError())});
505 job.retainDocument =
true;
std::vector< std::byte > bytes
std::uintmax_t maximumHtmlBytes
std::function< std::expected< void, std::string >(const std::filesystem::path &)> openDocument
HelpClock::time_point deadline
std::filesystem::path directory
std::filesystem::path document
std::uint32_t timeoutMilliseconds
std::deque< Bytes > output
void cancelFullHelp() noexcept
bool fullHelpPending() noexcept
std::expected< void, std::string > openQuickHelp(State &state, const data::ResourceSnapshot &resources)
std::expected< std::string, std::string > decodeQuickHelpText(std::string_view bytes)
std::optional< std::string > styleFullHelpHtml(std::string_view html, std::uintmax_t maximumBytes)
std::expected< bool, std::string > pollFullHelp()
std::expected< void, std::string > openFullHelp(const data::ResourceSnapshot &resources, const FullHelpOptions &options)
std::string helpFileName(data::LanguageId language, bool fullHelp)
std::function< std::expected< void, std::string >(const std::filesystem::path &)> openDocument
std::uintmax_t maximumHtmlBytes
std::string exporterExecutable
std::uint32_t timeoutMilliseconds