Skip to content

Commit f730e3c

Browse files
committed
Add script examples to the embedded manual
Bundle all 48 website scripts for offline browsing. Match the website's compact selector, metadata, discussion links and arrow navigation, with the example order defined in Markdown. Open the Run PDF Script page in the embedded manual from the Help button in both original and ng. Validation: both Windows debug builds, the PDF script test, browser checks and the running app's Help and examples pages. prompt: Match the website's script examples in the in-app manual and open in-program docs from the Run PDF Script Help button
1 parent c845f3d commit f730e3c

6 files changed

Lines changed: 264 additions & 5 deletions

File tree

‎cmd/gen-docs.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const mdDir = websiteDocsDir;
1414

1515
const kMainPage = "SumatraPDF-documentation.md";
1616
const kAllDocsFile = "all-docs.md";
17+
const kScriptExamplesPage = "Tool-run-javascript-examples.md";
1718
// hosted on the website, not in this repo
1819
const kExcludeFromAllDocs = new Set(["SumatraPDF-all-docs-for-llm-ai.md"]);
1920

@@ -134,6 +135,38 @@ function writeBundledRenderJs(outDir: string): void {
134135
writeFileSync(join(outDir, "gen_docs.render.js"), template.replace(marker, bundle));
135136
}
136137

138+
function copyScriptExamples(outDir: string, pages: string[]): void {
139+
if (!pages.includes(kScriptExamplesPage)) {
140+
return;
141+
}
142+
const text = readFileSync(join(mdDir, kScriptExamplesPage), "utf-8");
143+
const examples = /^## Examples\r?\n([\s\S]*?)(?=^## |$(?![\s\S]))/m.exec(text);
144+
if (!examples) {
145+
throw new Error(`${kScriptExamplesPage}: missing Examples section`);
146+
}
147+
const links = [...examples[1].matchAll(/^- \[[^\]]+\]\(scripts\/([a-zA-Z0-9_-]+\.js)\)\s*$/gm)];
148+
if (!links.length) {
149+
throw new Error(`${kScriptExamplesPage}: no script examples`);
150+
}
151+
const names = new Set<string>();
152+
const scriptsDir = join(mdDir, "scripts");
153+
mkdirSync(join(outDir, "scripts"), { recursive: true });
154+
for (const link of links) {
155+
const name = link[1];
156+
if (names.has(name)) {
157+
throw new Error(`${kScriptExamplesPage}: duplicate script ${name}`);
158+
}
159+
names.add(name);
160+
copyFileNormalized(join(outDir, "scripts", name), join(scriptsDir, name));
161+
}
162+
for (const file of readdirSync(scriptsDir, { withFileTypes: true })) {
163+
if (file.isFile() && file.name.endsWith(".js") && !names.has(file.name)) {
164+
throw new Error(`${kScriptExamplesPage}: script ${file.name} is not listed`);
165+
}
166+
}
167+
console.log(`staged ${names.size} PDF script examples`);
168+
}
169+
137170
// Build the manual in a scratch dir and swap it in at the end: a build's
138171
// prebuild (cmd/pack-embedded-prebuild.cmd) mirrors .work/docs and packs
139172
// without the manual when the dir is missing, so it must never see a
@@ -151,6 +184,8 @@ function writeManualPakFiles(pages: string[]): void {
151184
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
152185
console.log(`wrote manifest.json (${Object.keys(manifest).length} pages)`);
153186

187+
copyScriptExamples(outDir, pages);
188+
154189
genAllDocsMd(outDir);
155190

156191
for (const name of kManualStaticFiles) {

‎docs/gen_docs.render.js‎

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
let manifest = null;
2222
let mainDocText = null;
23+
let scriptExamplesCleanup = null;
24+
const kScriptExamplesPage = "Tool-run-javascript-examples.md";
2325

2426
function removeNotionId(s) {
2527
if (s.length <= 32) return s;
@@ -428,10 +430,12 @@
428430
text = text.replace(/^# .+\n?/, "");
429431
}
430432

431-
text = preProcess(text);
432-
433433
const md = global.markdownit({ html: true, typographer: true });
434434
createMarkdownRenderer(md);
435+
if (mdName === kScriptExamplesPage) {
436+
text = renderScriptExamples(text, md.utils.escapeHtml);
437+
}
438+
text = preProcess(text);
435439
let innerHTML = md.render(text);
436440

437441
if (h1Text && !isMainPage) {
@@ -475,6 +479,163 @@
475479
});
476480
}
477481

482+
function renderScriptExamples(text, escape) {
483+
return text.replace(/^## Examples\r?\n([\s\S]*?)(?=^## |$(?![\s\S]))/m, function (_, list) {
484+
const links = Array.from(list.matchAll(/^- \[([^\]]+)\]\(scripts\/([a-zA-Z0-9_-]+\.js)\)\s*$/gm));
485+
if (!links.length) throw new Error("No script examples listed.");
486+
const items = links.map(function (link, index) {
487+
const title = escape(link[1]);
488+
const name = escape(link[2]);
489+
const label =
490+
index === 0 ? '<span aria-current="true">' + title + "</span>" : '<a href="#' + name + '">' + title + "</a>";
491+
return '<span data-script="' + name + '" data-title="' + title + '">' + label + "</span>";
492+
});
493+
return (
494+
'<div class="script-examples" data-selected="">\n' +
495+
'<nav class="script-example-list" aria-label="JavaScript examples">' +
496+
items.join('<span class="script-example-dot" aria-hidden="true"> &bull; </span>') +
497+
"</nav>\n" +
498+
'<p class="script-example-hint">Choose an example, or use the left and right arrow keys.</p>\n' +
499+
'<div class="script-example-head"><h2 class="script-example-title" id="script-example-title">' +
500+
escape(links[0][1]) +
501+
'</h2><div class="script-example-arrows">' +
502+
'<button type="button" data-step="-1" aria-label="Previous example" title="Previous example (left arrow)">&larr;</button>' +
503+
'<button type="button" data-step="1" aria-label="Next example" title="Next example (right arrow)">&rarr;</button>' +
504+
"</div></div>\n" +
505+
'<div class="script-example-status" role="status"></div>\n' +
506+
'<div class="script-example-content"><p>Loading...</p></div>\n</div>\n\n'
507+
);
508+
});
509+
}
510+
511+
function scriptContentHTML(name, source) {
512+
const escape = global.markdownit().utils.escapeHtml;
513+
let info = "";
514+
const args = [];
515+
source.split("\n").forEach(function (line) {
516+
line = line.trim();
517+
if (line.startsWith("// Info:")) info = line.slice("// Info:".length).trim();
518+
if (line.startsWith("// Arg:")) args.push(line.slice("// Arg:".length).trim());
519+
});
520+
let html = info ? "<p>" + escape(info) + "</p>" : "";
521+
const discussion = /^discussion-([0-9]+)-/.exec(name);
522+
if (discussion) {
523+
html +=
524+
'<p>From <a href="https://github.com/sumatrapdfreader/sumatrapdf/discussions/' +
525+
discussion[1] +
526+
'" target="_blank" rel="noopener noreferrer">discussion #' +
527+
discussion[1] +
528+
"</a></p>";
529+
}
530+
html +=
531+
'<p><a href="https://www.sumatrapdfreader.org/docs/scripts/' +
532+
escape(name) +
533+
'" target="_blank" rel="noopener noreferrer">Download script</a></p>';
534+
if (args.length) {
535+
html +=
536+
'<ol class="script-example-args" aria-label="Script arguments">' +
537+
args
538+
.map(function (arg) {
539+
return "<li>" + escape(arg) + "</li>";
540+
})
541+
.join("") +
542+
"</ol>";
543+
}
544+
return html + genCodeBlockHTML(escape(source), "language-js");
545+
}
546+
547+
function initScriptExamples(root) {
548+
if (scriptExamplesCleanup) scriptExamplesCleanup();
549+
scriptExamplesCleanup = null;
550+
const examples = root && root.querySelector(".script-examples");
551+
if (!examples) return Promise.resolve();
552+
const items = Array.from(examples.querySelectorAll("[data-script]"));
553+
const panel = examples.querySelector(".script-example-content");
554+
const title = examples.querySelector(".script-example-title");
555+
const status = examples.querySelector(".script-example-status");
556+
const cache = new Map();
557+
let selected = 0;
558+
let requested = 0;
559+
let requestID = 0;
560+
let disposed = false;
561+
562+
async function select(index) {
563+
requested = index;
564+
const currentRequest = ++requestID;
565+
const name = items[index].dataset.script;
566+
panel.setAttribute("aria-busy", "true");
567+
status.textContent = "";
568+
try {
569+
if (!cache.has(name)) {
570+
cache.set(name, scriptContentHTML(name, await fetchText("scripts/" + name)));
571+
}
572+
if (disposed || currentRequest !== requestID) return;
573+
selected = index;
574+
items.forEach(function (item, i) {
575+
const label = document.createElement(i === index ? "span" : "a");
576+
label.textContent = item.dataset.title;
577+
if (i === index) label.setAttribute("aria-current", "true");
578+
else label.href = "#" + item.dataset.script;
579+
item.replaceChildren(label);
580+
});
581+
title.textContent = items[index].dataset.title;
582+
panel.innerHTML = cache.get(name);
583+
examples.dataset.selected = name;
584+
if (typeof global.rebuildPageToc === "function") global.rebuildPageToc();
585+
} catch (err) {
586+
if (disposed || currentRequest !== requestID) return;
587+
requested = selected;
588+
status.textContent = "Could not load that example. Choose it again to retry.";
589+
} finally {
590+
if (!disposed && currentRequest === requestID) panel.removeAttribute("aria-busy");
591+
}
592+
}
593+
594+
function navigate(index) {
595+
global.history.pushState(null, "", "#" + items[index].dataset.script);
596+
return select(index);
597+
}
598+
function step(direction) {
599+
return navigate((requested + direction + items.length) % items.length);
600+
}
601+
examples.querySelectorAll("[data-step]").forEach(function (button) {
602+
button.addEventListener("click", function () {
603+
step(Number(button.dataset.step));
604+
});
605+
});
606+
examples.querySelector("nav").addEventListener("click", function (event) {
607+
const link = event.target.closest("a");
608+
if (!link || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
609+
const index = items.indexOf(link.closest("[data-script]"));
610+
if (index < 0) return;
611+
event.preventDefault();
612+
navigate(index);
613+
});
614+
function onKey(event) {
615+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
616+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
617+
if (event.target.closest("input, textarea, select, [contenteditable]")) return;
618+
event.preventDefault();
619+
step(event.key === "ArrowRight" ? 1 : -1);
620+
}
621+
function selectHash() {
622+
const index = items.findIndex(function (item) {
623+
return "#" + item.dataset.script === global.location.hash;
624+
});
625+
return select(index < 0 ? 0 : index);
626+
}
627+
document.addEventListener("keydown", onKey);
628+
global.addEventListener("popstate", selectHash);
629+
global.addEventListener("hashchange", selectHash);
630+
scriptExamplesCleanup = function () {
631+
disposed = true;
632+
document.removeEventListener("keydown", onKey);
633+
global.removeEventListener("popstate", selectHash);
634+
global.removeEventListener("hashchange", selectHash);
635+
};
636+
return selectHash();
637+
}
638+
478639
function ensureManifest() {
479640
if (manifest) return Promise.resolve(manifest);
480641
return fetchText("manifest.json").then(function (text) {
@@ -557,8 +718,10 @@
557718
titleEl.textContent = title;
558719
}
559720
if (mdName === "Commands.md") {
721+
initScriptExamples(innerSlot);
560722
return injectCommandsSearch(innerSlot);
561723
}
724+
return initScriptExamples(innerSlot);
562725
});
563726
}
564727

‎docs/manual.css‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,3 +457,64 @@ html[data-theme="dark"] .notion-page a:hover {
457457
line-height: 1.4;
458458
letter-spacing: -0.005em;
459459
}
460+
461+
.script-example-list {
462+
line-height: 1.8;
463+
color: var(--text-secondary);
464+
}
465+
466+
.script-example-list a {
467+
color: inherit;
468+
text-decoration: none;
469+
}
470+
471+
.script-example-list a:hover {
472+
text-decoration: underline;
473+
}
474+
475+
.script-example-list [aria-current="true"] {
476+
color: #000;
477+
}
478+
479+
html[data-theme="dark"] .script-example-list [aria-current="true"] {
480+
color: var(--text-primary);
481+
}
482+
483+
.script-example-dot,
484+
.script-example-hint {
485+
color: var(--text-secondary);
486+
}
487+
488+
.script-example-hint {
489+
font-size: 0.9em;
490+
}
491+
492+
.script-example-head {
493+
display: flex;
494+
align-items: center;
495+
justify-content: space-between;
496+
gap: 1rem;
497+
}
498+
499+
.script-example-arrows {
500+
display: flex;
501+
gap: 0.5rem;
502+
}
503+
504+
.script-example-arrows button {
505+
padding: 0.15rem 0.6rem;
506+
border: 1px solid var(--border);
507+
border-radius: 4px;
508+
background: var(--bg-elevated);
509+
color: var(--text-secondary);
510+
font-size: 1.2rem;
511+
cursor: pointer;
512+
}
513+
514+
.script-example-arrows button:hover {
515+
color: var(--text-primary);
516+
}
517+
518+
.script-example-content[aria-busy="true"] {
519+
opacity: 0.6;
520+
}

‎src/PDFScriptDialog.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ void PDFScriptWnd::OnExample() {
168168
}
169169

170170
void PDFScriptWnd::OnHelp() {
171-
SumatraLaunchBrowser(Str(kPDFScriptHelpUrl));
171+
LaunchDocumentation(StrL(kPDFScriptHelpDocUri));
172172
}
173173

174174
void PDFScriptWnd::OnCancel() {

‎src/ng/PDFScriptDialog.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ void PDFScriptView::OnRun(PDFScriptView*, gp::Ctx*, const gp::ClickEvent*) {
175175
}
176176

177177
void PDFScriptView::OnHelp(PDFScriptView*, gp::Ctx*, const gp::ClickEvent*) {
178-
SumatraLaunchBrowser(Str(kPDFScriptHelpUrl));
178+
LaunchDocumentation(StrL(kPDFScriptHelpDocUri));
179179
}
180180

181181
void PDFScriptView::OnCancel(PDFScriptView*, gp::Ctx*, const gp::ClickEvent*) {

‎src/shared/PDFScript.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ TempStr ExpandPDFScript(Str script, const StrVec& args);
1717
void StartPDFScript(Str script, const StrVec& args, const Func1<PDFScriptResult*>& onDone);
1818
void ShowPDFScriptDialog(MainWindow* win);
1919

20-
constexpr const char* kPDFScriptHelpUrl = "https://www.sumatrapdfreader.org/docs/Run-PDF-script";
20+
constexpr char kPDFScriptHelpDocUri[] = "/Run-PDF-script";

0 commit comments

Comments
 (0)