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