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