]> Lady’s Gitweb - GitWikiWeb/blob - build.js
df9c016600b3fdf9539de47d162d7ff944fc9dc9
[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
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.195.0/fs/mod.ts";
41 import djot from "npm:@djot/djot@0.2.3";
42 import { Parser } from "npm:htmlparser2@9.0.0";
43 import { DomHandler, Element, Text } from "npm:domhandler@5.0.3";
44 import * as domutils from "npm:domutils@3.1.0";
45 import domSerializer from "npm:dom-serializer@2.0.0";
46
47 const DESTINATION = Deno.args[0] ?? "~/public/wiki";
48 const REMOTE = Deno.args[1] ?? "/srv/git/GitWikiWeb";
49
50 const READ_ONLY = {
51 configurable: false,
52 enumerable: true,
53 writable: false,
54 };
55
56 const NIL = Object.preventExtensions(Object.create(null));
57
58 const rawBlock = (strings, ...substitutions) => ({
59 tag: "raw_block",
60 format: "html",
61 text: String.raw(strings, substitutions),
62 });
63 const rawInline = (strings, ...substitutions) => ({
64 tag: "raw_inline",
65 format: "html",
66 text: String.raw(strings, substitutions),
67 });
68 const str = (strings, ...substitutions) => ({
69 tag: "str",
70 text: String.raw(strings, substitutions),
71 });
72
73 const getDOM = (source) => {
74 let result;
75 const handler = new DomHandler((error, dom) => {
76 if (error) {
77 throw new Error("GitWikiWeb: Failed to process DOM.", {
78 cause: error,
79 });
80 } else {
81 result = dom;
82 }
83 });
84 const parser = new Parser(handler);
85 parser.write(source);
86 parser.end();
87 return result;
88 };
89
90 const getRemoteContent = async (pathName) => {
91 const getArchive = new Deno.Command("git", {
92 args: ["archive", `--remote=${REMOTE}`, "HEAD", pathName],
93 stdout: "piped",
94 stderr: "piped",
95 }).spawn();
96 const untar = new Deno.Command("tar", {
97 args: ["-xO"],
98 stdin: "piped",
99 stdout: "piped",
100 stderr: "piped",
101 }).spawn();
102 getArchive.stdout.pipeTo(untar.stdin);
103 const [
104 err1,
105 getArchiveStatus,
106 result,
107 err2,
108 untarStatus,
109 ] = await Promise.allSettled([
110 new Response(getArchive.stderr).text(),
111 getArchive.status,
112 new Response(untar.stdout).text(),
113 new Response(untar.stderr).text(),
114 untar.status,
115 ]).then(logErrorsAndCollectResults);
116 if (err1 || err2) {
117 console.error(err1 + err2);
118 } else {
119 /* do nothing */
120 }
121 if (!getArchiveStatus.success) {
122 throw new Error(
123 `GitWikiWeb: git archive returned nonzero exit code: ${getArchiveStatus.code}.`,
124 );
125 } else if (!untarStatus.success) {
126 throw new Error(
127 `GitWikiWeb: tar returned nonzero exit code: ${untarStatus.code}.`,
128 );
129 } else {
130 return result || "";
131 }
132 };
133
134 const logErrorsAndCollectResults = (results) =>
135 results.map(({ value, reason }) => {
136 if (reason) {
137 console.error(reason);
138 return undefined;
139 } else {
140 return value;
141 }
142 });
143
144 const getReferenceFromPath = (path) =>
145 /Sources\/([A-Z][0-9A-Za-z]*\/[A-Z][0-9A-Za-z]*)\.djot$/u.exec(path)
146 ?.[1]?.replace?.("/", ":");
147
148 const listOfInternalLinks = (references, wrapper = ($) => $) => ({
149 tag: "bullet_list",
150 tight: true,
151 style: "*",
152 children: Array.from(
153 references,
154 (reference) => {
155 const [namespace, pageName] = splitReference(reference);
156 return {
157 tag: "list_item",
158 children: [{
159 tag: "para",
160 children: [wrapper({
161 tag: "link",
162 attributes: {
163 "data-realm": "internal",
164 "data-pagename": pageName,
165 "data-namespace": namespace,
166 },
167 reference,
168 children: [],
169 })],
170 }],
171 };
172 },
173 ),
174 });
175
176 const diffReferences = async (hash, againstHead = false) => {
177 const diff = new Deno.Command("git", {
178 args: [
179 "diff-tree",
180 "-r",
181 "-z",
182 "--name-only",
183 "--no-renames",
184 "--diff-filter=AM",
185 ...(againstHead ? [hash, "HEAD"] : [hash]),
186 ],
187 stdout: "piped",
188 stderr: "piped",
189 }).spawn();
190 const [diffNames] = await Promise.allSettled([
191 new Response(diff.stdout).text(),
192 new Response(diff.stderr).text(),
193 ]).then(logErrorsAndCollectResults);
194 return references(diffNames.split("\0")); // returns an iterable
195 };
196
197 function* references(paths) {
198 for (const path of paths) {
199 const reference = getReferenceFromPath(path);
200 if (reference) {
201 yield reference;
202 } else {
203 /* do nothing */
204 }
205 }
206 }
207
208 const splitReference = (reference) => {
209 const colonIndex = reference.indexOf(":");
210 return [
211 reference.substring(0, colonIndex),
212 reference.substring(colonIndex + 1),
213 ];
214 };
215
216 class GitWikiWebPage {
217 #internalLinks = new Set();
218 #externalLinks = new Map();
219
220 constructor(namespace, name, ast, source) {
221 const internalLinks = this.#internalLinks;
222 const externalLinks = this.#externalLinks;
223 const sections = Object.create(null);
224 djot.applyFilter(ast, () => {
225 let titleSoFar = null; // used to collect strs from headings
226 return {
227 doc: {
228 enter: (_) => {},
229 exit: (e) => {
230 const links_section = [];
231 if (internalLinks.size || externalLinks.size) {
232 links_section.push(
233 rawBlock`<footer>`,
234 rawBlock`<nav id="links">`,
235 {
236 tag: "heading",
237 level: 2,
238 children: [str`this page contains links`],
239 },
240 );
241 if (internalLinks.size) {
242 links_section.push(
243 rawBlock`<details open="">`,
244 rawBlock`<summary>on this wiki</summary>`,
245 listOfInternalLinks(internalLinks),
246 rawBlock`</details>`,
247 );
248 } else {
249 /* do nothing */
250 }
251 if (externalLinks.size) {
252 links_section.push(
253 rawBlock`<details open="">`,
254 rawBlock`<summary>elsewhere on the Web</summary>`,
255 {
256 tag: "bullet_list",
257 tight: true,
258 style: "*",
259 children: Array.from(
260 externalLinks,
261 ([destination, text]) => ({
262 tag: "list_item",
263 children: [{
264 tag: "para",
265 children: [{
266 tag: "link",
267 attributes: { "data-realm": "external" },
268 destination,
269 children: text
270 ? [
271 rawInline`<cite>`,
272 str`${text}`,
273 rawInline`</cite>`,
274 ]
275 : [
276 rawInline`<code>`,
277 str`${destination}`,
278 rawInline`</code>`,
279 ],
280 }],
281 }],
282 }),
283 ),
284 },
285 rawBlock`</details>`,
286 );
287 } else {
288 /* do nothing */
289 }
290 links_section.push(
291 rawBlock`</nav>`,
292 rawBlock`</footer>`,
293 );
294 } else {
295 /* do nothing */
296 }
297 e.children.push(...links_section);
298 },
299 },
300 emph: {
301 enter: (_) => {},
302 exit: (e) => {
303 const attributes = e.attributes ?? NIL;
304 const { as } = attributes;
305 if (as) {
306 delete attributes.as;
307 if (
308 as == "b" || as == "cite" || as == "i" || as == "u"
309 ) {
310 return [
311 rawInline`<${as}>`,
312 ...e.children,
313 rawInline`</${as}>`,
314 ];
315 } else {
316 /* do nothing */
317 }
318 } else {
319 /* do nothing */
320 }
321 },
322 },
323 hard_break: {
324 enter: (_) => {
325 if (titleSoFar != null) {
326 titleSoFar += " ";
327 } else {
328 /* do nothing */
329 }
330 },
331 exit: (_) => {},
332 },
333 heading: {
334 enter: (_) => {
335 titleSoFar = "";
336 },
337 exit: (e) => {
338 e.attributes ??= {};
339 const { attributes } = e;
340 attributes.title ??= titleSoFar;
341 titleSoFar = null;
342 },
343 },
344 link: {
345 enter: (e) => {
346 e.attributes ??= {};
347 const { attributes, reference, destination } = e;
348 if (
349 /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*)?$/u
350 .test(reference ?? "")
351 ) {
352 const [namespacePrefix, pageName] = splitReference(
353 reference,
354 );
355 const expandedNamespace = {
356 "": "Page",
357 "@": "Editor",
358 "#": "Category",
359 }[namespacePrefix] ?? namespacePrefix;
360 const resolvedReference = pageName == ""
361 ? `Namespace:${expandedNamespace}`
362 : `${expandedNamespace}:${pageName}`;
363 this.#internalLinks.add(resolvedReference);
364 e.reference = resolvedReference;
365 attributes["data-realm"] = "internal";
366 attributes["data-pagename"] = pageName;
367 attributes["data-namespace"] = expandedNamespace;
368 } else {
369 attributes["data-realm"] = "external";
370 const remote = destination ??
371 ast.references[reference]?.destination;
372 if (remote) {
373 externalLinks.set(remote, attributes?.title);
374 } else {
375 /* do nothing */
376 }
377 }
378 },
379 },
380 non_breaking_space: {
381 enter: (_) => {
382 if (titleSoFar != null) {
383 titleSoFar += "\xA0";
384 } else {
385 /* do nothing */
386 }
387 },
388 exit: (_) => {},
389 },
390 section: {
391 enter: (_) => {},
392 exit: (e) => {
393 e.attributes ??= {};
394 const { attributes, children } = e;
395 const heading = children.find(({ tag }) =>
396 tag == "heading"
397 );
398 const title = (() => {
399 if (heading?.attributes?.title) {
400 const result = heading.attributes.title;
401 delete heading.attributes.title;
402 return result;
403 } else {
404 return heading.level == 1
405 ? `${namespace}:${name}`
406 : "untitled section";
407 }
408 })();
409 const variantTitles = Object.create(null);
410 for (const attr in attributes) {
411 if (attr.startsWith("v-")) {
412 Object.defineProperty(
413 variantTitles,
414 attr.substring(2),
415 { ...READ_ONLY, value: attributes[attr] },
416 );
417 delete attributes[attr];
418 } else {
419 continue;
420 }
421 }
422 const definition = Object.create(null, {
423 title: { ...READ_ONLY, value: title },
424 variantTitles: {
425 ...READ_ONLY,
426 value: Object.preventExtensions(variantTitles),
427 },
428 });
429 if (heading.level == 1 && !("main" in sections)) {
430 attributes.id = "main";
431 heading.attributes ??= {};
432 heading.attributes.class = "main";
433 } else {
434 /* do nothing */
435 }
436 try {
437 Object.defineProperty(
438 sections,
439 attributes.id,
440 {
441 ...READ_ONLY,
442 writable: true,
443 value: Object.preventExtensions(definition),
444 },
445 );
446 } catch (cause) {
447 throw new Error(
448 `GitWikiWeb: A section with the provided @id already exists: ${attributes.id}`,
449 { cause },
450 );
451 }
452 },
453 },
454 soft_break: {
455 enter: (_) => {
456 if (titleSoFar != null) {
457 titleSoFar += " ";
458 } else {
459 /* do nothing */
460 }
461 },
462 exit: (_) => {},
463 },
464 str: {
465 enter: ({ text }) => {
466 if (titleSoFar != null) {
467 titleSoFar += text;
468 } else {
469 /* do nothing */
470 }
471 },
472 exit: (_) => {},
473 },
474 symb: {
475 enter: (e) => {
476 const { alias } = e;
477 const codepoint = /^U\+([0-9A-Fa-f]+)$/u.exec(alias)?.[1];
478 if (codepoint) {
479 return str`${
480 String.fromCodePoint(parseInt(codepoint, 16))
481 }`;
482 } else {
483 return {
484 "--8": str`${"—\u2060:\u202F"}`, // reverse puppyprick
485 "8--": str`${"\u202F:\u2060—"}`, // forward puppyprick
486 sp: rawInline` `, // space
487 nbsp: str`${"\xA0"}`, // no‐break space
488 cgj: str`${"\u034F"}`, // combining grapheme joiner
489 ensp: str`${"\u2002"}`, // enspace
490 emsp: str`${"\u2003"}`, // emspace
491 figsp: str`${"\u2007"}`, // figure space
492 zwsp: str`${"\u200B"}`, // zero‐width space
493 zwnj: str`${"\u200C"}`, // zero‐width nonjoiner
494 zwj: str`${"\u200D"}`, // zero‐width joiner
495 nnbsp: str`${"\u202F"}`, // narrow no‐break space
496 mathsp: str`${"\u205F"}`, // math space
497 wj: str`${"\u2060"}`, // word joiner
498 fwsp: str`${"\u3000"}`, // fullwidth space
499 }[alias] ?? e;
500 }
501 },
502 },
503 };
504 });
505 Object.defineProperties(this, {
506 ast: { ...READ_ONLY, value: ast },
507 namespace: { ...READ_ONLY, value: namespace },
508 name: { ...READ_ONLY, value: name },
509 sections: {
510 ...READ_ONLY,
511 value: Object.preventExtensions(sections),
512 },
513 source: { ...READ_ONLY, value: source },
514 });
515 }
516
517 *externalLinks() {
518 yield* this.#externalLinks;
519 }
520
521 *internalLinks() {
522 yield* this.#internalLinks;
523 }
524 }
525
526 {
527 const ls = new Deno.Command("git", {
528 args: ["ls-tree", "-rz", "live"],
529 stdout: "piped",
530 stderr: "piped",
531 }).spawn();
532 const [
533 objects,
534 lserr,
535 lsstatus,
536 ] = await Promise.allSettled([
537 new Response(ls.stdout).text().then((lsout) =>
538 lsout
539 .split("\0")
540 .slice(0, -1) // drop the last entry; it is empty
541 .map(($) => $.split(/\s+/g))
542 ),
543 new Response(ls.stderr).text(),
544 ls.status,
545 ]).then(logErrorsAndCollectResults);
546 if (lserr) {
547 console.error(lserr);
548 } else {
549 /* do nothing */
550 }
551 if (!lsstatus.success) {
552 throw new Error(
553 `GitWikiWeb: git ls-tree returned nonzero exit code: ${lsstatus.code}.`,
554 );
555 } else {
556 const requiredButMissingPages = new Map([
557 ["Special:FrontPage", "front page"],
558 ["Special:NotFound", "not found"],
559 ["Special:RecentlyChanged", "recently changed"],
560 ]);
561 const pages = new Map();
562 const promises = [emptyDir(DESTINATION)];
563 for (const object of objects) {
564 const hash = object[2];
565 const path = object[3];
566 const reference = getReferenceFromPath(path);
567 if (reference == null) {
568 continue;
569 } else {
570 const [namespace, pageName] = splitReference(reference);
571 const cat = new Deno.Command("git", {
572 args: ["cat-file", "blob", hash],
573 stdout: "piped",
574 stderr: "piped",
575 }).spawn();
576 const promise = Promise.allSettled([
577 new Response(cat.stdout).text(),
578 new Response(cat.stderr).text(),
579 cat.status,
580 ]).then(logErrorsAndCollectResults).then(
581 ([source, caterr, catstatus]) => {
582 if (caterr) {
583 console.error(caterr);
584 } else {
585 /* do nothing */
586 }
587 if (!catstatus.success) {
588 throw new Error(
589 `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
590 );
591 } else {
592 const page = new GitWikiWebPage(
593 namespace,
594 pageName,
595 djot.parse(source, {
596 warn: ($) =>
597 console.warn(`Djot(${reference}): ${$.render()}`),
598 }),
599 source,
600 );
601 const reference = `${namespace}:${pageName}`;
602 pages.set(reference, page);
603 requiredButMissingPages.delete(reference);
604 }
605 },
606 );
607 promises.push(promise);
608 }
609 }
610 for (const [reference, defaultTitle] of requiredButMissingPages) {
611 const [namespace, pageName] = splitReference(reference);
612 const source = `# ${defaultTitle}\n`;
613 const page = new GitWikiWebPage(
614 namespace,
615 pageName,
616 djot.parse(source, {
617 warn: ($) =>
618 console.warn(`Djot(${reference}): ${$.render()}`),
619 }),
620 source,
621 );
622 pages.set(reference, page);
623 }
624 await Promise.allSettled(promises).then(
625 logErrorsAndCollectResults,
626 );
627 const [template, recentlyChanged] = await Promise.allSettled([
628 getRemoteContent("template.html"),
629 (async () => {
630 const dateParse = new Deno.Command("git", {
631 args: ["rev-parse", "--after=1 week ago"],
632 stdout: "piped",
633 stderr: "piped",
634 }).spawn();
635 const [maxAge] = await Promise.allSettled([
636 new Response(dateParse.stdout).text(),
637 new Response(dateParse.stderr).text(),
638 ]).then(logErrorsAndCollectResults);
639 let commit;
640 if (!maxAge) {
641 /* do nothing */
642 } else {
643 const revList = new Deno.Command("git", {
644 args: ["rev-list", maxAge, "--reverse", "HEAD"],
645 stdout: "piped",
646 stderr: "piped",
647 }).spawn();
648 [commit] = await Promise.allSettled([
649 new Response(revList.stdout).text().then((list) =>
650 list.split("\n")[0]
651 ),
652 new Response(revList.stderr).text(),
653 ]).then(logErrorsAndCollectResults);
654 }
655 if (!commit) {
656 const revList2 = new Deno.Command("git", {
657 args: ["rev-list", "--max-count=1", "HEAD^"],
658 stdout: "piped",
659 stderr: "piped",
660 }).spawn();
661 [commit] = await Promise.allSettled([
662 new Response(revList2.stdout).text().then((list) =>
663 list.trim()
664 ),
665 new Response(revList2.stderr).text(),
666 ]).then(logErrorsAndCollectResults);
667 } else {
668 /* do nothing */
669 }
670 const results = new Array(6);
671 const seen = new Set();
672 let recency = 5;
673 let current;
674 do {
675 const show = new Deno.Command("git", {
676 args: [
677 "show",
678 "-s",
679 "--format=%H%x00%cI%x00%cD",
680 recency ? `HEAD~${5 - recency}` : commit,
681 ],
682 stdout: "piped",
683 stderr: "piped",
684 }).spawn();
685 const [
686 [hash, dateTime, humanReadable],
687 ] = await Promise.allSettled([
688 new Response(show.stdout).text().then((rev) =>
689 rev.trim().split("\0")
690 ),
691 new Response(show.stderr).text(),
692 ]).then(logErrorsAndCollectResults);
693 const refs = [];
694 current = hash;
695 for (
696 const ref of (await diffReferences(current, !recency))
697 ) {
698 if (seen.has(ref)) {
699 /* do nothing */
700 } else {
701 refs.push(ref);
702 seen.add(ref);
703 }
704 }
705 results[recency] = { dateTime, hash, humanReadable, refs };
706 } while (recency-- > 0 && current && current != commit);
707 return results;
708 })(),
709 ...Array.from(
710 pages.keys(),
711 (name) => ensureDir(`${DESTINATION}/${name}`),
712 ),
713 ["style.css"].map((dependency) =>
714 getRemoteContent(dependency).then((source) =>
715 Deno.writeTextFile(
716 `${DESTINATION}/${dependency}`,
717 source,
718 { createNew: true },
719 )
720 )
721 ),
722 ]).then(logErrorsAndCollectResults);
723 promises.length = 0;
724 const redLinks = (() => {
725 const result = new Set();
726 for (const page of pages.values()) {
727 for (const link of page.internalLinks()) {
728 if (pages.has(link)) {
729 continue;
730 } else {
731 result.add(link);
732 }
733 }
734 }
735 return result;
736 })();
737 for (
738 const [pageRef, { ast, namespace, sections, source }] of pages
739 ) {
740 const title = sections.main?.title ?? pageRef;
741 djot.applyFilter(ast, () => {
742 let isNavigationPage = true;
743 return {
744 doc: {
745 enter: (e) => {
746 const { content, navigation } = (() => {
747 const navigation = [];
748 if (pageRef == "Special:RecentlyChanged") {
749 navigation.push({
750 tag: "bullet_list",
751 attributes: { class: "recent-changes" },
752 tight: true,
753 style: "*",
754 children: Array.from(function* () {
755 for (
756 const [index, result] of recentlyChanged
757 .entries()
758 ) {
759 if (result != null) {
760 const {
761 dateTime,
762 humanReadable,
763 refs,
764 } = result;
765 yield* listOfInternalLinks(refs, (link) => ({
766 tag: index == 0 ? "span" : "strong",
767 attributes: { "data-recency": `${index}` },
768 children: [
769 link,
770 ...(index == 0 ? [] : [
771 str` `,
772 rawInline`<small>(<time dateTime="${dateTime}">`,
773 str`${humanReadable}`,
774 rawInline`</time>)</small>`,
775 ]),
776 ],
777 })).children;
778 } else {
779 /* do nothing */
780 }
781 }
782 }()).reverse(),
783 });
784 } else {
785 isNavigationPage = false;
786 return { content: e.children, navigation };
787 }
788 return {
789 content: [
790 {
791 tag: "heading",
792 attributes: {
793 class: "main",
794 generated: "", // will be removed later
795 },
796 level: 1,
797 children: [str`${title}`],
798 },
799 rawBlock`<details id="navigation-about" open="">`,
800 rawBlock`<summary>about this listing</summary>`,
801 rawBlock`<article>`,
802 ...e.children,
803 rawBlock`</article>`,
804 rawBlock`</details>`,
805 ],
806 navigation: [
807 rawBlock`<nav id="navigation">`,
808 ...navigation,
809 rawBlock`</nav>`,
810 ],
811 };
812 })();
813 e.children = [
814 rawBlock`<article>`,
815 ...content,
816 ...navigation,
817 rawBlock`</article>`,
818 ];
819 },
820 exit: (_) => {},
821 },
822 heading: {
823 enter: (e) => {
824 const attributes = e.attributes ?? NIL;
825 if (
826 isNavigationPage && e.level == 1 &&
827 attributes?.class == "main"
828 ) {
829 if ("generated" in attributes) {
830 delete attributes.generated;
831 } else {
832 return { stop: [] };
833 }
834 } else {
835 /* do nothing */
836 }
837 },
838 exit: (e) => {
839 if (e.level == 1 && e.attributes?.class == "main") {
840 return [
841 rawBlock`<header class="main">`,
842 e,
843 { tag: "verbatim", text: pageRef },
844 rawBlock`</header>`,
845 ];
846 } else {
847 /* do nothing */
848 }
849 },
850 },
851 link: {
852 enter: (_) => {},
853 exit: (e) => {
854 e.attributes ??= {};
855 const { attributes, children, reference } = e;
856 if (attributes["data-realm"] == "internal") {
857 delete e.reference;
858 if (redLinks.has(reference)) {
859 e.destination =
860 `/Special:NotFound?path=/${reference}`;
861 attributes["data-notfound"] = "";
862 } else {
863 e.destination = `/${reference}`;
864 }
865 if (children.length == 0) {
866 const section =
867 pages.get(reference)?.sections?.main ?? NIL;
868 const { v } = attributes;
869 if (v == null) {
870 children.push(
871 str`${section.title ?? reference}`,
872 );
873 } else {
874 delete attributes.v;
875 children.push(
876 str`${
877 section.variantTitles?.[v] ?? section.title ??
878 reference
879 }`,
880 );
881 }
882 }
883 } else {
884 if (children.length == 0 && "title" in attributes) {
885 children.push(
886 rawInline`<cite>`,
887 str`${attributes.title}`,
888 rawInline`</cite>`,
889 );
890 }
891 }
892 if (
893 (attributes.class ?? "").split(/\s/gu).includes("sig")
894 ) {
895 return {
896 tag: "span",
897 attributes: { class: "sig" },
898 children: [str`—${"\xA0"}`, e],
899 };
900 } else {
901 /* do nothing */
902 }
903 },
904 },
905 section: {
906 enter: (_) => {},
907 exit: (e) => {
908 if (e.children.length < 1) {
909 // The heading for this section was removed and it had
910 // no other children.
911 return [];
912 } else {
913 /* do nothing */
914 }
915 },
916 },
917 };
918 });
919 const doc = getDOM(template);
920 const result = getDOM(`${djot.renderHTML(ast)}`);
921 const headElement = domutils.findOne(
922 (node) => node.name == "head",
923 doc,
924 );
925 const titleElement = domutils.findOne(
926 (node) => node.name == "title",
927 headElement,
928 );
929 const contentElement = domutils.findOne(
930 (node) => node.name == "gitwikiweb-content",
931 doc,
932 );
933 if (headElement == null) {
934 throw new Error(
935 "GitWikiWeb: Template must explicitly include a <head> element.",
936 );
937 } else {
938 domutils.appendChild(
939 headElement,
940 new Element("link", {
941 rel: "source",
942 type: "text/x.djot",
943 href: `/${pageRef}/source.djot`,
944 }),
945 );
946 if (titleElement == null) {
947 domutils.prependChild(
948 headElement,
949 new Element("title", {}, [new Text(title)]),
950 );
951 } else {
952 domutils.prependChild(titleElement, new Text(`${title} | `));
953 }
954 }
955 if (contentElement == null) {
956 throw new Error(
957 "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
958 );
959 } else {
960 for (const node of result) {
961 domutils.prepend(contentElement, node);
962 }
963 domutils.removeElement(contentElement);
964 }
965 promises.push(
966 Deno.writeTextFile(
967 `${DESTINATION}/${pageRef}/index.html`,
968 domSerializer(doc, {
969 emptyAttrs: true,
970 encodeEntities: "utf8",
971 selfClosingTags: true,
972 xmlMode: false,
973 }),
974 { createNew: true },
975 ),
976 );
977 promises.push(
978 Deno.writeTextFile(
979 `${DESTINATION}/${pageRef}/source.djot`,
980 source,
981 { createNew: true },
982 ),
983 );
984 }
985 await Promise.allSettled(promises).then(
986 logErrorsAndCollectResults,
987 );
988 console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
989 }
990 }
This page took 0.139169 seconds and 3 git commands to generate.