X-Git-Url: https://git.ladys.computer/GitWikiWeb/blobdiff_plain/5428dd106d223e93f02cff3bdb49b136fa040245..b84c8cdaebeed996e2173dac494ac2feaf38cf47:/build.js diff --git a/build.js b/build.js index 8eab7f4..fa0b836 100644 --- 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 @@ -37,7 +37,11 @@ import { emptyDir, ensureDir, -} from "https://deno.land/std@0.195.0/fs/mod.ts"; +} 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.2.3"; import { Parser } from "npm:htmlparser2@9.0.0"; import { DomHandler, Element, Text } from "npm:domhandler@5.0.3"; @@ -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,86 +222,13 @@ 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``, - ); - } else { - /* do nothing */ - } - e.children.push(...links_section); - }, - }, hard_break: { enter: (_) => { if (titleSoFar != null) { @@ -321,7 +255,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 +269,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 +389,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 +427,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 +527,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 +536,8 @@ class GitWikiWebPage { console.warn(`Djot(${reference}): ${$.render()}`), }), source, + config, ); - const reference = `${namespace}:${pageName}`; pages.set(reference, page); requiredButMissingPages.delete(reference); } @@ -564,6 +557,7 @@ class GitWikiWebPage { console.warn(`Djot(${reference}): ${$.render()}`), }), source, + config, ); pages.set(reference, page); } @@ -615,7 +609,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 +618,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 +633,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 +643,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 +662,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``, + ); + } else { + /* do nothing */ + } + if (internalLinks.size || externalLinks.size) { + linksSection.push( + rawBlock``, + ); + } else { + /* do nothing */ + } + const childrenAndLinks = [ + ...e.children, + ...seeAlsoSection, + rawBlock``, + ]; const { content, navigation } = (() => { const navigation = []; if (pageRef == "Special:RecentlyChanged") { @@ -707,14 +833,16 @@ class GitWikiWebPage { refs, } = result; yield* listOfInternalLinks(refs, (link) => ({ - tag: index ? "strong" : "span", + tag: index == 0 ? "span" : "strong", attributes: { "data-recency": `${index}` }, children: [ link, - str` `, - rawInline`()`, + ...(index == 0 ? [] : [ + str` `, + rawInline`()`, + ]), ], })).children; } else { @@ -725,7 +853,7 @@ class GitWikiWebPage { }); } else { isNavigationPage = false; - return { content: e.children, navigation }; + return { content: childrenAndLinks, navigation }; } return { content: [ @@ -741,7 +869,7 @@ class GitWikiWebPage { rawBlock``, ], @@ -763,7 +891,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" @@ -806,8 +934,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( @@ -859,8 +986,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, @@ -900,7 +1044,7 @@ class GitWikiWebPage { "GitWikiWeb: Template did not include a element.", ); } else { - for (const node of result) { + for (const node of [...result]) { domutils.prepend(contentElement, node); } domutils.removeElement(contentElement); @@ -917,6 +1061,13 @@ class GitWikiWebPage { { createNew: true }, ), ); + promises.push( + Deno.writeTextFile( + `${DESTINATION}/${pageRef}/index.ast`, + renderedAST, + { createNew: true }, + ), + ); promises.push( Deno.writeTextFile( `${DESTINATION}/${pageRef}/source.djot`,