]> Lady’s Gitweb - GitWikiWeb/blob - build.js
0544a03217e4407eb18863058ac0486933f7cb8f
[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 };
475 });
476 Object.defineProperties(this, {
477 ast: { ...READ_ONLY, value: ast },
478 namespace: { ...READ_ONLY, value: namespace },
479 name: { ...READ_ONLY, value: name },
480 sections: {
481 ...READ_ONLY,
482 value: Object.preventExtensions(sections),
483 },
484 source: { ...READ_ONLY, value: source },
485 });
486 }
487
488 *externalLinks() {
489 yield* this.#externalLinks;
490 }
491
492 *internalLinks() {
493 yield* this.#internalLinks;
494 }
495 }
496
497 {
498 const ls = new Deno.Command("git", {
499 args: ["ls-tree", "-rz", "live"],
500 stdout: "piped",
501 stderr: "piped",
502 }).spawn();
503 const [
504 objects,
505 lserr,
506 lsstatus,
507 ] = await Promise.allSettled([
508 new Response(ls.stdout).text().then((lsout) =>
509 lsout
510 .split("\0")
511 .slice(0, -1) // drop the last entry; it is empty
512 .map(($) => $.split(/\s+/g))
513 ),
514 new Response(ls.stderr).text(),
515 ls.status,
516 ]).then(logErrorsAndCollectResults);
517 if (lserr) {
518 console.error(lserr);
519 } else {
520 /* do nothing */
521 }
522 if (!lsstatus.success) {
523 throw new Error(
524 `GitWikiWeb: git ls-tree returned nonzero exit code: ${lsstatus.code}.`,
525 );
526 } else {
527 const requiredButMissingPages = new Map([
528 ["Special:FrontPage", "front page"],
529 ["Special:NotFound", "not found"],
530 ["Special:RecentlyChanged", "recently changed"],
531 ]);
532 const pages = new Map();
533 const promises = [emptyDir(DESTINATION)];
534 for (const object of objects) {
535 const hash = object[2];
536 const path = object[3];
537 const reference = getReferenceFromPath(path);
538 if (reference == null) {
539 continue;
540 } else {
541 const [namespace, pageName] = splitReference(reference);
542 const cat = new Deno.Command("git", {
543 args: ["cat-file", "blob", hash],
544 stdout: "piped",
545 stderr: "piped",
546 }).spawn();
547 const promise = Promise.allSettled([
548 new Response(cat.stdout).text(),
549 new Response(cat.stderr).text(),
550 cat.status,
551 ]).then(logErrorsAndCollectResults).then(
552 ([source, caterr, catstatus]) => {
553 if (caterr) {
554 console.error(caterr);
555 } else {
556 /* do nothing */
557 }
558 if (!catstatus.success) {
559 throw new Error(
560 `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
561 );
562 } else {
563 const page = new GitWikiWebPage(
564 namespace,
565 pageName,
566 djot.parse(source, {
567 warn: ($) =>
568 console.warn(`Djot(${reference}): ${$.render()}`),
569 }),
570 source,
571 );
572 const reference = `${namespace}:${pageName}`;
573 pages.set(reference, page);
574 requiredButMissingPages.delete(reference);
575 }
576 },
577 );
578 promises.push(promise);
579 }
580 }
581 for (const [reference, defaultTitle] of requiredButMissingPages) {
582 const [namespace, pageName] = splitReference(reference);
583 const source = `# ${defaultTitle}\n`;
584 const page = new GitWikiWebPage(
585 namespace,
586 pageName,
587 djot.parse(source, {
588 warn: ($) =>
589 console.warn(`Djot(${reference}): ${$.render()}`),
590 }),
591 source,
592 );
593 pages.set(reference, page);
594 }
595 await Promise.allSettled(promises).then(
596 logErrorsAndCollectResults,
597 );
598 const [template, recentlyChanged] = await Promise.allSettled([
599 getRemoteContent("template.html"),
600 (async () => {
601 const dateParse = new Deno.Command("git", {
602 args: ["rev-parse", "--after=1 week ago"],
603 stdout: "piped",
604 stderr: "piped",
605 }).spawn();
606 const [maxAge] = await Promise.allSettled([
607 new Response(dateParse.stdout).text(),
608 new Response(dateParse.stderr).text(),
609 ]).then(logErrorsAndCollectResults);
610 let commit;
611 if (!maxAge) {
612 /* do nothing */
613 } else {
614 const revList = new Deno.Command("git", {
615 args: ["rev-list", maxAge, "--reverse", "HEAD"],
616 stdout: "piped",
617 stderr: "piped",
618 }).spawn();
619 [commit] = await Promise.allSettled([
620 new Response(revList.stdout).text().then((list) =>
621 list.split("\n")[0]
622 ),
623 new Response(revList.stderr).text(),
624 ]).then(logErrorsAndCollectResults);
625 }
626 if (!commit) {
627 const revList2 = new Deno.Command("git", {
628 args: ["rev-list", "--max-count=1", "HEAD^"],
629 stdout: "piped",
630 stderr: "piped",
631 }).spawn();
632 [commit] = await Promise.allSettled([
633 new Response(revList2.stdout).text().then((list) =>
634 list.trim()
635 ),
636 new Response(revList2.stderr).text(),
637 ]).then(logErrorsAndCollectResults);
638 } else {
639 /* do nothing */
640 }
641 const results = new Array(6);
642 const seen = new Set();
643 let recency = 5;
644 let current;
645 do {
646 const show = new Deno.Command("git", {
647 args: [
648 "show",
649 "-s",
650 "--format=%H%x00%cI%x00%cD",
651 recency ? `HEAD~${5 - recency}` : commit,
652 ],
653 stdout: "piped",
654 stderr: "piped",
655 }).spawn();
656 const [
657 [hash, dateTime, humanReadable],
658 ] = await Promise.allSettled([
659 new Response(show.stdout).text().then((rev) =>
660 rev.trim().split("\0")
661 ),
662 new Response(show.stderr).text(),
663 ]).then(logErrorsAndCollectResults);
664 const refs = [];
665 current = hash;
666 for (const ref of (await diffReferences(current))) {
667 if (seen.has(ref)) {
668 /* do nothing */
669 } else {
670 refs.push(ref);
671 seen.add(ref);
672 }
673 }
674 results[recency] = { dateTime, humanReadable, refs };
675 } while (recency-- > 0 && current && current != commit);
676 return results;
677 })(),
678 ...Array.from(
679 pages.keys(),
680 (name) => ensureDir(`${DESTINATION}/${name}`),
681 ),
682 ["style.css"].map((dependency) =>
683 getRemoteContent(dependency).then((source) =>
684 Deno.writeTextFile(
685 `${DESTINATION}/${dependency}`,
686 source,
687 { createNew: true },
688 )
689 )
690 ),
691 ]).then(logErrorsAndCollectResults);
692 promises.length = 0;
693 const redLinks = (() => {
694 const result = new Set();
695 for (const page of pages.values()) {
696 for (const link of page.internalLinks()) {
697 if (pages.has(link)) {
698 continue;
699 } else {
700 result.add(link);
701 }
702 }
703 }
704 return result;
705 })();
706 for (
707 const [pageRef, { ast, namespace, sections, source }] of pages
708 ) {
709 const title = sections.main?.title ?? pageRef;
710 djot.applyFilter(ast, () => {
711 let isNavigationPage = true;
712 return {
713 doc: {
714 enter: (e) => {
715 const { content, navigation } = (() => {
716 const navigation = [];
717 if (pageRef == "Special:RecentlyChanged") {
718 navigation.push({
719 tag: "bullet_list",
720 attributes: { class: "recent-changes" },
721 tight: true,
722 style: "*",
723 children: Array.from(function* () {
724 for (
725 const [index, result] of recentlyChanged
726 .entries()
727 ) {
728 if (result != null) {
729 const {
730 dateTime,
731 humanReadable,
732 refs,
733 } = result;
734 yield* listOfInternalLinks(refs, (link) => ({
735 tag: index == 0 ? "span" : "strong",
736 attributes: { "data-recency": `${index}` },
737 children: [
738 link,
739 ...(index == 0 ? [] : [
740 str` `,
741 rawInline`<small>(<time dateTime="${dateTime}">`,
742 str`${humanReadable}`,
743 rawInline`</time>)</small>`,
744 ]),
745 ],
746 })).children;
747 } else {
748 /* do nothing */
749 }
750 }
751 }()).reverse(),
752 });
753 } else {
754 isNavigationPage = false;
755 return { content: e.children, navigation };
756 }
757 return {
758 content: [
759 {
760 tag: "heading",
761 attributes: {
762 class: "main",
763 generated: "", // will be removed later
764 },
765 level: 1,
766 children: [str`${title}`],
767 },
768 rawBlock`<details id="navigation-about" open="">`,
769 rawBlock`<summary>about this listing</summary>`,
770 rawBlock`<article>`,
771 ...e.children,
772 rawBlock`</article>`,
773 rawBlock`</details>`,
774 ],
775 navigation: [
776 rawBlock`<nav id="navigation">`,
777 ...navigation,
778 rawBlock`</nav>`,
779 ],
780 };
781 })();
782 e.children = [
783 rawBlock`<article>`,
784 ...content,
785 ...navigation,
786 rawBlock`</article>`,
787 ];
788 },
789 exit: (_) => {},
790 },
791 heading: {
792 enter: (e) => {
793 const attributes = e.attributes ?? NIL;
794 if (
795 isNavigationPage && e.level == 1 &&
796 attributes?.class == "main"
797 ) {
798 if ("generated" in attributes) {
799 delete attributes.generated;
800 } else {
801 return { stop: [] };
802 }
803 } else {
804 /* do nothing */
805 }
806 },
807 exit: (e) => {
808 if (e.level == 1 && e.attributes?.class == "main") {
809 return [
810 rawBlock`<header class="main">`,
811 e,
812 { tag: "verbatim", text: pageRef },
813 rawBlock`</header>`,
814 ];
815 } else {
816 /* do nothing */
817 }
818 },
819 },
820 link: {
821 enter: (_) => {},
822 exit: (e) => {
823 e.attributes ??= {};
824 const { attributes, children, reference } = e;
825 if (attributes["data-realm"] == "internal") {
826 delete e.reference;
827 if (redLinks.has(reference)) {
828 e.destination =
829 `/Special:NotFound?path=/${reference}`;
830 attributes["data-notfound"] = "";
831 } else {
832 e.destination = `/${reference}`;
833 }
834 if (children.length == 0) {
835 const section =
836 pages.get(reference)?.sections?.main ?? NIL;
837 const { v } = attributes;
838 if (v == null) {
839 children.push(
840 str`${section.title ?? reference}`,
841 );
842 } else {
843 delete attributes.v;
844 children.push(
845 str`${
846 section.variantTitles?.[v] ?? section.title ??
847 reference
848 }`,
849 );
850 }
851 }
852 } else {
853 if (children.length == 0 && "title" in attributes) {
854 children.push(
855 rawInline`<cite>`,
856 str`${attributes.title}`,
857 rawInline`</cite>`,
858 );
859 }
860 }
861 if (
862 (attributes.class ?? "").split(/\s/gu).includes("sig")
863 ) {
864 return {
865 tag: "span",
866 attributes: { class: "sig" },
867 children: [str`—${"\xA0"}`, e],
868 };
869 } else {
870 /* do nothing */
871 }
872 },
873 },
874 section: {
875 enter: (_) => {},
876 exit: (e) => {
877 if (e.children.length < 1) {
878 // The heading for this section was removed and it had
879 // no other children.
880 return [];
881 } else {
882 /* do nothing */
883 }
884 },
885 },
886 };
887 });
888 const doc = getDOM(template);
889 const result = getDOM(`${djot.renderHTML(ast)}`);
890 const headElement = domutils.findOne(
891 (node) => node.name == "head",
892 doc,
893 );
894 const titleElement = domutils.findOne(
895 (node) => node.name == "title",
896 headElement,
897 );
898 const contentElement = domutils.findOne(
899 (node) => node.name == "gitwikiweb-content",
900 doc,
901 );
902 if (headElement == null) {
903 throw new Error(
904 "GitWikiWeb: Template must explicitly include a <head> element.",
905 );
906 } else {
907 domutils.appendChild(
908 headElement,
909 new Element("link", {
910 rel: "source",
911 type: "text/x.djot",
912 href: `/${pageRef}/source.djot`,
913 }),
914 );
915 if (titleElement == null) {
916 domutils.prependChild(
917 headElement,
918 new Element("title", {}, [new Text(title)]),
919 );
920 } else {
921 domutils.prependChild(titleElement, new Text(`${title} | `));
922 }
923 }
924 if (contentElement == null) {
925 throw new Error(
926 "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
927 );
928 } else {
929 for (const node of result) {
930 domutils.prepend(contentElement, node);
931 }
932 domutils.removeElement(contentElement);
933 }
934 promises.push(
935 Deno.writeTextFile(
936 `${DESTINATION}/${pageRef}/index.html`,
937 domSerializer(doc, {
938 emptyAttrs: true,
939 encodeEntities: "utf8",
940 selfClosingTags: true,
941 xmlMode: false,
942 }),
943 { createNew: true },
944 ),
945 );
946 promises.push(
947 Deno.writeTextFile(
948 `${DESTINATION}/${pageRef}/source.djot`,
949 source,
950 { createNew: true },
951 ),
952 );
953 }
954 await Promise.allSettled(promises).then(
955 logErrorsAndCollectResults,
956 );
957 console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
958 }
959 }
This page took 0.276415 seconds and 3 git commands to generate.