]> Lady’s Gitweb - GitWikiWeb/blobdiff - build.js
Update djot.js to 0.3.1
[GitWikiWeb] / build.js
index 5c8bd39c54f317780bcab94f79a2f4f0f56d45c1..23b30bb0ab1563ded405a40862b03272daa539c2 100644 (file)
--- a/build.js
+++ b/build.js
@@ -20,7 +20,7 @@
 //     export GITWIKIWEB=/srv/git/GitWikiWeb
 //     git archive --remote=$GITWIKIWEB HEAD build.js \
 //       | tar -xO \
-//       | deno run -A - ~/public/wiki $GITWIKIWEB
+//       | deno run -A - ~/public/wiki $GITWIKIWEB current
 //
 // The directory `~/public/wiki` (or whatever you specify as the first
 // argument to `deno run -A -`) **will be deleted** and a new static
 import {
   emptyDir,
   ensureDir,
-} from "https://deno.land/std@0.195.0/fs/mod.ts";
-import djot from "npm:@djot/djot@0.2.3";
+} from "https://deno.land/std@0.196.0/fs/mod.ts";
+import {
+  JSON_SCHEMA,
+  parse as parseYaml,
+} from "https://deno.land/std@0.196.0/yaml/mod.ts";
+import djot from "npm:@djot/djot@0.3.1";
 import { Parser } from "npm:htmlparser2@9.0.0";
 import { DomHandler, Element, Text } from "npm:domhandler@5.0.3";
 import * as domutils from "npm:domutils@3.1.0";
@@ -46,6 +50,7 @@ import domSerializer from "npm:dom-serializer@2.0.0";
 
 const DESTINATION = Deno.args[0] ?? "~/public/wiki";
 const REMOTE = Deno.args[1] ?? "/srv/git/GitWikiWeb";
+const REV = Deno.args[2] ?? "HEAD";
 
 const READ_ONLY = {
   configurable: false,
@@ -53,6 +58,8 @@ const READ_ONLY = {
   writable: false,
 };
 
+const NIL = Object.preventExtensions(Object.create(null));
+
 const rawBlock = (strings, ...substitutions) => ({
   tag: "raw_block",
   format: "html",
@@ -87,7 +94,7 @@ const getDOM = (source) => {
 
 const getRemoteContent = async (pathName) => {
   const getArchive = new Deno.Command("git", {
-    args: ["archive", `--remote=${REMOTE}`, "HEAD", pathName],
+    args: ["archive", `--remote=${REMOTE}`, REV, pathName],
     stdout: "piped",
     stderr: "piped",
   }).spawn();
@@ -140,8 +147,8 @@ const logErrorsAndCollectResults = (results) =>
   });
 
 const getReferenceFromPath = (path) =>
-  /Sources\/([A-Z][0-9A-Za-z]*\/[A-Z][0-9A-Za-z]*)\.djot$/u.exec(path)
-    ?.[1]?.replace?.("/", ":");
+  /Sources\/([A-Z][0-9A-Za-z]*(?:\/[A-Z][0-9A-Za-z]*)+)\.djot$/u
+    .exec(path)?.[1]?.replace?.("/", ":"); // only replaces first slash
 
 const listOfInternalLinks = (references, wrapper = ($) => $) => ({
   tag: "bullet_list",
@@ -171,7 +178,7 @@ const listOfInternalLinks = (references, wrapper = ($) => $) => ({
   ),
 });
 
-const diffReferences = async (hash) => {
+const diffReferences = async (hash, againstHead = false) => {
   const diff = new Deno.Command("git", {
     args: [
       "diff-tree",
@@ -180,7 +187,7 @@ const diffReferences = async (hash) => {
       "--name-only",
       "--no-renames",
       "--diff-filter=AM",
-      hash,
+      ...(againstHead ? [hash, "HEAD"] : [hash]),
     ],
     stdout: "piped",
     stderr: "piped",
@@ -215,85 +222,121 @@ class GitWikiWebPage {
   #internalLinks = new Set();
   #externalLinks = new Map();
 
-  constructor(namespace, name, ast, source) {
+  constructor(namespace, name, ast, source, config) {
     const internalLinks = this.#internalLinks;
     const externalLinks = this.#externalLinks;
     const sections = Object.create(null);
     djot.applyFilter(ast, () => {
       let titleSoFar = null; // used to collect strs from headings
       return {
-        doc: {
-          enter: (_) => {},
-          exit: (e) => {
-            const links_section = [];
-            if (internalLinks.size || externalLinks.size) {
-              links_section.push(
-                rawBlock`<footer>`,
-                rawBlock`<nav id="links">`,
-                {
-                  tag: "heading",
-                  level: 2,
-                  children: [str`this page contains links`],
-                },
-              );
-              if (internalLinks.size) {
-                links_section.push(
-                  rawBlock`<details open="">`,
-                  rawBlock`<summary>on this wiki</summary>`,
-                  listOfInternalLinks(internalLinks),
-                  rawBlock`</details>`,
-                );
-              } else {
-                /* do nothing */
-              }
-              if (externalLinks.size) {
-                links_section.push(
-                  rawBlock`<details open="">`,
-                  rawBlock`<summary>elsewhere on the Web</summary>`,
-                  {
+        definition_list: {
+          enter: (e) => {
+            const attributes = e.attributes ?? {};
+            if (
+              (attributes.class ?? "").split(/\s/gu).includes(
+                "timeline",
+              )
+            ) {
+              const years = new Map();
+              for (
+                const { children: [{ children: termChildren }, defn] }
+                  of e.children
+              ) {
+                const { label, year } = (() => {
+                  if (termChildren.length != 1) {
+                    return { label: termChildren, year: termChildren };
+                  } else {
+                    const str = termChildren[0];
+                    if (str.tag != "str") {
+                      return {
+                        label: termChildren,
+                        year: termChildren,
+                      };
+                    } else {
+                      const { text } = str;
+                      return {
+                        label: text,
+                        year:
+                          /^(-?[0-9X]{4})(?:-[0-9X]{2}(?:-[0-9X]{2})?)?$/u
+                            .exec(text)?.[1] ?? text,
+                      };
+                    }
+                  }
+                })();
+                const yearList = (() => {
+                  const result = {
                     tag: "bullet_list",
-                    tight: true,
-                    style: "*",
-                    children: Array.from(
-                      externalLinks,
-                      ([destination, text]) => ({
-                        tag: "list_item",
-                        children: [{
-                          tag: "para",
-                          children: [{
-                            tag: "link",
-                            attributes: { "data-realm": "external" },
-                            destination,
-                            children: text
-                              ? [
-                                rawInline`<cite>`,
-                                str`${text}`,
-                                rawInline`</cite>`,
-                              ]
-                              : [
-                                rawInline`<code>`,
-                                str`${destination}`,
-                                rawInline`</code>`,
-                              ],
-                          }],
-                        }],
-                      }),
-                    ),
-                  },
-                  rawBlock`</details>`,
-                );
-              } else {
-                /* do nothing */
+                    tight: false,
+                    style: "-",
+                    children: [],
+                  };
+                  if (years.has(year)) {
+                    const yearMap = years.get(year);
+                    if (yearMap.has(label)) {
+                      return yearMap.get(label);
+                    } else {
+                      yearMap.set(label, result);
+                      return result;
+                    }
+                  } else {
+                    years.set(year, new Map([[label, result]]));
+                    return result;
+                  }
+                })();
+                const misc = { tag: "list_item", children: [] };
+                for (const child of defn.children) {
+                  if (child.tag == "bullet_list") {
+                    yearList.children.push(...child.children);
+                  } else {
+                    misc.children.push(child);
+                  }
+                }
+                if (misc.children.length > 0) {
+                  yearList.children.unshift(misc);
+                } else {
+                  /* do nothing */
+                }
               }
-              links_section.push(
-                rawBlock`</nav>`,
-                rawBlock`</footer>`,
+              const sorted = [...years].sort(([a], [b]) =>
+                typeof a != "string" || isNaN(a) ||
+                  typeof b != "string" || isNaN(b) || +a == +b
+                  ? 0
+                  : 1 - 2 * (+a < b)
               );
+              sorted.forEach((pair) =>
+                pair[1] = [...pair[1]].sort(([a], [b]) =>
+                  1 - 2 * (a < b)
+                )
+              );
+              return {
+                tag: "div",
+                attributes,
+                children: sorted.flatMap(([year, yearDef]) => [
+                  rawBlock`<details open="">`,
+                  rawBlock`<summary>`,
+                  ...(Array.isArray(year) ? year : [str`${year}`]),
+                  rawBlock`</summary>`,
+                  rawBlock`<dl>`,
+                  ...yearDef.map(([label, list]) => ({
+                      tag: "div",
+                      children: [
+                        rawBlock`<dt>`,
+                        ...(Array.isArray(label) ? label : [str`${label}`]),
+                        rawBlock`</dt>`,
+                        rawBlock`<dd>`,
+                        list,
+                        rawBlock`</dd>`,
+                      ],
+                    })),
+                  rawBlock`</dl>`,
+                  rawBlock`</details>`,
+                ]),
+              };
             } else {
               /* do nothing */
             }
-            e.children.push(...links_section);
           },
+          exit: (_) => {},
         },
         hard_break: {
           enter: (_) => {
@@ -321,7 +364,7 @@ class GitWikiWebPage {
             e.attributes ??= {};
             const { attributes, reference, destination } = e;
             if (
-              /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*)?$/u
+              /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*(?:\/[A-Z][0-9A-Za-z]*)*)?$/u
                 .test(reference ?? "")
             ) {
               const [namespacePrefix, pageName] = splitReference(
@@ -335,11 +378,20 @@ class GitWikiWebPage {
               const resolvedReference = pageName == ""
                 ? `Namespace:${expandedNamespace}`
                 : `${expandedNamespace}:${pageName}`;
-              this.#internalLinks.add(resolvedReference);
               e.reference = resolvedReference;
               attributes["data-realm"] = "internal";
               attributes["data-pagename"] = pageName;
               attributes["data-namespace"] = expandedNamespace;
+              if (
+                resolvedReference.startsWith("Editor:") &&
+                (attributes.class ?? "").split(/\s/gu).includes("sig")
+              ) {
+                // This is a special internal link; do not record it.
+                /* do nothing */
+              } else {
+                // This is a non‐special internal link; record it.
+                internalLinks.add(resolvedReference);
+              }
             } else {
               attributes["data-realm"] = "external";
               const remote = destination ??
@@ -446,6 +498,20 @@ class GitWikiWebPage {
           },
           exit: (_) => {},
         },
+        symb: {
+          enter: (e) => {
+            const { alias } = e;
+            const codepoint = /^U\+([0-9A-Fa-f]+)$/u.exec(alias)?.[1];
+            if (codepoint) {
+              return str`${
+                String.fromCodePoint(parseInt(codepoint, 16))
+              }`;
+            } else {
+              const resolved = config.symbols?.[alias];
+              return resolved != null ? str`${resolved}` : e;
+            }
+          },
+        },
       };
     });
     Object.defineProperties(this, {
@@ -470,8 +536,43 @@ class GitWikiWebPage {
 }
 
 {
+  // Patches for Djot HTML renderer.
+  const { HTMLRenderer: { prototype: htmlRendererPrototype } } = djot;
+  const { inTags: upstreamInTags } = htmlRendererPrototype;
+  htmlRendererPrototype.inTags = function (
+    tag,
+    node,
+    newlines,
+    extraAttrs = undefined,
+  ) {
+    const attributes = node.attributes ?? NIL;
+    if ("as" in attributes) {
+      const newTag = attributes.as;
+      delete attributes.as;
+      return upstreamInTags.call(
+        this,
+        newTag,
+        node,
+        newlines,
+        extraAttrs,
+      );
+    } else {
+      return upstreamInTags.call(
+        this,
+        tag,
+        node,
+        newlines,
+        extraAttrs,
+      );
+    }
+  };
+}
+{
+  const config = await getRemoteContent("config.yaml").then((yaml) =>
+    parseYaml(yaml, { schema: JSON_SCHEMA })
+  );
   const ls = new Deno.Command("git", {
-    args: ["ls-tree", "-rz", "live"],
+    args: ["ls-tree", "-rz", "HEAD"],
     stdout: "piped",
     stderr: "piped",
   }).spawn();
@@ -535,6 +636,7 @@ class GitWikiWebPage {
                 `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
               );
             } else {
+              const reference = `${namespace}:${pageName}`;
               const page = new GitWikiWebPage(
                 namespace,
                 pageName,
@@ -543,8 +645,8 @@ class GitWikiWebPage {
                     console.warn(`Djot(${reference}): ${$.render()}`),
                 }),
                 source,
+                config,
               );
-              const reference = `${namespace}:${pageName}`;
               pages.set(reference, page);
               requiredButMissingPages.delete(reference);
             }
@@ -564,6 +666,7 @@ class GitWikiWebPage {
             console.warn(`Djot(${reference}): ${$.render()}`),
         }),
         source,
+        config,
       );
       pages.set(reference, page);
     }
@@ -615,7 +718,8 @@ class GitWikiWebPage {
         }
         const results = new Array(6);
         const seen = new Set();
-        let recency = 5;
+        const maxRecency = Math.max(config.max_recency | 0, 0);
+        let recency = maxRecency;
         let current;
         do {
           const show = new Deno.Command("git", {
@@ -623,7 +727,7 @@ class GitWikiWebPage {
               "show",
               "-s",
               "--format=%H%x00%cI%x00%cD",
-              recency ? `HEAD~${5 - recency}` : commit,
+              recency ? `HEAD~${maxRecency - recency}` : commit,
             ],
             stdout: "piped",
             stderr: "piped",
@@ -638,7 +742,9 @@ class GitWikiWebPage {
           ]).then(logErrorsAndCollectResults);
           const refs = [];
           current = hash;
-          for (const ref of (await diffReferences(current))) {
+          for (
+            const ref of (await diffReferences(current, !recency))
+          ) {
             if (seen.has(ref)) {
               /* do nothing */
             } else {
@@ -646,7 +752,7 @@ class GitWikiWebPage {
               seen.add(ref);
             }
           }
-          results[recency] = { dateTime, humanReadable, refs };
+          results[recency] = { dateTime, hash, humanReadable, refs };
         } while (recency-- > 0 && current && current != commit);
         return results;
       })(),
@@ -665,28 +771,157 @@ class GitWikiWebPage {
       ),
     ]).then(logErrorsAndCollectResults);
     promises.length = 0;
-    const redLinks = (() => {
-      const result = new Set();
-      for (const page of pages.values()) {
+    const { redLinks, subpages } = (() => {
+      const redLinks = new Set();
+      const subpages = new Map();
+      for (const [pageRef, page] of pages) {
+        let superRef = pageRef;
+        while (
+          (superRef = superRef.substring(0, superRef.indexOf("/")))
+        ) {
+          // Iterate over potential superpages and record them if they
+          // actually exist.
+          if (pages.has(superRef)) {
+            // There is a superpage for the current page; record it.
+            if (subpages.has(superRef)) {
+              // The identified superpage already has other subpages.
+              subpages.get(superRef).add(pageRef);
+            } else {
+              // The identified superpage does not already have other
+              // subpages.
+              subpages.set(superRef, new Set([pageRef]));
+            }
+            break;
+          } else {
+            // The superpage for the current page has not been found
+            // yet.
+            /* do nothing */
+          }
+        }
         for (const link of page.internalLinks()) {
+          // Iterate over the internal links of the current page and
+          // ensure they are all defined.
           if (pages.has(link)) {
+            // The link was defined.
             continue;
           } else {
-            result.add(link);
+            // The link was not defined; it is a redlink.
+            redLinks.add(link);
           }
         }
       }
-      return result;
+      return { redLinks, subpages };
     })();
-    for (
-      const [pageRef, { ast, namespace, sections, source }] of pages
-    ) {
+    for (const [pageRef, page] of pages) {
+      const { ast, sections, source } = page;
       const title = sections.main?.title ?? pageRef;
+      const internalLinks = new Set(page.internalLinks());
+      const externalLinks = new Map(page.externalLinks());
+      const subpageRefs = subpages.get(pageRef) ?? new Set();
       djot.applyFilter(ast, () => {
         let isNavigationPage = true;
         return {
           doc: {
             enter: (e) => {
+              const seeAlsoSection = [];
+              const linksSection = [];
+              if (subpageRefs.size) {
+                seeAlsoSection.push(
+                  rawBlock`<nav id="seealso">`,
+                  {
+                    tag: "heading",
+                    level: 2,
+                    children: [
+                      str`see also`,
+                    ],
+                  },
+                  rawBlock`<section id="subpages">`,
+                  {
+                    tag: "heading",
+                    level: 3,
+                    children: [
+                      str`subpages`,
+                    ],
+                  },
+                  listOfInternalLinks(subpageRefs),
+                  rawBlock`</section>`,
+                  rawBlock`</nav>`,
+                );
+              } else {
+                /* do nothing */
+              }
+              if (internalLinks.size || externalLinks.size) {
+                linksSection.push(
+                  rawBlock`<nav id="links">`,
+                  {
+                    tag: "heading",
+                    level: 2,
+                    children: [str`this page contains links`],
+                  },
+                );
+                if (internalLinks.size) {
+                  linksSection.push(
+                    rawBlock`<details open="">`,
+                    rawBlock`<summary>on this wiki</summary>`,
+                    listOfInternalLinks(internalLinks),
+                    rawBlock`</details>`,
+                  );
+                } else {
+                  /* do nothing */
+                }
+                if (externalLinks.size) {
+                  linksSection.push(
+                    rawBlock`<details open="">`,
+                    rawBlock`<summary>elsewhere on the Web</summary>`,
+                    {
+                      tag: "bullet_list",
+                      tight: true,
+                      style: "*",
+                      children: Array.from(
+                        externalLinks,
+                        ([destination, text]) => ({
+                          tag: "list_item",
+                          children: [{
+                            tag: "para",
+                            children: [{
+                              tag: "link",
+                              attributes: { "data-realm": "external" },
+                              destination,
+                              children: text
+                                ? [
+                                  rawInline`<cite>`,
+                                  str`${text}`,
+                                  rawInline`</cite>`,
+                                ]
+                                : [
+                                  rawInline`<code>`,
+                                  str`${destination}`,
+                                  rawInline`</code>`,
+                                ],
+                            }],
+                          }],
+                        }),
+                      ),
+                    },
+                    rawBlock`</details>`,
+                  );
+                } else {
+                  /* do nothing */
+                }
+                linksSection.push(
+                  rawBlock`</nav>`,
+                );
+              } else {
+                /* do nothing */
+              }
+              const childrenAndLinks = [
+                ...e.children,
+                ...seeAlsoSection,
+                rawBlock`<footer>`,
+                rawBlock`${"\uFFFF"}`, // footnote placeholder
+                ...linksSection,
+                rawBlock`</footer>`,
+              ];
               const { content, navigation } = (() => {
                 const navigation = [];
                 if (pageRef == "Special:RecentlyChanged") {
@@ -727,7 +962,7 @@ class GitWikiWebPage {
                   });
                 } else {
                   isNavigationPage = false;
-                  return { content: e.children, navigation };
+                  return { content: childrenAndLinks, navigation };
                 }
                 return {
                   content: [
@@ -743,7 +978,7 @@ class GitWikiWebPage {
                     rawBlock`<details id="navigation-about" open="">`,
                     rawBlock`<summary>about this listing</summary>`,
                     rawBlock`<article>`,
-                    ...e.children,
+                    ...childrenAndLinks,
                     rawBlock`</article>`,
                     rawBlock`</details>`,
                   ],
@@ -765,7 +1000,7 @@ class GitWikiWebPage {
           },
           heading: {
             enter: (e) => {
-              const attributes = e.attributes ?? Object.create(null);
+              const attributes = e.attributes ?? NIL;
               if (
                 isNavigationPage && e.level == 1 &&
                 attributes?.class == "main"
@@ -808,8 +1043,7 @@ class GitWikiWebPage {
                 }
                 if (children.length == 0) {
                   const section =
-                    pages.get(reference)?.sections?.main ??
-                      Object.create(null);
+                    pages.get(reference)?.sections?.main ?? NIL;
                   const { v } = attributes;
                   if (v == null) {
                     children.push(
@@ -861,8 +1095,25 @@ class GitWikiWebPage {
           },
         };
       });
+      const renderedAST = djot.renderAST(ast);
       const doc = getDOM(template);
-      const result = getDOM(`${djot.renderHTML(ast)}`);
+      const result = getDOM(djot.renderHTML(ast, {
+        overrides: {
+          raw_block: (node, context) => {
+            if (node.format == "html" && node.text == "\uFFFF") {
+              if (context.nextFootnoteIndex > 1) {
+                const result = context.renderNotes(ast.footnotes);
+                context.nextFootnoteIndex = 1;
+                return result;
+              } else {
+                return "";
+              }
+            } else {
+              return context.renderAstNodeDefault(node);
+            }
+          },
+        },
+      }));
       const headElement = domutils.findOne(
         (node) => node.name == "head",
         doc,
@@ -902,7 +1153,7 @@ class GitWikiWebPage {
           "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
         );
       } else {
-        for (const node of result) {
+        for (const node of [...result]) {
           domutils.prepend(contentElement, node);
         }
         domutils.removeElement(contentElement);
@@ -919,6 +1170,13 @@ class GitWikiWebPage {
           { createNew: true },
         ),
       );
+      promises.push(
+        Deno.writeTextFile(
+          `${DESTINATION}/${pageRef}/index.ast`,
+          renderedAST,
+          { createNew: true },
+        ),
+      );
       promises.push(
         Deno.writeTextFile(
           `${DESTINATION}/${pageRef}/source.djot`,
This page took 0.21152 seconds and 4 git commands to generate.