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