]> Lady’s Gitweb - GitWikiWeb/blob - build.js
Add symbol substitutions
[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) => {
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 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 (const ref of (await diffReferences(current))) {
696 if (seen.has(ref)) {
697 /* do nothing */
698 } else {
699 refs.push(ref);
700 seen.add(ref);
701 }
702 }
703 results[recency] = { dateTime, humanReadable, refs };
704 } while (recency-- > 0 && current && current != commit);
705 return results;
706 })(),
707 ...Array.from(
708 pages.keys(),
709 (name) => ensureDir(`${DESTINATION}/${name}`),
710 ),
711 ["style.css"].map((dependency) =>
712 getRemoteContent(dependency).then((source) =>
713 Deno.writeTextFile(
714 `${DESTINATION}/${dependency}`,
715 source,
716 { createNew: true },
717 )
718 )
719 ),
720 ]).then(logErrorsAndCollectResults);
721 promises.length = 0;
722 const redLinks = (() => {
723 const result = new Set();
724 for (const page of pages.values()) {
725 for (const link of page.internalLinks()) {
726 if (pages.has(link)) {
727 continue;
728 } else {
729 result.add(link);
730 }
731 }
732 }
733 return result;
734 })();
735 for (
736 const [pageRef, { ast, namespace, sections, source }] of pages
737 ) {
738 const title = sections.main?.title ?? pageRef;
739 djot.applyFilter(ast, () => {
740 let isNavigationPage = true;
741 return {
742 doc: {
743 enter: (e) => {
744 const { content, navigation } = (() => {
745 const navigation = [];
746 if (pageRef == "Special:RecentlyChanged") {
747 navigation.push({
748 tag: "bullet_list",
749 attributes: { class: "recent-changes" },
750 tight: true,
751 style: "*",
752 children: Array.from(function* () {
753 for (
754 const [index, result] of recentlyChanged
755 .entries()
756 ) {
757 if (result != null) {
758 const {
759 dateTime,
760 humanReadable,
761 refs,
762 } = result;
763 yield* listOfInternalLinks(refs, (link) => ({
764 tag: index == 0 ? "span" : "strong",
765 attributes: { "data-recency": `${index}` },
766 children: [
767 link,
768 ...(index == 0 ? [] : [
769 str` `,
770 rawInline`<small>(<time dateTime="${dateTime}">`,
771 str`${humanReadable}`,
772 rawInline`</time>)</small>`,
773 ]),
774 ],
775 })).children;
776 } else {
777 /* do nothing */
778 }
779 }
780 }()).reverse(),
781 });
782 } else {
783 isNavigationPage = false;
784 return { content: e.children, navigation };
785 }
786 return {
787 content: [
788 {
789 tag: "heading",
790 attributes: {
791 class: "main",
792 generated: "", // will be removed later
793 },
794 level: 1,
795 children: [str`${title}`],
796 },
797 rawBlock`<details id="navigation-about" open="">`,
798 rawBlock`<summary>about this listing</summary>`,
799 rawBlock`<article>`,
800 ...e.children,
801 rawBlock`</article>`,
802 rawBlock`</details>`,
803 ],
804 navigation: [
805 rawBlock`<nav id="navigation">`,
806 ...navigation,
807 rawBlock`</nav>`,
808 ],
809 };
810 })();
811 e.children = [
812 rawBlock`<article>`,
813 ...content,
814 ...navigation,
815 rawBlock`</article>`,
816 ];
817 },
818 exit: (_) => {},
819 },
820 heading: {
821 enter: (e) => {
822 const attributes = e.attributes ?? NIL;
823 if (
824 isNavigationPage && e.level == 1 &&
825 attributes?.class == "main"
826 ) {
827 if ("generated" in attributes) {
828 delete attributes.generated;
829 } else {
830 return { stop: [] };
831 }
832 } else {
833 /* do nothing */
834 }
835 },
836 exit: (e) => {
837 if (e.level == 1 && e.attributes?.class == "main") {
838 return [
839 rawBlock`<header class="main">`,
840 e,
841 { tag: "verbatim", text: pageRef },
842 rawBlock`</header>`,
843 ];
844 } else {
845 /* do nothing */
846 }
847 },
848 },
849 link: {
850 enter: (_) => {},
851 exit: (e) => {
852 e.attributes ??= {};
853 const { attributes, children, reference } = e;
854 if (attributes["data-realm"] == "internal") {
855 delete e.reference;
856 if (redLinks.has(reference)) {
857 e.destination =
858 `/Special:NotFound?path=/${reference}`;
859 attributes["data-notfound"] = "";
860 } else {
861 e.destination = `/${reference}`;
862 }
863 if (children.length == 0) {
864 const section =
865 pages.get(reference)?.sections?.main ?? NIL;
866 const { v } = attributes;
867 if (v == null) {
868 children.push(
869 str`${section.title ?? reference}`,
870 );
871 } else {
872 delete attributes.v;
873 children.push(
874 str`${
875 section.variantTitles?.[v] ?? section.title ??
876 reference
877 }`,
878 );
879 }
880 }
881 } else {
882 if (children.length == 0 && "title" in attributes) {
883 children.push(
884 rawInline`<cite>`,
885 str`${attributes.title}`,
886 rawInline`</cite>`,
887 );
888 }
889 }
890 if (
891 (attributes.class ?? "").split(/\s/gu).includes("sig")
892 ) {
893 return {
894 tag: "span",
895 attributes: { class: "sig" },
896 children: [str`—${"\xA0"}`, e],
897 };
898 } else {
899 /* do nothing */
900 }
901 },
902 },
903 section: {
904 enter: (_) => {},
905 exit: (e) => {
906 if (e.children.length < 1) {
907 // The heading for this section was removed and it had
908 // no other children.
909 return [];
910 } else {
911 /* do nothing */
912 }
913 },
914 },
915 };
916 });
917 const doc = getDOM(template);
918 const result = getDOM(`${djot.renderHTML(ast)}`);
919 const headElement = domutils.findOne(
920 (node) => node.name == "head",
921 doc,
922 );
923 const titleElement = domutils.findOne(
924 (node) => node.name == "title",
925 headElement,
926 );
927 const contentElement = domutils.findOne(
928 (node) => node.name == "gitwikiweb-content",
929 doc,
930 );
931 if (headElement == null) {
932 throw new Error(
933 "GitWikiWeb: Template must explicitly include a <head> element.",
934 );
935 } else {
936 domutils.appendChild(
937 headElement,
938 new Element("link", {
939 rel: "source",
940 type: "text/x.djot",
941 href: `/${pageRef}/source.djot`,
942 }),
943 );
944 if (titleElement == null) {
945 domutils.prependChild(
946 headElement,
947 new Element("title", {}, [new Text(title)]),
948 );
949 } else {
950 domutils.prependChild(titleElement, new Text(`${title} | `));
951 }
952 }
953 if (contentElement == null) {
954 throw new Error(
955 "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
956 );
957 } else {
958 for (const node of result) {
959 domutils.prepend(contentElement, node);
960 }
961 domutils.removeElement(contentElement);
962 }
963 promises.push(
964 Deno.writeTextFile(
965 `${DESTINATION}/${pageRef}/index.html`,
966 domSerializer(doc, {
967 emptyAttrs: true,
968 encodeEntities: "utf8",
969 selfClosingTags: true,
970 xmlMode: false,
971 }),
972 { createNew: true },
973 ),
974 );
975 promises.push(
976 Deno.writeTextFile(
977 `${DESTINATION}/${pageRef}/source.djot`,
978 source,
979 { createNew: true },
980 ),
981 );
982 }
983 await Promise.allSettled(promises).then(
984 logErrorsAndCollectResults,
985 );
986 console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
987 }
988 }
This page took 0.23819 seconds and 5 git commands to generate.