]> Lady’s Gitweb - GitWikiWeb/blob - build.js
0eb61fd8de30773f9abed8bea7f99eb6cd9d674e
[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 doc: {
233 enter: (_) => {},
234 exit: (e) => {
235 const links_section = [];
236 if (internalLinks.size || externalLinks.size) {
237 links_section.push(
238 rawBlock`<nav id="links">`,
239 {
240 tag: "heading",
241 level: 2,
242 children: [str`this page contains links`],
243 },
244 );
245 if (internalLinks.size) {
246 links_section.push(
247 rawBlock`<details open="">`,
248 rawBlock`<summary>on this wiki</summary>`,
249 listOfInternalLinks(internalLinks),
250 rawBlock`</details>`,
251 );
252 } else {
253 /* do nothing */
254 }
255 if (externalLinks.size) {
256 links_section.push(
257 rawBlock`<details open="">`,
258 rawBlock`<summary>elsewhere on the Web</summary>`,
259 {
260 tag: "bullet_list",
261 tight: true,
262 style: "*",
263 children: Array.from(
264 externalLinks,
265 ([destination, text]) => ({
266 tag: "list_item",
267 children: [{
268 tag: "para",
269 children: [{
270 tag: "link",
271 attributes: { "data-realm": "external" },
272 destination,
273 children: text
274 ? [
275 rawInline`<cite>`,
276 str`${text}`,
277 rawInline`</cite>`,
278 ]
279 : [
280 rawInline`<code>`,
281 str`${destination}`,
282 rawInline`</code>`,
283 ],
284 }],
285 }],
286 }),
287 ),
288 },
289 rawBlock`</details>`,
290 );
291 } else {
292 /* do nothing */
293 }
294 links_section.push(
295 rawBlock`</nav>`,
296 );
297 } else {
298 /* do nothing */
299 }
300 e.children.push(
301 rawBlock`<footer>`,
302 rawBlock`${"\uFFFF"}`, // footnote placeholder
303 ...links_section,
304 rawBlock`</footer>`,
305 );
306 },
307 },
308 hard_break: {
309 enter: (_) => {
310 if (titleSoFar != null) {
311 titleSoFar += " ";
312 } else {
313 /* do nothing */
314 }
315 },
316 exit: (_) => {},
317 },
318 heading: {
319 enter: (_) => {
320 titleSoFar = "";
321 },
322 exit: (e) => {
323 e.attributes ??= {};
324 const { attributes } = e;
325 attributes.title ??= titleSoFar;
326 titleSoFar = null;
327 },
328 },
329 link: {
330 enter: (e) => {
331 e.attributes ??= {};
332 const { attributes, reference, destination } = e;
333 if (
334 /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*(?:\/[A-Z][0-9A-Za-z]*)*)?$/u
335 .test(reference ?? "")
336 ) {
337 const [namespacePrefix, pageName] = splitReference(
338 reference,
339 );
340 const expandedNamespace = {
341 "": "Page",
342 "@": "Editor",
343 "#": "Category",
344 }[namespacePrefix] ?? namespacePrefix;
345 const resolvedReference = pageName == ""
346 ? `Namespace:${expandedNamespace}`
347 : `${expandedNamespace}:${pageName}`;
348 this.#internalLinks.add(resolvedReference);
349 e.reference = resolvedReference;
350 attributes["data-realm"] = "internal";
351 attributes["data-pagename"] = pageName;
352 attributes["data-namespace"] = expandedNamespace;
353 } else {
354 attributes["data-realm"] = "external";
355 const remote = destination ??
356 ast.references[reference]?.destination;
357 if (remote) {
358 externalLinks.set(remote, attributes?.title);
359 } else {
360 /* do nothing */
361 }
362 }
363 },
364 },
365 non_breaking_space: {
366 enter: (_) => {
367 if (titleSoFar != null) {
368 titleSoFar += "\xA0";
369 } else {
370 /* do nothing */
371 }
372 },
373 exit: (_) => {},
374 },
375 section: {
376 enter: (_) => {},
377 exit: (e) => {
378 e.attributes ??= {};
379 const { attributes, children } = e;
380 const heading = children.find(({ tag }) =>
381 tag == "heading"
382 );
383 const title = (() => {
384 if (heading?.attributes?.title) {
385 const result = heading.attributes.title;
386 delete heading.attributes.title;
387 return result;
388 } else {
389 return heading.level == 1
390 ? `${namespace}:${name}`
391 : "untitled section";
392 }
393 })();
394 const variantTitles = Object.create(null);
395 for (const attr in attributes) {
396 if (attr.startsWith("v-")) {
397 Object.defineProperty(
398 variantTitles,
399 attr.substring(2),
400 { ...READ_ONLY, value: attributes[attr] },
401 );
402 delete attributes[attr];
403 } else {
404 continue;
405 }
406 }
407 const definition = Object.create(null, {
408 title: { ...READ_ONLY, value: title },
409 variantTitles: {
410 ...READ_ONLY,
411 value: Object.preventExtensions(variantTitles),
412 },
413 });
414 if (heading.level == 1 && !("main" in sections)) {
415 attributes.id = "main";
416 heading.attributes ??= {};
417 heading.attributes.class = "main";
418 } else {
419 /* do nothing */
420 }
421 try {
422 Object.defineProperty(
423 sections,
424 attributes.id,
425 {
426 ...READ_ONLY,
427 writable: true,
428 value: Object.preventExtensions(definition),
429 },
430 );
431 } catch (cause) {
432 throw new Error(
433 `GitWikiWeb: A section with the provided @id already exists: ${attributes.id}`,
434 { cause },
435 );
436 }
437 },
438 },
439 soft_break: {
440 enter: (_) => {
441 if (titleSoFar != null) {
442 titleSoFar += " ";
443 } else {
444 /* do nothing */
445 }
446 },
447 exit: (_) => {},
448 },
449 str: {
450 enter: ({ text }) => {
451 if (titleSoFar != null) {
452 titleSoFar += text;
453 } else {
454 /* do nothing */
455 }
456 },
457 exit: (_) => {},
458 },
459 symb: {
460 enter: (e) => {
461 const { alias } = e;
462 const codepoint = /^U\+([0-9A-Fa-f]+)$/u.exec(alias)?.[1];
463 if (codepoint) {
464 return str`${
465 String.fromCodePoint(parseInt(codepoint, 16))
466 }`;
467 } else {
468 const resolved = config.symbols?.[alias];
469 return resolved != null ? str`${resolved}` : e;
470 }
471 },
472 },
473 };
474 });
475 Object.defineProperties(this, {
476 ast: { ...READ_ONLY, value: ast },
477 namespace: { ...READ_ONLY, value: namespace },
478 name: { ...READ_ONLY, value: name },
479 sections: {
480 ...READ_ONLY,
481 value: Object.preventExtensions(sections),
482 },
483 source: { ...READ_ONLY, value: source },
484 });
485 }
486
487 *externalLinks() {
488 yield* this.#externalLinks;
489 }
490
491 *internalLinks() {
492 yield* this.#internalLinks;
493 }
494 }
495
496 {
497 // Patches for Djot HTML renderer.
498 const { HTMLRenderer: { prototype: htmlRendererPrototype } } = djot;
499 const { inTags: upstreamInTags } = htmlRendererPrototype;
500 htmlRendererPrototype.inTags = function (
501 tag,
502 node,
503 newlines,
504 extraAttrs = undefined,
505 ) {
506 const attributes = node.attributes ?? NIL;
507 if ("as" in attributes) {
508 const newTag = attributes.as;
509 delete attributes.as;
510 return upstreamInTags.call(
511 this,
512 newTag,
513 node,
514 newlines,
515 extraAttrs,
516 );
517 } else {
518 return upstreamInTags.call(
519 this,
520 tag,
521 node,
522 newlines,
523 extraAttrs,
524 );
525 }
526 };
527 }
528 {
529 const config = await getRemoteContent("config.yaml").then((yaml) =>
530 parseYaml(yaml, { schema: JSON_SCHEMA })
531 );
532 const ls = new Deno.Command("git", {
533 args: ["ls-tree", "-rz", "HEAD"],
534 stdout: "piped",
535 stderr: "piped",
536 }).spawn();
537 const [
538 objects,
539 lserr,
540 lsstatus,
541 ] = await Promise.allSettled([
542 new Response(ls.stdout).text().then((lsout) =>
543 lsout
544 .split("\0")
545 .slice(0, -1) // drop the last entry; it is empty
546 .map(($) => $.split(/\s+/g))
547 ),
548 new Response(ls.stderr).text(),
549 ls.status,
550 ]).then(logErrorsAndCollectResults);
551 if (lserr) {
552 console.error(lserr);
553 } else {
554 /* do nothing */
555 }
556 if (!lsstatus.success) {
557 throw new Error(
558 `GitWikiWeb: git ls-tree returned nonzero exit code: ${lsstatus.code}.`,
559 );
560 } else {
561 const requiredButMissingPages = new Map([
562 ["Special:FrontPage", "front page"],
563 ["Special:NotFound", "not found"],
564 ["Special:RecentlyChanged", "recently changed"],
565 ]);
566 const pages = new Map();
567 const promises = [emptyDir(DESTINATION)];
568 for (const object of objects) {
569 const hash = object[2];
570 const path = object[3];
571 const reference = getReferenceFromPath(path);
572 if (reference == null) {
573 continue;
574 } else {
575 const [namespace, pageName] = splitReference(reference);
576 const cat = new Deno.Command("git", {
577 args: ["cat-file", "blob", hash],
578 stdout: "piped",
579 stderr: "piped",
580 }).spawn();
581 const promise = Promise.allSettled([
582 new Response(cat.stdout).text(),
583 new Response(cat.stderr).text(),
584 cat.status,
585 ]).then(logErrorsAndCollectResults).then(
586 ([source, caterr, catstatus]) => {
587 if (caterr) {
588 console.error(caterr);
589 } else {
590 /* do nothing */
591 }
592 if (!catstatus.success) {
593 throw new Error(
594 `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
595 );
596 } else {
597 const page = new GitWikiWebPage(
598 namespace,
599 pageName,
600 djot.parse(source, {
601 warn: ($) =>
602 console.warn(`Djot(${reference}): ${$.render()}`),
603 }),
604 source,
605 config,
606 );
607 const reference = `${namespace}:${pageName}`;
608 pages.set(reference, page);
609 requiredButMissingPages.delete(reference);
610 }
611 },
612 );
613 promises.push(promise);
614 }
615 }
616 for (const [reference, defaultTitle] of requiredButMissingPages) {
617 const [namespace, pageName] = splitReference(reference);
618 const source = `# ${defaultTitle}\n`;
619 const page = new GitWikiWebPage(
620 namespace,
621 pageName,
622 djot.parse(source, {
623 warn: ($) =>
624 console.warn(`Djot(${reference}): ${$.render()}`),
625 }),
626 source,
627 config,
628 );
629 pages.set(reference, page);
630 }
631 await Promise.allSettled(promises).then(
632 logErrorsAndCollectResults,
633 );
634 const [template, recentlyChanged] = await Promise.allSettled([
635 getRemoteContent("template.html"),
636 (async () => {
637 const dateParse = new Deno.Command("git", {
638 args: ["rev-parse", "--after=1 week ago"],
639 stdout: "piped",
640 stderr: "piped",
641 }).spawn();
642 const [maxAge] = await Promise.allSettled([
643 new Response(dateParse.stdout).text(),
644 new Response(dateParse.stderr).text(),
645 ]).then(logErrorsAndCollectResults);
646 let commit;
647 if (!maxAge) {
648 /* do nothing */
649 } else {
650 const revList = new Deno.Command("git", {
651 args: ["rev-list", maxAge, "--reverse", "HEAD"],
652 stdout: "piped",
653 stderr: "piped",
654 }).spawn();
655 [commit] = await Promise.allSettled([
656 new Response(revList.stdout).text().then((list) =>
657 list.split("\n")[0]
658 ),
659 new Response(revList.stderr).text(),
660 ]).then(logErrorsAndCollectResults);
661 }
662 if (!commit) {
663 const revList2 = new Deno.Command("git", {
664 args: ["rev-list", "--max-count=1", "HEAD^"],
665 stdout: "piped",
666 stderr: "piped",
667 }).spawn();
668 [commit] = await Promise.allSettled([
669 new Response(revList2.stdout).text().then((list) =>
670 list.trim()
671 ),
672 new Response(revList2.stderr).text(),
673 ]).then(logErrorsAndCollectResults);
674 } else {
675 /* do nothing */
676 }
677 const results = new Array(6);
678 const seen = new Set();
679 const maxRecency = Math.max(config.max_recency | 0, 0);
680 let recency = maxRecency;
681 let current;
682 do {
683 const show = new Deno.Command("git", {
684 args: [
685 "show",
686 "-s",
687 "--format=%H%x00%cI%x00%cD",
688 recency ? `HEAD~${maxRecency - recency}` : commit,
689 ],
690 stdout: "piped",
691 stderr: "piped",
692 }).spawn();
693 const [
694 [hash, dateTime, humanReadable],
695 ] = await Promise.allSettled([
696 new Response(show.stdout).text().then((rev) =>
697 rev.trim().split("\0")
698 ),
699 new Response(show.stderr).text(),
700 ]).then(logErrorsAndCollectResults);
701 const refs = [];
702 current = hash;
703 for (
704 const ref of (await diffReferences(current, !recency))
705 ) {
706 if (seen.has(ref)) {
707 /* do nothing */
708 } else {
709 refs.push(ref);
710 seen.add(ref);
711 }
712 }
713 results[recency] = { dateTime, hash, humanReadable, refs };
714 } while (recency-- > 0 && current && current != commit);
715 return results;
716 })(),
717 ...Array.from(
718 pages.keys(),
719 (name) => ensureDir(`${DESTINATION}/${name}`),
720 ),
721 ["style.css"].map((dependency) =>
722 getRemoteContent(dependency).then((source) =>
723 Deno.writeTextFile(
724 `${DESTINATION}/${dependency}`,
725 source,
726 { createNew: true },
727 )
728 )
729 ),
730 ]).then(logErrorsAndCollectResults);
731 promises.length = 0;
732 const redLinks = (() => {
733 const result = new Set();
734 for (const page of pages.values()) {
735 for (const link of page.internalLinks()) {
736 if (pages.has(link)) {
737 continue;
738 } else {
739 result.add(link);
740 }
741 }
742 }
743 return result;
744 })();
745 for (
746 const [pageRef, { ast, namespace, sections, source }] of pages
747 ) {
748 const title = sections.main?.title ?? pageRef;
749 djot.applyFilter(ast, () => {
750 let isNavigationPage = true;
751 return {
752 doc: {
753 enter: (e) => {
754 const { content, navigation } = (() => {
755 const navigation = [];
756 if (pageRef == "Special:RecentlyChanged") {
757 navigation.push({
758 tag: "bullet_list",
759 attributes: { class: "recent-changes" },
760 tight: true,
761 style: "*",
762 children: Array.from(function* () {
763 for (
764 const [index, result] of recentlyChanged
765 .entries()
766 ) {
767 if (result != null) {
768 const {
769 dateTime,
770 humanReadable,
771 refs,
772 } = result;
773 yield* listOfInternalLinks(refs, (link) => ({
774 tag: index == 0 ? "span" : "strong",
775 attributes: { "data-recency": `${index}` },
776 children: [
777 link,
778 ...(index == 0 ? [] : [
779 str` `,
780 rawInline`<small>(<time dateTime="${dateTime}">`,
781 str`${humanReadable}`,
782 rawInline`</time>)</small>`,
783 ]),
784 ],
785 })).children;
786 } else {
787 /* do nothing */
788 }
789 }
790 }()).reverse(),
791 });
792 } else {
793 isNavigationPage = false;
794 return { content: e.children, navigation };
795 }
796 return {
797 content: [
798 {
799 tag: "heading",
800 attributes: {
801 class: "main",
802 generated: "", // will be removed later
803 },
804 level: 1,
805 children: [str`${title}`],
806 },
807 rawBlock`<details id="navigation-about" open="">`,
808 rawBlock`<summary>about this listing</summary>`,
809 rawBlock`<article>`,
810 ...e.children,
811 rawBlock`</article>`,
812 rawBlock`</details>`,
813 ],
814 navigation: [
815 rawBlock`<nav id="navigation">`,
816 ...navigation,
817 rawBlock`</nav>`,
818 ],
819 };
820 })();
821 e.children = [
822 rawBlock`<article>`,
823 ...content,
824 ...navigation,
825 rawBlock`</article>`,
826 ];
827 },
828 exit: (_) => {},
829 },
830 heading: {
831 enter: (e) => {
832 const attributes = e.attributes ?? NIL;
833 if (
834 isNavigationPage && e.level == 1 &&
835 attributes?.class == "main"
836 ) {
837 if ("generated" in attributes) {
838 delete attributes.generated;
839 } else {
840 return { stop: [] };
841 }
842 } else {
843 /* do nothing */
844 }
845 },
846 exit: (e) => {
847 if (e.level == 1 && e.attributes?.class == "main") {
848 return [
849 rawBlock`<header class="main">`,
850 e,
851 { tag: "verbatim", text: pageRef },
852 rawBlock`</header>`,
853 ];
854 } else {
855 /* do nothing */
856 }
857 },
858 },
859 link: {
860 enter: (_) => {},
861 exit: (e) => {
862 e.attributes ??= {};
863 const { attributes, children, reference } = e;
864 if (attributes["data-realm"] == "internal") {
865 delete e.reference;
866 if (redLinks.has(reference)) {
867 e.destination =
868 `/Special:NotFound?path=/${reference}`;
869 attributes["data-notfound"] = "";
870 } else {
871 e.destination = `/${reference}`;
872 }
873 if (children.length == 0) {
874 const section =
875 pages.get(reference)?.sections?.main ?? NIL;
876 const { v } = attributes;
877 if (v == null) {
878 children.push(
879 str`${section.title ?? reference}`,
880 );
881 } else {
882 delete attributes.v;
883 children.push(
884 str`${
885 section.variantTitles?.[v] ?? section.title ??
886 reference
887 }`,
888 );
889 }
890 }
891 } else {
892 if (children.length == 0 && "title" in attributes) {
893 children.push(
894 rawInline`<cite>`,
895 str`${attributes.title}`,
896 rawInline`</cite>`,
897 );
898 }
899 }
900 if (
901 (attributes.class ?? "").split(/\s/gu).includes("sig")
902 ) {
903 return {
904 tag: "span",
905 attributes: { class: "sig" },
906 children: [str`—${"\xA0"}`, e],
907 };
908 } else {
909 /* do nothing */
910 }
911 },
912 },
913 section: {
914 enter: (_) => {},
915 exit: (e) => {
916 if (e.children.length < 1) {
917 // The heading for this section was removed and it had
918 // no other children.
919 return [];
920 } else {
921 /* do nothing */
922 }
923 },
924 },
925 };
926 });
927 const renderedAST = djot.renderAST(ast);
928 const doc = getDOM(template);
929 const result = getDOM(djot.renderHTML(ast, {
930 overrides: {
931 raw_block: (node, context) => {
932 if (node.format == "html" && node.text == "\uFFFF") {
933 if (context.nextFootnoteIndex > 1) {
934 const result = context.renderNotes(ast.footnotes);
935 context.nextFootnoteIndex = 1;
936 return result;
937 } else {
938 return "";
939 }
940 } else {
941 return context.renderAstNodeDefault(node);
942 }
943 },
944 },
945 }));
946 const headElement = domutils.findOne(
947 (node) => node.name == "head",
948 doc,
949 );
950 const titleElement = domutils.findOne(
951 (node) => node.name == "title",
952 headElement,
953 );
954 const contentElement = domutils.findOne(
955 (node) => node.name == "gitwikiweb-content",
956 doc,
957 );
958 if (headElement == null) {
959 throw new Error(
960 "GitWikiWeb: Template must explicitly include a <head> element.",
961 );
962 } else {
963 domutils.appendChild(
964 headElement,
965 new Element("link", {
966 rel: "source",
967 type: "text/x.djot",
968 href: `/${pageRef}/source.djot`,
969 }),
970 );
971 if (titleElement == null) {
972 domutils.prependChild(
973 headElement,
974 new Element("title", {}, [new Text(title)]),
975 );
976 } else {
977 domutils.prependChild(titleElement, new Text(`${title} | `));
978 }
979 }
980 if (contentElement == null) {
981 throw new Error(
982 "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
983 );
984 } else {
985 for (const node of [...result]) {
986 domutils.prepend(contentElement, node);
987 }
988 domutils.removeElement(contentElement);
989 }
990 promises.push(
991 Deno.writeTextFile(
992 `${DESTINATION}/${pageRef}/index.html`,
993 domSerializer(doc, {
994 emptyAttrs: true,
995 encodeEntities: "utf8",
996 selfClosingTags: true,
997 xmlMode: false,
998 }),
999 { createNew: true },
1000 ),
1001 );
1002 promises.push(
1003 Deno.writeTextFile(
1004 `${DESTINATION}/${pageRef}/index.ast`,
1005 renderedAST,
1006 { createNew: true },
1007 ),
1008 );
1009 promises.push(
1010 Deno.writeTextFile(
1011 `${DESTINATION}/${pageRef}/source.djot`,
1012 source,
1013 { createNew: true },
1014 ),
1015 );
1016 }
1017 await Promise.allSettled(promises).then(
1018 logErrorsAndCollectResults,
1019 );
1020 console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
1021 }
1022 }
This page took 0.302322 seconds and 3 git commands to generate.