]> Lady’s Gitweb - GitWikiWeb/blob - build.js
List subpages in a see also section
[GitWikiWeb] / build.js
1 // 🐙🕸️ GitWikiWeb ∷ build.js
2 // ====================================================================
3 //
4 // Copyright © 2023 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9 //
10 // --------------------------------------------------------------------
11 //
12 // A script for generating static wiki pages from a git repository.
13 //
14 // First, clone this repository to your machine in an accessible
15 // location (for example, `/srv/git/GitWikiWeb`). A bare repository is
16 // fine; customize the templates and stylesheets as you like. Then, use
17 // this file as the post‐receive hook for your wiki as follows :—
18 //
19 // #!/usr/bin/env -S sh
20 // export GITWIKIWEB=/srv/git/GitWikiWeb
21 // git archive --remote=$GITWIKIWEB HEAD build.js \
22 // | tar -xO \
23 // | deno run -A - ~/public/wiki $GITWIKIWEB current
24 //
25 // The directory `~/public/wiki` (or whatever you specify as the first
26 // argument to `deno run -A -`) **will be deleted** and a new static
27 // wiki will be generated in its place. This script is not very smart
28 // (yet) and cannot selectively determine which pages will need
29 // updating. It just wipes and regenerates the whole thing.
30 //
31 // This script will make a number of requests to `$GITWIKIWEB` to
32 // download the latest templates, stylesheets, ⁊·c from this
33 // repository. Consequently, it is best that you set it to a repository
34 // you control and can ensure uptime for—ideally one local to the
35 // computer hosting the wiki.
36
37 import {
38 emptyDir,
39 ensureDir,
40 } from "https://deno.land/std@0.196.0/fs/mod.ts";
41 import {
42 JSON_SCHEMA,
43 parse as parseYaml,
44 } from "https://deno.land/std@0.196.0/yaml/mod.ts";
45 import djot from "npm:@djot/djot@0.2.3";
46 import { Parser } from "npm:htmlparser2@9.0.0";
47 import { DomHandler, Element, Text } from "npm:domhandler@5.0.3";
48 import * as domutils from "npm:domutils@3.1.0";
49 import domSerializer from "npm:dom-serializer@2.0.0";
50
51 const DESTINATION = Deno.args[0] ?? "~/public/wiki";
52 const REMOTE = Deno.args[1] ?? "/srv/git/GitWikiWeb";
53 const REV = Deno.args[2] ?? "HEAD";
54
55 const READ_ONLY = {
56 configurable: false,
57 enumerable: true,
58 writable: false,
59 };
60
61 const NIL = Object.preventExtensions(Object.create(null));
62
63 const rawBlock = (strings, ...substitutions) => ({
64 tag: "raw_block",
65 format: "html",
66 text: String.raw(strings, substitutions),
67 });
68 const rawInline = (strings, ...substitutions) => ({
69 tag: "raw_inline",
70 format: "html",
71 text: String.raw(strings, substitutions),
72 });
73 const str = (strings, ...substitutions) => ({
74 tag: "str",
75 text: String.raw(strings, substitutions),
76 });
77
78 const getDOM = (source) => {
79 let result;
80 const handler = new DomHandler((error, dom) => {
81 if (error) {
82 throw new Error("GitWikiWeb: Failed to process DOM.", {
83 cause: error,
84 });
85 } else {
86 result = dom;
87 }
88 });
89 const parser = new Parser(handler);
90 parser.write(source);
91 parser.end();
92 return result;
93 };
94
95 const getRemoteContent = async (pathName) => {
96 const getArchive = new Deno.Command("git", {
97 args: ["archive", `--remote=${REMOTE}`, REV, pathName],
98 stdout: "piped",
99 stderr: "piped",
100 }).spawn();
101 const untar = new Deno.Command("tar", {
102 args: ["-xO"],
103 stdin: "piped",
104 stdout: "piped",
105 stderr: "piped",
106 }).spawn();
107 getArchive.stdout.pipeTo(untar.stdin);
108 const [
109 err1,
110 getArchiveStatus,
111 result,
112 err2,
113 untarStatus,
114 ] = await Promise.allSettled([
115 new Response(getArchive.stderr).text(),
116 getArchive.status,
117 new Response(untar.stdout).text(),
118 new Response(untar.stderr).text(),
119 untar.status,
120 ]).then(logErrorsAndCollectResults);
121 if (err1 || err2) {
122 console.error(err1 + err2);
123 } else {
124 /* do nothing */
125 }
126 if (!getArchiveStatus.success) {
127 throw new Error(
128 `GitWikiWeb: git archive returned nonzero exit code: ${getArchiveStatus.code}.`,
129 );
130 } else if (!untarStatus.success) {
131 throw new Error(
132 `GitWikiWeb: tar returned nonzero exit code: ${untarStatus.code}.`,
133 );
134 } else {
135 return result || "";
136 }
137 };
138
139 const logErrorsAndCollectResults = (results) =>
140 results.map(({ value, reason }) => {
141 if (reason) {
142 console.error(reason);
143 return undefined;
144 } else {
145 return value;
146 }
147 });
148
149 const getReferenceFromPath = (path) =>
150 /Sources\/([A-Z][0-9A-Za-z]*(?:\/[A-Z][0-9A-Za-z]*)+)\.djot$/u
151 .exec(path)?.[1]?.replace?.("/", ":"); // only replaces first slash
152
153 const listOfInternalLinks = (references, wrapper = ($) => $) => ({
154 tag: "bullet_list",
155 tight: true,
156 style: "*",
157 children: Array.from(
158 references,
159 (reference) => {
160 const [namespace, pageName] = splitReference(reference);
161 return {
162 tag: "list_item",
163 children: [{
164 tag: "para",
165 children: [wrapper({
166 tag: "link",
167 attributes: {
168 "data-realm": "internal",
169 "data-pagename": pageName,
170 "data-namespace": namespace,
171 },
172 reference,
173 children: [],
174 })],
175 }],
176 };
177 },
178 ),
179 });
180
181 const diffReferences = async (hash, againstHead = false) => {
182 const diff = new Deno.Command("git", {
183 args: [
184 "diff-tree",
185 "-r",
186 "-z",
187 "--name-only",
188 "--no-renames",
189 "--diff-filter=AM",
190 ...(againstHead ? [hash, "HEAD"] : [hash]),
191 ],
192 stdout: "piped",
193 stderr: "piped",
194 }).spawn();
195 const [diffNames] = await Promise.allSettled([
196 new Response(diff.stdout).text(),
197 new Response(diff.stderr).text(),
198 ]).then(logErrorsAndCollectResults);
199 return references(diffNames.split("\0")); // returns an iterable
200 };
201
202 function* references(paths) {
203 for (const path of paths) {
204 const reference = getReferenceFromPath(path);
205 if (reference) {
206 yield reference;
207 } else {
208 /* do nothing */
209 }
210 }
211 }
212
213 const splitReference = (reference) => {
214 const colonIndex = reference.indexOf(":");
215 return [
216 reference.substring(0, colonIndex),
217 reference.substring(colonIndex + 1),
218 ];
219 };
220
221 class GitWikiWebPage {
222 #internalLinks = new Set();
223 #externalLinks = new Map();
224
225 constructor(namespace, name, ast, source, config) {
226 const internalLinks = this.#internalLinks;
227 const externalLinks = this.#externalLinks;
228 const sections = Object.create(null);
229 djot.applyFilter(ast, () => {
230 let titleSoFar = null; // used to collect strs from headings
231 return {
232 hard_break: {
233 enter: (_) => {
234 if (titleSoFar != null) {
235 titleSoFar += " ";
236 } else {
237 /* do nothing */
238 }
239 },
240 exit: (_) => {},
241 },
242 heading: {
243 enter: (_) => {
244 titleSoFar = "";
245 },
246 exit: (e) => {
247 e.attributes ??= {};
248 const { attributes } = e;
249 attributes.title ??= titleSoFar;
250 titleSoFar = null;
251 },
252 },
253 link: {
254 enter: (e) => {
255 e.attributes ??= {};
256 const { attributes, reference, destination } = e;
257 if (
258 /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*(?:\/[A-Z][0-9A-Za-z]*)*)?$/u
259 .test(reference ?? "")
260 ) {
261 const [namespacePrefix, pageName] = splitReference(
262 reference,
263 );
264 const expandedNamespace = {
265 "": "Page",
266 "@": "Editor",
267 "#": "Category",
268 }[namespacePrefix] ?? namespacePrefix;
269 const resolvedReference = pageName == ""
270 ? `Namespace:${expandedNamespace}`
271 : `${expandedNamespace}:${pageName}`;
272 e.reference = resolvedReference;
273 attributes["data-realm"] = "internal";
274 attributes["data-pagename"] = pageName;
275 attributes["data-namespace"] = expandedNamespace;
276 if (
277 resolvedReference.startsWith("Editor:") &&
278 (attributes.class ?? "").split(/\s/gu).includes("sig")
279 ) {
280 // This is a special internal link; do not record it.
281 /* do nothing */
282 } else {
283 // This is a non‐special internal link; record it.
284 internalLinks.add(resolvedReference);
285 }
286 } else {
287 attributes["data-realm"] = "external";
288 const remote = destination ??
289 ast.references[reference]?.destination;
290 if (remote) {
291 externalLinks.set(remote, attributes?.title);
292 } else {
293 /* do nothing */
294 }
295 }
296 },
297 },
298 non_breaking_space: {
299 enter: (_) => {
300 if (titleSoFar != null) {
301 titleSoFar += "\xA0";
302 } else {
303 /* do nothing */
304 }
305 },
306 exit: (_) => {},
307 },
308 section: {
309 enter: (_) => {},
310 exit: (e) => {
311 e.attributes ??= {};
312 const { attributes, children } = e;
313 const heading = children.find(({ tag }) =>
314 tag == "heading"
315 );
316 const title = (() => {
317 if (heading?.attributes?.title) {
318 const result = heading.attributes.title;
319 delete heading.attributes.title;
320 return result;
321 } else {
322 return heading.level == 1
323 ? `${namespace}:${name}`
324 : "untitled section";
325 }
326 })();
327 const variantTitles = Object.create(null);
328 for (const attr in attributes) {
329 if (attr.startsWith("v-")) {
330 Object.defineProperty(
331 variantTitles,
332 attr.substring(2),
333 { ...READ_ONLY, value: attributes[attr] },
334 );
335 delete attributes[attr];
336 } else {
337 continue;
338 }
339 }
340 const definition = Object.create(null, {
341 title: { ...READ_ONLY, value: title },
342 variantTitles: {
343 ...READ_ONLY,
344 value: Object.preventExtensions(variantTitles),
345 },
346 });
347 if (heading.level == 1 && !("main" in sections)) {
348 attributes.id = "main";
349 heading.attributes ??= {};
350 heading.attributes.class = "main";
351 } else {
352 /* do nothing */
353 }
354 try {
355 Object.defineProperty(
356 sections,
357 attributes.id,
358 {
359 ...READ_ONLY,
360 writable: true,
361 value: Object.preventExtensions(definition),
362 },
363 );
364 } catch (cause) {
365 throw new Error(
366 `GitWikiWeb: A section with the provided @id already exists: ${attributes.id}`,
367 { cause },
368 );
369 }
370 },
371 },
372 soft_break: {
373 enter: (_) => {
374 if (titleSoFar != null) {
375 titleSoFar += " ";
376 } else {
377 /* do nothing */
378 }
379 },
380 exit: (_) => {},
381 },
382 str: {
383 enter: ({ text }) => {
384 if (titleSoFar != null) {
385 titleSoFar += text;
386 } else {
387 /* do nothing */
388 }
389 },
390 exit: (_) => {},
391 },
392 symb: {
393 enter: (e) => {
394 const { alias } = e;
395 const codepoint = /^U\+([0-9A-Fa-f]+)$/u.exec(alias)?.[1];
396 if (codepoint) {
397 return str`${
398 String.fromCodePoint(parseInt(codepoint, 16))
399 }`;
400 } else {
401 const resolved = config.symbols?.[alias];
402 return resolved != null ? str`${resolved}` : e;
403 }
404 },
405 },
406 };
407 });
408 Object.defineProperties(this, {
409 ast: { ...READ_ONLY, value: ast },
410 namespace: { ...READ_ONLY, value: namespace },
411 name: { ...READ_ONLY, value: name },
412 sections: {
413 ...READ_ONLY,
414 value: Object.preventExtensions(sections),
415 },
416 source: { ...READ_ONLY, value: source },
417 });
418 }
419
420 *externalLinks() {
421 yield* this.#externalLinks;
422 }
423
424 *internalLinks() {
425 yield* this.#internalLinks;
426 }
427 }
428
429 {
430 // Patches for Djot HTML renderer.
431 const { HTMLRenderer: { prototype: htmlRendererPrototype } } = djot;
432 const { inTags: upstreamInTags } = htmlRendererPrototype;
433 htmlRendererPrototype.inTags = function (
434 tag,
435 node,
436 newlines,
437 extraAttrs = undefined,
438 ) {
439 const attributes = node.attributes ?? NIL;
440 if ("as" in attributes) {
441 const newTag = attributes.as;
442 delete attributes.as;
443 return upstreamInTags.call(
444 this,
445 newTag,
446 node,
447 newlines,
448 extraAttrs,
449 );
450 } else {
451 return upstreamInTags.call(
452 this,
453 tag,
454 node,
455 newlines,
456 extraAttrs,
457 );
458 }
459 };
460 }
461 {
462 const config = await getRemoteContent("config.yaml").then((yaml) =>
463 parseYaml(yaml, { schema: JSON_SCHEMA })
464 );
465 const ls = new Deno.Command("git", {
466 args: ["ls-tree", "-rz", "HEAD"],
467 stdout: "piped",
468 stderr: "piped",
469 }).spawn();
470 const [
471 objects,
472 lserr,
473 lsstatus,
474 ] = await Promise.allSettled([
475 new Response(ls.stdout).text().then((lsout) =>
476 lsout
477 .split("\0")
478 .slice(0, -1) // drop the last entry; it is empty
479 .map(($) => $.split(/\s+/g))
480 ),
481 new Response(ls.stderr).text(),
482 ls.status,
483 ]).then(logErrorsAndCollectResults);
484 if (lserr) {
485 console.error(lserr);
486 } else {
487 /* do nothing */
488 }
489 if (!lsstatus.success) {
490 throw new Error(
491 `GitWikiWeb: git ls-tree returned nonzero exit code: ${lsstatus.code}.`,
492 );
493 } else {
494 const requiredButMissingPages = new Map([
495 ["Special:FrontPage", "front page"],
496 ["Special:NotFound", "not found"],
497 ["Special:RecentlyChanged", "recently changed"],
498 ]);
499 const pages = new Map();
500 const promises = [emptyDir(DESTINATION)];
501 for (const object of objects) {
502 const hash = object[2];
503 const path = object[3];
504 const reference = getReferenceFromPath(path);
505 if (reference == null) {
506 continue;
507 } else {
508 const [namespace, pageName] = splitReference(reference);
509 const cat = new Deno.Command("git", {
510 args: ["cat-file", "blob", hash],
511 stdout: "piped",
512 stderr: "piped",
513 }).spawn();
514 const promise = Promise.allSettled([
515 new Response(cat.stdout).text(),
516 new Response(cat.stderr).text(),
517 cat.status,
518 ]).then(logErrorsAndCollectResults).then(
519 ([source, caterr, catstatus]) => {
520 if (caterr) {
521 console.error(caterr);
522 } else {
523 /* do nothing */
524 }
525 if (!catstatus.success) {
526 throw new Error(
527 `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
528 );
529 } else {
530 const reference = `${namespace}:${pageName}`;
531 const page = new GitWikiWebPage(
532 namespace,
533 pageName,
534 djot.parse(source, {
535 warn: ($) =>
536 console.warn(`Djot(${reference}): ${$.render()}`),
537 }),
538 source,
539 config,
540 );
541 pages.set(reference, page);
542 requiredButMissingPages.delete(reference);
543 }
544 },
545 );
546 promises.push(promise);
547 }
548 }
549 for (const [reference, defaultTitle] of requiredButMissingPages) {
550 const [namespace, pageName] = splitReference(reference);
551 const source = `# ${defaultTitle}\n`;
552 const page = new GitWikiWebPage(
553 namespace,
554 pageName,
555 djot.parse(source, {
556 warn: ($) =>
557 console.warn(`Djot(${reference}): ${$.render()}`),
558 }),
559 source,
560 config,
561 );
562 pages.set(reference, page);
563 }
564 await Promise.allSettled(promises).then(
565 logErrorsAndCollectResults,
566 );
567 const [template, recentlyChanged] = await Promise.allSettled([
568 getRemoteContent("template.html"),
569 (async () => {
570 const dateParse = new Deno.Command("git", {
571 args: ["rev-parse", "--after=1 week ago"],
572 stdout: "piped",
573 stderr: "piped",
574 }).spawn();
575 const [maxAge] = await Promise.allSettled([
576 new Response(dateParse.stdout).text(),
577 new Response(dateParse.stderr).text(),
578 ]).then(logErrorsAndCollectResults);
579 let commit;
580 if (!maxAge) {
581 /* do nothing */
582 } else {
583 const revList = new Deno.Command("git", {
584 args: ["rev-list", maxAge, "--reverse", "HEAD"],
585 stdout: "piped",
586 stderr: "piped",
587 }).spawn();
588 [commit] = await Promise.allSettled([
589 new Response(revList.stdout).text().then((list) =>
590 list.split("\n")[0]
591 ),
592 new Response(revList.stderr).text(),
593 ]).then(logErrorsAndCollectResults);
594 }
595 if (!commit) {
596 const revList2 = new Deno.Command("git", {
597 args: ["rev-list", "--max-count=1", "HEAD^"],
598 stdout: "piped",
599 stderr: "piped",
600 }).spawn();
601 [commit] = await Promise.allSettled([
602 new Response(revList2.stdout).text().then((list) =>
603 list.trim()
604 ),
605 new Response(revList2.stderr).text(),
606 ]).then(logErrorsAndCollectResults);
607 } else {
608 /* do nothing */
609 }
610 const results = new Array(6);
611 const seen = new Set();
612 const maxRecency = Math.max(config.max_recency | 0, 0);
613 let recency = maxRecency;
614 let current;
615 do {
616 const show = new Deno.Command("git", {
617 args: [
618 "show",
619 "-s",
620 "--format=%H%x00%cI%x00%cD",
621 recency ? `HEAD~${maxRecency - recency}` : commit,
622 ],
623 stdout: "piped",
624 stderr: "piped",
625 }).spawn();
626 const [
627 [hash, dateTime, humanReadable],
628 ] = await Promise.allSettled([
629 new Response(show.stdout).text().then((rev) =>
630 rev.trim().split("\0")
631 ),
632 new Response(show.stderr).text(),
633 ]).then(logErrorsAndCollectResults);
634 const refs = [];
635 current = hash;
636 for (
637 const ref of (await diffReferences(current, !recency))
638 ) {
639 if (seen.has(ref)) {
640 /* do nothing */
641 } else {
642 refs.push(ref);
643 seen.add(ref);
644 }
645 }
646 results[recency] = { dateTime, hash, humanReadable, refs };
647 } while (recency-- > 0 && current && current != commit);
648 return results;
649 })(),
650 ...Array.from(
651 pages.keys(),
652 (name) => ensureDir(`${DESTINATION}/${name}`),
653 ),
654 ["style.css"].map((dependency) =>
655 getRemoteContent(dependency).then((source) =>
656 Deno.writeTextFile(
657 `${DESTINATION}/${dependency}`,
658 source,
659 { createNew: true },
660 )
661 )
662 ),
663 ]).then(logErrorsAndCollectResults);
664 promises.length = 0;
665 const { redLinks, subpages } = (() => {
666 const redLinks = new Set();
667 const subpages = new Map();
668 for (const [pageRef, page] of pages) {
669 let superRef = pageRef;
670 while (
671 (superRef = superRef.substring(0, superRef.indexOf("/")))
672 ) {
673 // Iterate over potential superpages and record them if they
674 // actually exist.
675 if (pages.has(superRef)) {
676 // There is a superpage for the current page; record it.
677 if (subpages.has(superRef)) {
678 // The identified superpage already has other subpages.
679 subpages.get(superRef).add(pageRef);
680 } else {
681 // The identified superpage does not already have other
682 // subpages.
683 subpages.set(superRef, new Set([pageRef]));
684 }
685 break;
686 } else {
687 // The superpage for the current page has not been found
688 // yet.
689 /* do nothing */
690 }
691 }
692 for (const link of page.internalLinks()) {
693 // Iterate over the internal links of the current page and
694 // ensure they are all defined.
695 if (pages.has(link)) {
696 // The link was defined.
697 continue;
698 } else {
699 // The link was not defined; it is a redlink.
700 redLinks.add(link);
701 }
702 }
703 }
704 return { redLinks, subpages };
705 })();
706 for (const [pageRef, page] of pages) {
707 const { ast, sections, source } = page;
708 const title = sections.main?.title ?? pageRef;
709 const internalLinks = new Set(page.internalLinks());
710 const externalLinks = new Map(page.externalLinks());
711 const subpageRefs = subpages.get(pageRef) ?? new Set();
712 djot.applyFilter(ast, () => {
713 let isNavigationPage = true;
714 return {
715 doc: {
716 enter: (e) => {
717 const seeAlsoSection = [];
718 const linksSection = [];
719 if (subpageRefs.size) {
720 seeAlsoSection.push(
721 rawBlock`<nav id="seealso">`,
722 {
723 tag: "heading",
724 level: 2,
725 children: [
726 str`see also`,
727 ],
728 },
729 rawBlock`<section id="subpages">`,
730 {
731 tag: "heading",
732 level: 3,
733 children: [
734 str`subpages`,
735 ],
736 },
737 listOfInternalLinks(subpageRefs),
738 rawBlock`</section>`,
739 rawBlock`</nav>`,
740 );
741 } else {
742 /* do nothing */
743 }
744 if (internalLinks.size || externalLinks.size) {
745 linksSection.push(
746 rawBlock`<nav id="links">`,
747 {
748 tag: "heading",
749 level: 2,
750 children: [str`this page contains links`],
751 },
752 );
753 if (internalLinks.size) {
754 linksSection.push(
755 rawBlock`<details open="">`,
756 rawBlock`<summary>on this wiki</summary>`,
757 listOfInternalLinks(internalLinks),
758 rawBlock`</details>`,
759 );
760 } else {
761 /* do nothing */
762 }
763 if (externalLinks.size) {
764 linksSection.push(
765 rawBlock`<details open="">`,
766 rawBlock`<summary>elsewhere on the Web</summary>`,
767 {
768 tag: "bullet_list",
769 tight: true,
770 style: "*",
771 children: Array.from(
772 externalLinks,
773 ([destination, text]) => ({
774 tag: "list_item",
775 children: [{
776 tag: "para",
777 children: [{
778 tag: "link",
779 attributes: { "data-realm": "external" },
780 destination,
781 children: text
782 ? [
783 rawInline`<cite>`,
784 str`${text}`,
785 rawInline`</cite>`,
786 ]
787 : [
788 rawInline`<code>`,
789 str`${destination}`,
790 rawInline`</code>`,
791 ],
792 }],
793 }],
794 }),
795 ),
796 },
797 rawBlock`</details>`,
798 );
799 } else {
800 /* do nothing */
801 }
802 linksSection.push(
803 rawBlock`</nav>`,
804 );
805 } else {
806 /* do nothing */
807 }
808 const childrenAndLinks = [
809 ...e.children,
810 ...seeAlsoSection,
811 rawBlock`<footer>`,
812 rawBlock`${"\uFFFF"}`, // footnote placeholder
813 ...linksSection,
814 rawBlock`</footer>`,
815 ];
816 const { content, navigation } = (() => {
817 const navigation = [];
818 if (pageRef == "Special:RecentlyChanged") {
819 navigation.push({
820 tag: "bullet_list",
821 attributes: { class: "recent-changes" },
822 tight: true,
823 style: "*",
824 children: Array.from(function* () {
825 for (
826 const [index, result] of recentlyChanged
827 .entries()
828 ) {
829 if (result != null) {
830 const {
831 dateTime,
832 humanReadable,
833 refs,
834 } = result;
835 yield* listOfInternalLinks(refs, (link) => ({
836 tag: index == 0 ? "span" : "strong",
837 attributes: { "data-recency": `${index}` },
838 children: [
839 link,
840 ...(index == 0 ? [] : [
841 str` `,
842 rawInline`<small>(<time dateTime="${dateTime}">`,
843 str`${humanReadable}`,
844 rawInline`</time>)</small>`,
845 ]),
846 ],
847 })).children;
848 } else {
849 /* do nothing */
850 }
851 }
852 }()).reverse(),
853 });
854 } else {
855 isNavigationPage = false;
856 return { content: childrenAndLinks, navigation };
857 }
858 return {
859 content: [
860 {
861 tag: "heading",
862 attributes: {
863 class: "main",
864 generated: "", // will be removed later
865 },
866 level: 1,
867 children: [str`${title}`],
868 },
869 rawBlock`<details id="navigation-about" open="">`,
870 rawBlock`<summary>about this listing</summary>`,
871 rawBlock`<article>`,
872 ...childrenAndLinks,
873 rawBlock`</article>`,
874 rawBlock`</details>`,
875 ],
876 navigation: [
877 rawBlock`<nav id="navigation">`,
878 ...navigation,
879 rawBlock`</nav>`,
880 ],
881 };
882 })();
883 e.children = [
884 rawBlock`<article>`,
885 ...content,
886 ...navigation,
887 rawBlock`</article>`,
888 ];
889 },
890 exit: (_) => {},
891 },
892 heading: {
893 enter: (e) => {
894 const attributes = e.attributes ?? NIL;
895 if (
896 isNavigationPage && e.level == 1 &&
897 attributes?.class == "main"
898 ) {
899 if ("generated" in attributes) {
900 delete attributes.generated;
901 } else {
902 return { stop: [] };
903 }
904 } else {
905 /* do nothing */
906 }
907 },
908 exit: (e) => {
909 if (e.level == 1 && e.attributes?.class == "main") {
910 return [
911 rawBlock`<header class="main">`,
912 e,
913 { tag: "verbatim", text: pageRef },
914 rawBlock`</header>`,
915 ];
916 } else {
917 /* do nothing */
918 }
919 },
920 },
921 link: {
922 enter: (_) => {},
923 exit: (e) => {
924 e.attributes ??= {};
925 const { attributes, children, reference } = e;
926 if (attributes["data-realm"] == "internal") {
927 delete e.reference;
928 if (redLinks.has(reference)) {
929 e.destination =
930 `/Special:NotFound?path=/${reference}`;
931 attributes["data-notfound"] = "";
932 } else {
933 e.destination = `/${reference}`;
934 }
935 if (children.length == 0) {
936 const section =
937 pages.get(reference)?.sections?.main ?? NIL;
938 const { v } = attributes;
939 if (v == null) {
940 children.push(
941 str`${section.title ?? reference}`,
942 );
943 } else {
944 delete attributes.v;
945 children.push(
946 str`${
947 section.variantTitles?.[v] ?? section.title ??
948 reference
949 }`,
950 );
951 }
952 }
953 } else {
954 if (children.length == 0 && "title" in attributes) {
955 children.push(
956 rawInline`<cite>`,
957 str`${attributes.title}`,
958 rawInline`</cite>`,
959 );
960 }
961 }
962 if (
963 (attributes.class ?? "").split(/\s/gu).includes("sig")
964 ) {
965 return {
966 tag: "span",
967 attributes: { class: "sig" },
968 children: [str`—${"\xA0"}`, e],
969 };
970 } else {
971 /* do nothing */
972 }
973 },
974 },
975 section: {
976 enter: (_) => {},
977 exit: (e) => {
978 if (e.children.length < 1) {
979 // The heading for this section was removed and it had
980 // no other children.
981 return [];
982 } else {
983 /* do nothing */
984 }
985 },
986 },
987 };
988 });
989 const renderedAST = djot.renderAST(ast);
990 const doc = getDOM(template);
991 const result = getDOM(djot.renderHTML(ast, {
992 overrides: {
993 raw_block: (node, context) => {
994 if (node.format == "html" && node.text == "\uFFFF") {
995 if (context.nextFootnoteIndex > 1) {
996 const result = context.renderNotes(ast.footnotes);
997 context.nextFootnoteIndex = 1;
998 return result;
999 } else {
1000 return "";
1001 }
1002 } else {
1003 return context.renderAstNodeDefault(node);
1004 }
1005 },
1006 },
1007 }));
1008 const headElement = domutils.findOne(
1009 (node) => node.name == "head",
1010 doc,
1011 );
1012 const titleElement = domutils.findOne(
1013 (node) => node.name == "title",
1014 headElement,
1015 );
1016 const contentElement = domutils.findOne(
1017 (node) => node.name == "gitwikiweb-content",
1018 doc,
1019 );
1020 if (headElement == null) {
1021 throw new Error(
1022 "GitWikiWeb: Template must explicitly include a <head> element.",
1023 );
1024 } else {
1025 domutils.appendChild(
1026 headElement,
1027 new Element("link", {
1028 rel: "source",
1029 type: "text/x.djot",
1030 href: `/${pageRef}/source.djot`,
1031 }),
1032 );
1033 if (titleElement == null) {
1034 domutils.prependChild(
1035 headElement,
1036 new Element("title", {}, [new Text(title)]),
1037 );
1038 } else {
1039 domutils.prependChild(titleElement, new Text(`${title} | `));
1040 }
1041 }
1042 if (contentElement == null) {
1043 throw new Error(
1044 "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
1045 );
1046 } else {
1047 for (const node of [...result]) {
1048 domutils.prepend(contentElement, node);
1049 }
1050 domutils.removeElement(contentElement);
1051 }
1052 promises.push(
1053 Deno.writeTextFile(
1054 `${DESTINATION}/${pageRef}/index.html`,
1055 domSerializer(doc, {
1056 emptyAttrs: true,
1057 encodeEntities: "utf8",
1058 selfClosingTags: true,
1059 xmlMode: false,
1060 }),
1061 { createNew: true },
1062 ),
1063 );
1064 promises.push(
1065 Deno.writeTextFile(
1066 `${DESTINATION}/${pageRef}/index.ast`,
1067 renderedAST,
1068 { createNew: true },
1069 ),
1070 );
1071 promises.push(
1072 Deno.writeTextFile(
1073 `${DESTINATION}/${pageRef}/source.djot`,
1074 source,
1075 { createNew: true },
1076 ),
1077 );
1078 }
1079 await Promise.allSettled(promises).then(
1080 logErrorsAndCollectResults,
1081 );
1082 console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
1083 }
1084 }
This page took 0.174212 seconds and 5 git commands to generate.