--- /dev/null
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
--- /dev/null
+// 🐙🕸️ GitWikiWeb ∷ build.js
+// ====================================================================
+//
+// Copyright © 2023 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+//
+// --------------------------------------------------------------------
+//
+// A script for generating static wiki pages from a git repository.
+//
+// First, clone this repository to your machine in an accessible
+// location (for example, `/srv/git/GitWikiWeb`). A bare repository is
+// fine; customize the templates and stylesheets as you like. Then, use
+// this file as the post‐receive hook for your wiki as follows :—
+//
+// #!/usr/bin/env -S sh
+// export GITWIKIWEB=/srv/git/GitWikiWeb
+// git archive --remote=$GITWIKIWEB HEAD build.js \
+// | tar -xO \
+// | deno run -A - ~/public/wiki $GITWIKIWEB
+//
+// The directory `~/public/wiki` (or whatever you specify as the first
+// argument to `deno run -A -`) **will be deleted** and a new static
+// wiki will be generated in its place. This script is not very smart
+// (yet) and cannot selectively determine which pages will need
+// updating. It just wipes and regenerates the whole thing.
+//
+// This script will make a number of requests to `$GITWIKIWEB` to
+// download the latest templates, stylesheets, ⁊·c from this
+// repository. Consequently, it is best that you set it to a repository
+// you control and can ensure uptime for—ideally one local to the
+// computer hosting the wiki.
+
+import {
+ emptyDir,
+ ensureDir,
+} from "https://deno.land/std@0.195.0/fs/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";
+import * as domutils from "npm:domutils@3.1.0";
+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 READ_ONLY = {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+};
+
+const rawBlock = (strings, ...substitutions) => ({
+ tag: "raw_block",
+ format: "html",
+ text: String.raw(strings, substitutions),
+});
+const rawInline = (strings, ...substitutions) => ({
+ tag: "raw_inline",
+ format: "html",
+ text: String.raw(strings, substitutions),
+});
+const str = (strings, ...substitutions) => ({
+ tag: "str",
+ text: String.raw(strings, substitutions),
+});
+
+const getDOM = (source) => {
+ let result;
+ const handler = new DomHandler((error, dom) => {
+ if (error) {
+ throw new Error("GitWikiWeb: Failed to process DOM.", {
+ cause: error,
+ });
+ } else {
+ result = dom;
+ }
+ });
+ const parser = new Parser(handler);
+ parser.write(source);
+ parser.end();
+ return result;
+};
+
+const getRemoteContent = async (pathName) => {
+ const getArchive = new Deno.Command("git", {
+ args: ["archive", `--remote=${REMOTE}`, "HEAD", pathName],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ const untar = new Deno.Command("tar", {
+ args: ["-xO"],
+ stdin: "piped",
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ getArchive.stdout.pipeTo(untar.stdin);
+ const [
+ err1,
+ getArchiveStatus,
+ result,
+ err2,
+ untarStatus,
+ ] = await Promise.allSettled([
+ new Response(getArchive.stderr).text(),
+ getArchive.status,
+ new Response(untar.stdout).text(),
+ new Response(untar.stderr).text(),
+ untar.status,
+ ]).then(logErrorsAndCollectResults);
+ if (err1 || err2) {
+ console.error(err1 + err2);
+ } else {
+ /* do nothing */
+ }
+ if (!getArchiveStatus.success) {
+ throw new Error(
+ `GitWikiWeb: git archive returned nonzero exit code: ${getArchiveStatus.code}.`,
+ );
+ } else if (!untarStatus.success) {
+ throw new Error(
+ `GitWikiWeb: tar returned nonzero exit code: ${untarStatus.code}.`,
+ );
+ } else {
+ return result || "";
+ }
+};
+
+const logErrorsAndCollectResults = (results) =>
+ results.map(({ value, reason }) => {
+ if (reason) {
+ console.error(reason);
+ return undefined;
+ } else {
+ return value;
+ }
+ });
+
+const getReferenceFromPath = (path) =>
+ /Sources\/([A-Z][0-9A-Za-z]*\/[A-Z][0-9A-Za-z]*)\.djot$/u.exec(path)
+ ?.[1]?.replace?.("/", ":");
+
+const listOfInternalLinks = (references) => ({
+ tag: "bullet_list",
+ tight: true,
+ style: "*",
+ children: Array.from(
+ references,
+ (reference) => {
+ const [namespace, pageName] = splitReference(reference);
+ return {
+ tag: "list_item",
+ children: [{
+ tag: "para",
+ children: [{
+ tag: "link",
+ attributes: {
+ "data-realm": "internal",
+ "data-pagename": pageName,
+ "data-namespace": namespace,
+ },
+ reference,
+ children: [],
+ }],
+ }],
+ };
+ },
+ ),
+});
+
+function* references(paths) {
+ for (const path of paths) {
+ const reference = getReferenceFromPath(path);
+ if (reference) {
+ yield reference;
+ } else {
+ /* do nothing */
+ }
+ }
+}
+
+const splitReference = (reference) => {
+ const colonIndex = reference.indexOf(":");
+ return [
+ reference.substring(0, colonIndex),
+ reference.substring(colonIndex + 1),
+ ];
+};
+
+class GitWikiWebPage {
+ #internalLinks = new Set();
+ #externalLinks = new Map();
+
+ constructor(namespace, name, ast, source) {
+ 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>`,
+ rawBlock`<summary>on this wiki</summary>`,
+ listOfInternalLinks(internalLinks),
+ rawBlock`</details>`,
+ );
+ } else {
+ /* do nothing */
+ }
+ if (externalLinks.size) {
+ links_section.push(
+ rawBlock`<details>`,
+ 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 */
+ }
+ links_section.push(
+ rawBlock`</nav>`,
+ rawBlock`</footer>`,
+ );
+ } else {
+ /* do nothing */
+ }
+ e.children.push(...links_section);
+ },
+ },
+ hard_break: {
+ enter: (_) => {
+ if (titleSoFar != null) {
+ titleSoFar += " ";
+ } else {
+ /* do nothing */
+ }
+ },
+ exit: (_) => {},
+ },
+ heading: {
+ enter: (_) => {
+ titleSoFar = "";
+ },
+ exit: (e) => {
+ e.attributes ??= {};
+ const { attributes } = e;
+ attributes.title ??= titleSoFar;
+ titleSoFar = null;
+ },
+ },
+ link: {
+ enter: (e) => {
+ e.attributes ??= {};
+ const { attributes, reference, destination } = e;
+ if (
+ /^(?:[A-Z][0-9A-Za-z]*|[@#])?:(?:[A-Z][0-9A-Za-z]*)?$/u
+ .test(reference ?? "")
+ ) {
+ const [namespacePrefix, pageName] = splitReference(
+ reference,
+ );
+ const expandedNamespace = {
+ "": "Page",
+ "@": "Editor",
+ "#": "Category",
+ }[namespacePrefix] ?? namespacePrefix;
+ 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;
+ } else {
+ attributes["data-realm"] = "external";
+ const remote = destination ??
+ ast.references[reference]?.destination;
+ if (remote) {
+ externalLinks.set(remote, attributes?.title);
+ } else {
+ /* do nothing */
+ }
+ }
+ },
+ },
+ non_breaking_space: {
+ enter: (_) => {
+ if (titleSoFar != null) {
+ titleSoFar += "\xA0";
+ } else {
+ /* do nothing */
+ }
+ },
+ exit: (_) => {},
+ },
+ section: {
+ enter: (_) => {},
+ exit: (e) => {
+ e.attributes ??= {};
+ const { attributes, children } = e;
+ const heading = children.find(({ tag }) =>
+ tag == "heading"
+ );
+ const title = (() => {
+ if (heading?.attributes?.title) {
+ const result = heading.attributes.title;
+ delete heading.attributes.title;
+ return result;
+ } else {
+ return heading.level == 1
+ ? `${namespace}:${name}`
+ : "untitled section";
+ }
+ })();
+ const variantTitles = Object.create(null);
+ for (const attr in attributes) {
+ if (attr.startsWith("v-")) {
+ Object.defineProperty(
+ variantTitles,
+ attr.substring(2),
+ { ...READ_ONLY, value: attributes[attr] },
+ );
+ delete attributes[attr];
+ } else {
+ continue;
+ }
+ }
+ const definition = Object.create(null, {
+ title: { ...READ_ONLY, value: title },
+ variantTitles: {
+ ...READ_ONLY,
+ value: Object.preventExtensions(variantTitles),
+ },
+ });
+ if (heading.level == 1 && !("main" in sections)) {
+ attributes.id = "main";
+ heading.attributes ??= {};
+ heading.attributes.class = "main";
+ } else {
+ /* do nothing */
+ }
+ try {
+ Object.defineProperty(
+ sections,
+ attributes.id,
+ {
+ ...READ_ONLY,
+ writable: true,
+ value: Object.preventExtensions(definition),
+ },
+ );
+ } catch (cause) {
+ throw new Error(
+ `GitWikiWeb: A section with the provided @id already exists: ${attributes.id}`,
+ { cause },
+ );
+ }
+ },
+ },
+ soft_break: {
+ enter: (_) => {
+ if (titleSoFar != null) {
+ titleSoFar += " ";
+ } else {
+ /* do nothing */
+ }
+ },
+ exit: (_) => {},
+ },
+ str: {
+ enter: ({ text }) => {
+ if (titleSoFar != null) {
+ titleSoFar += text;
+ } else {
+ /* do nothing */
+ }
+ },
+ exit: (_) => {},
+ },
+ };
+ });
+ Object.defineProperties(this, {
+ ast: { ...READ_ONLY, value: ast },
+ namespace: { ...READ_ONLY, value: namespace },
+ name: { ...READ_ONLY, value: name },
+ sections: {
+ ...READ_ONLY,
+ value: Object.preventExtensions(sections),
+ },
+ source: { ...READ_ONLY, value: source },
+ });
+ }
+
+ *externalLinks() {
+ yield* this.#externalLinks;
+ }
+
+ *internalLinks() {
+ yield* this.#internalLinks;
+ }
+}
+
+{
+ const ls = new Deno.Command("git", {
+ args: ["ls-tree", "-rz", "live"],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ const [
+ objects,
+ lserr,
+ lsstatus,
+ ] = await Promise.allSettled([
+ new Response(ls.stdout).text().then((lsout) =>
+ lsout
+ .split("\0")
+ .slice(0, -1) // drop the last entry; it is empty
+ .map(($) => $.split(/\s+/g))
+ ),
+ new Response(ls.stderr).text(),
+ ls.status,
+ ]).then(logErrorsAndCollectResults);
+ if (lserr) {
+ console.error(lserr);
+ } else {
+ /* do nothing */
+ }
+ if (!lsstatus.success) {
+ throw new Error(
+ `GitWikiWeb: git ls-tree returned nonzero exit code: ${lsstatus.code}.`,
+ );
+ } else {
+ const requiredButMissingPages = new Map([
+ ["Special:FrontPage", "front page"],
+ ["Special:NotFound", "not found"],
+ ["Special:RecentlyChanged", "recently changed"],
+ ]);
+ const pages = new Map();
+ const promises = [emptyDir(DESTINATION)];
+ for (const object of objects) {
+ const hash = object[2];
+ const path = object[3];
+ const reference = getReferenceFromPath(path);
+ if (reference == null) {
+ continue;
+ } else {
+ const [namespace, pageName] = splitReference(reference);
+ const cat = new Deno.Command("git", {
+ args: ["cat-file", "blob", hash],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ const promise = Promise.allSettled([
+ new Response(cat.stdout).text(),
+ new Response(cat.stderr).text(),
+ cat.status,
+ ]).then(logErrorsAndCollectResults).then(
+ ([source, caterr, catstatus]) => {
+ if (caterr) {
+ console.error(caterr);
+ } else {
+ /* do nothing */
+ }
+ if (!catstatus.success) {
+ throw new Error(
+ `GitWikiWeb: git cat-file returned nonzero exit code: ${catstatus.code}.`,
+ );
+ } else {
+ const page = new GitWikiWebPage(
+ namespace,
+ pageName,
+ djot.parse(source, {
+ warn: ($) =>
+ console.warn(`Djot(${reference}): ${$.render()}`),
+ }),
+ source,
+ );
+ const reference = `${namespace}:${pageName}`;
+ pages.set(reference, page);
+ requiredButMissingPages.delete(reference);
+ }
+ },
+ );
+ promises.push(promise);
+ }
+ }
+ for (const [reference, defaultTitle] of requiredButMissingPages) {
+ const [namespace, pageName] = splitReference(reference);
+ const source = `# ${defaultTitle}\n`;
+ const page = new GitWikiWebPage(
+ namespace,
+ pageName,
+ djot.parse(source, {
+ warn: ($) =>
+ console.warn(`Djot(${reference}): ${$.render()}`),
+ }),
+ source,
+ );
+ pages.set(reference, page);
+ }
+ await Promise.allSettled(promises).then(
+ logErrorsAndCollectResults,
+ );
+ const [template, recentlyChanged] = await Promise.allSettled([
+ getRemoteContent("template.html"),
+ (async () => {
+ const dateParse = new Deno.Command("git", {
+ args: ["rev-parse", "--after=1 week ago"],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ const [maxAge] = await Promise.allSettled([
+ new Response(dateParse.stdout).text(),
+ new Response(dateParse.stderr).text(),
+ ]).then(logErrorsAndCollectResults);
+ let commit;
+ if (!maxAge) {
+ /* do nothing */
+ } else {
+ const revList = new Deno.Command("git", {
+ args: ["rev-list", maxAge, "--reverse", "HEAD"],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ [commit] = await Promise.allSettled([
+ new Response(revList.stdout).text().then((list) =>
+ list.split("\n")[0]
+ ),
+ new Response(revList.stderr).text(),
+ ]).then(logErrorsAndCollectResults);
+ }
+ if (!commit) {
+ const revList2 = new Deno.Command("git", {
+ args: ["rev-list", "--max-count=1", "HEAD^"],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ [commit] = await Promise.allSettled([
+ new Response(revList2.stdout).text().then((list) =>
+ list.trim()
+ ),
+ new Response(revList2.stderr).text(),
+ ]).then(logErrorsAndCollectResults);
+ } else {
+ /* do nothing */
+ }
+ const diff = new Deno.Command("git", {
+ args: [
+ "diff",
+ "-z",
+ "--name-only",
+ "--no-renames",
+ "--diff-filter=AM",
+ commit,
+ ],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+ const [diffNames] = await Promise.allSettled([
+ new Response(diff.stdout).text(),
+ new Response(diff.stderr).text(),
+ ]).then(logErrorsAndCollectResults);
+ return [...references(diffNames.split("\0"))];
+ })(),
+ ...Array.from(
+ pages.keys(),
+ (name) => ensureDir(`${DESTINATION}/${name}`),
+ ),
+ ["style.css"].map((dependency) =>
+ getRemoteContent(dependency).then((source) =>
+ Deno.writeTextFile(
+ `${DESTINATION}/${dependency}`,
+ source,
+ { createNew: true },
+ )
+ )
+ ),
+ ]).then(logErrorsAndCollectResults);
+ promises.length = 0;
+ const redLinks = (() => {
+ const result = new Set();
+ for (const page of pages.values()) {
+ for (const link of page.internalLinks()) {
+ if (pages.has(link)) {
+ continue;
+ } else {
+ result.add(link);
+ }
+ }
+ }
+ return result;
+ })();
+ for (
+ const [pageRef, { ast, namespace, sections, source }] of pages
+ ) {
+ const title = sections.main?.title ?? pageRef;
+ djot.applyFilter(ast, () => {
+ let isNavigationPage = true;
+ return {
+ doc: {
+ enter: (e) => {
+ const { content, navigation } = (() => {
+ const navigation = [];
+ if (pageRef == "Special:RecentlyChanged") {
+ navigation.push(
+ listOfInternalLinks(recentlyChanged),
+ );
+ } else {
+ isNavigationPage = false;
+ return { content: e.children, navigation };
+ }
+ return {
+ content: [
+ {
+ tag: "heading",
+ attributes: {
+ class: "main",
+ generated: "", // will be removed later
+ },
+ level: 1,
+ children: [str`${title}`],
+ },
+ rawBlock`<details open="">`,
+ rawBlock`<summary>about this listing</summary>`,
+ ...e.children,
+ rawBlock`</details>`,
+ ],
+ navigation: [
+ rawBlock`<nav id="navigation">`,
+ ...navigation,
+ rawBlock`</nav>`,
+ ],
+ };
+ })();
+ e.children = [
+ rawBlock`<article>`,
+ ...content,
+ ...navigation,
+ rawBlock`</article>`,
+ ];
+ },
+ exit: (_) => {},
+ },
+ heading: {
+ enter: (e) => {
+ const attributes = e.attributes ?? Object.create(null);
+ if (
+ isNavigationPage && e.level == 1 &&
+ attributes?.class == "main"
+ ) {
+ if ("generated" in attributes) {
+ delete attributes.generated;
+ } else {
+ return { stop: [] };
+ }
+ } else {
+ /* do nothing */
+ }
+ },
+ exit: (e) => {
+ if (e.level == 1 && e.attributes?.class == "main") {
+ return [
+ rawBlock`<header class="main">`,
+ e,
+ { tag: "verbatim", text: pageRef },
+ rawBlock`</header>`,
+ ];
+ } else {
+ /* do nothing */
+ }
+ },
+ },
+ link: {
+ enter: (_) => {},
+ exit: (e) => {
+ e.attributes ??= {};
+ const { attributes, children, reference } = e;
+ if (attributes["data-realm"] == "internal") {
+ delete e.reference;
+ if (redLinks.has(reference)) {
+ e.destination = `/Special:NotFound?path=/${reference}`;
+ attributes["data-notfound"] = "";
+ } else {
+ e.destination = `/${reference}`;
+ }
+ if (children.length == 0) {
+ const section =
+ pages.get(reference)?.sections?.main ??
+ Object.create(null);
+ const { v } = attributes;
+ if (v == null) {
+ children.push(
+ str`${section.title ?? reference}`,
+ );
+ } else {
+ delete attributes.v;
+ children.push(
+ str`${
+ section.variantTitles?.[v] ?? section.title ??
+ reference
+ }`,
+ );
+ }
+ }
+ } else {
+ if (children.length == 0 && "title" in attributes) {
+ children.push(
+ rawInline`<cite>`,
+ str`${attributes.title}`,
+ rawInline`</cite>`,
+ );
+ }
+ }
+ if (
+ (attributes.class ?? "").split(/\s/gu).includes("sig")
+ ) {
+ return {
+ tag: "span",
+ attributes: { class: "sig" },
+ children: [str`—${"\xA0"}`, e],
+ };
+ } else {
+ /* do nothing */
+ }
+ },
+ },
+ section: {
+ enter: (_) => {},
+ exit: (e) => {
+ if (e.children.length < 1) {
+ // The heading for this section was removed and it had
+ // no other children.
+ return [];
+ } else {
+ /* do nothing */
+ }
+ },
+ },
+ };
+ });
+ const doc = getDOM(template);
+ const result = getDOM(`${djot.renderHTML(ast)}`);
+ const headElement = domutils.findOne(
+ (node) => node.name == "head",
+ doc,
+ );
+ const titleElement = domutils.findOne(
+ (node) => node.name == "title",
+ headElement,
+ );
+ const contentElement = domutils.findOne(
+ (node) => node.name == "gitwikiweb-content",
+ doc,
+ );
+ if (headElement == null) {
+ throw new Error(
+ "GitWikiWeb: Template must explicitly include a <head> element.",
+ );
+ } else {
+ domutils.appendChild(
+ headElement,
+ new Element("link", {
+ rel: "source",
+ type: "text/x.djot",
+ href: `/${pageRef}/source.djot`,
+ }),
+ );
+ if (titleElement == null) {
+ domutils.prependChild(
+ headElement,
+ new Element("title", {}, [new Text(title)]),
+ );
+ } else {
+ domutils.prependChild(titleElement, new Text(`${title} | `));
+ }
+ }
+ if (contentElement == null) {
+ throw new Error(
+ "GitWikiWeb: Template did not include a <gitwikiweb-content> element.",
+ );
+ } else {
+ for (const node of result) {
+ domutils.prepend(contentElement, node);
+ }
+ domutils.removeElement(contentElement);
+ }
+ promises.push(
+ Deno.writeTextFile(
+ `${DESTINATION}/${pageRef}/index.html`,
+ domSerializer(doc, {
+ emptyAttrs: true,
+ encodeEntities: "utf8",
+ selfClosingTags: true,
+ xmlMode: false,
+ }),
+ { createNew: true },
+ ),
+ );
+ promises.push(
+ Deno.writeTextFile(
+ `${DESTINATION}/${pageRef}/source.djot`,
+ source,
+ { createNew: true },
+ ),
+ );
+ }
+ await Promise.allSettled(promises).then(
+ logErrorsAndCollectResults,
+ );
+ console.log(`GitWikiWeb: Wrote ${pages.size} page(s).`);
+ }
+}