|
20 | 20 |
|
21 | 21 | let manifest = null; |
22 | 22 | let mainDocText = null; |
| 23 | + let scriptExamplesCleanup = null; |
| 24 | + const kScriptExamplesPage = "Tool-run-javascript-examples.md"; |
23 | 25 |
|
24 | 26 | function removeNotionId(s) { |
25 | 27 | if (s.length <= 32) return s; |
|
428 | 430 | text = text.replace(/^# .+\n?/, ""); |
429 | 431 | } |
430 | 432 |
|
431 | | - text = preProcess(text); |
432 | | - |
433 | 433 | const md = global.markdownit({ html: true, typographer: true }); |
434 | 434 | createMarkdownRenderer(md); |
| 435 | + if (mdName === kScriptExamplesPage) { |
| 436 | + text = renderScriptExamples(text, md.utils.escapeHtml); |
| 437 | + } |
| 438 | + text = preProcess(text); |
435 | 439 | let innerHTML = md.render(text); |
436 | 440 |
|
437 | 441 | if (h1Text && !isMainPage) { |
|
475 | 479 | }); |
476 | 480 | } |
477 | 481 |
|
| 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"> • </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)">←</button>' + |
| 503 | + '<button type="button" data-step="1" aria-label="Next example" title="Next example (right arrow)">→</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 | + |
478 | 639 | function ensureManifest() { |
479 | 640 | if (manifest) return Promise.resolve(manifest); |
480 | 641 | return fetchText("manifest.json").then(function (text) { |
|
557 | 718 | titleEl.textContent = title; |
558 | 719 | } |
559 | 720 | if (mdName === "Commands.md") { |
| 721 | + initScriptExamples(innerSlot); |
560 | 722 | return injectCommandsSearch(innerSlot); |
561 | 723 | } |
| 724 | + return initScriptExamples(innerSlot); |
562 | 725 | }); |
563 | 726 | } |
564 | 727 |
|
|
0 commit comments