]> Lady’s Gitweb - Beorn/blobdiff - build.js
Support hooks for transforming build script output
[Beorn] / build.js
index 743de647467f963c661e95eb092d75f18a6fd3bf..4c26518b85d4a2597126c582d50d305fd4ccd38c 100755 (executable)
--- a/build.js
+++ b/build.js
@@ -2,7 +2,7 @@
 // 🧸📔 Bjørn ∷ build.js
 // ====================================================================
 //
-// Copyright © 2022 Lady [@ Lady’s Computer].
+// Copyright © 2022‐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
@@ -54,6 +54,7 @@ const AWOL = "http://bblfish.net/work/atom-owl/2006-06-06/";
 const DC11 = "http://purl.org/dc/elements/1.1/";
 const FOAF = "http://xmlns.com/foaf/0.1/";
 const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const RDFS = "http://www.w3.org/2000/01/rdf-schema#";
 const SIOC = "http://rdfs.org/sioc/ns#";
 const XML = "http://www.w3.org/XML/1998/namespace";
 const XHTML = "http://www.w3.org/1999/xhtml";
@@ -201,55 +202,17 @@ const applyMetadata = (node, metadata) => {
     if (hasExpandedName(documentElement, XHTML, "html")) {
       // This is an XHTML template.
       const LMN = Lemon.bind({ document });
-      const head = Array.from(documentElement.childNodes).find(($) =>
-        hasExpandedName($, XHTML, "head")
-      ) ?? documentElement.insertBefore(
-        LMN.head``,
-        documentElement.childNodes.item(0),
-      );
-      const titleElement = Array.from(head.childNodes).find(($) =>
-        hasExpandedName($, XHTML, "title")
-      ) ?? head.appendChild(LMN.title``);
       const {
         id,
         title,
         author,
-        summary,
         contributor,
         published,
         content,
         rights,
         updated,
       } = metadata;
-      titleElement.textContent = Object(title) instanceof String
-        ? title
-        : Array.from(title ?? []).map(($) =>
-          $.textContent
-        ).join("");
-      for (const person of author) {
-        // Iterate over authors and add appropriate meta tags.
-        head.appendChild(
-          LMN.meta({ name: "author" })
-            .content(`${person.name ?? person.uri}`)``,
-        );
-      }
-      if (summary) {
-        // The entry has a summary.
-        head.appendChild(
-          LMN.meta({ name: "description" })
-            .content(
-              `${
-                Object(summary) instanceof String
-                  ? summary
-                  : Array.from(summary).map(($) => $.textContent).join(
-                    "",
-                  )
-              }`,
-            )``,
-        );
-      } else {
-        /* do nothing */
-      }
+      fillOutHead(document, metadata, "entry");
       const contentPlaceholder = document.getElementsByTagNameNS(
         XHTML,
         "bjørn-content",
@@ -381,6 +344,7 @@ const applyMetadata = (node, metadata) => {
         // There is no content placeholder.
         /* do nothing */
       }
+      globalThis.bjørnTransformEntryHTML?.(document, metadata);
     } else {
       // This is not an XHTML template.
       /* do nothing */
@@ -440,13 +404,19 @@ const applyMetadata = (node, metadata) => {
       // This entry does not have a publication date.
       /* do nothing */
     }
-    addContent(contentElement, "\n");
-    addContent(
-      contentElement.appendChild(
-        LMN.div.property(`${DC11}abstract`)``,
-      ),
-      summary,
-    );
+    if (summary) {
+      // This entry has a summary.
+      addContent(contentElement, "\n");
+      addContent(
+        contentElement.appendChild(
+          LMN.div.property(`${DC11}abstract`)``,
+        ),
+        summary,
+      );
+    } else {
+      // This entry does not have a summary.
+      /* do nothing */
+    }
     addContent(contentElement, "\n");
     addContent(node, "\n");
   } else {
@@ -455,6 +425,12 @@ const applyMetadata = (node, metadata) => {
     // Assume it is an Atom element of some sort and add the
     // the appropriate metadata as child elements.
     const { ownerDocument: document } = node;
+    const alternateLink = node.appendChild(
+      document.createElement("link"),
+    );
+    alternateLink.setAttribute("rel", "alternate");
+    alternateLink.setAttribute("type", "application/xhtml+xml");
+    alternateLink.setAttribute("href", metadata.id);
     for (const [property, values] of Object.entries(metadata)) {
       for (const value of Array.isArray(values) ? values : [values]) {
         const propertyNode = document.createElement(property);
@@ -555,7 +531,7 @@ const context = {
   },
   rights: { namespace: DC11, localName: "rights", type: "text" },
   // source is not supported at this time
-  // subtitle is not supported at this time
+  subtitle: { namespace: RDFS, localName: "label", type: "text" },
   summary: { namespace: DC11, localName: "abstract", type: "text" },
   title: { namespace: DC11, localName: "title", type: "text" },
   // updated is provided by the build script
@@ -582,6 +558,91 @@ const {
   };
 })();
 
+/**
+ * Fills out the `head` of the provided H·T·M·L document with the
+ * appropriate metadata.
+ */
+const fillOutHead = (document, metadata, type) => {
+  const { documentElement } = document;
+  const LMN = Lemon.bind({ document });
+  const head =
+    Array.from(documentElement.childNodes).find(($) =>
+      hasExpandedName($, XHTML, "head")
+    ) ?? documentElement.insertBefore(
+      LMN.head``,
+      documentElement.childNodes.item(0),
+    );
+  const titleElement =
+    Array.from(head.childNodes).find(($) =>
+      hasExpandedName($, XHTML, "title")
+    ) ?? head.appendChild(LMN.title``);
+  const {
+    title,
+    author,
+    subtitle,
+    summary,
+  } = metadata;
+  titleElement.textContent = Object(title) instanceof String
+    ? title
+    : Array.from(title ?? []).map(($) => $.textContent).join("");
+  for (const person of author) {
+    // Iterate over authors and add appropriate meta tags.
+    head.appendChild(
+      LMN.meta({
+        name: "author",
+        content: person.name ?? person.uri,
+      })``,
+    );
+  }
+  head.appendChild(
+    LMN.meta({ name: "generator", content: "🧸📔 Bjørn" })``,
+  );
+  if (type == "entry") {
+    // The provided document is an entry document.
+    if (summary) {
+      // The entry has a summary.
+      head.appendChild(
+        LMN.meta({
+          name: "description",
+          content: Object(summary) instanceof String
+            ? summary
+            : Array.from(summary).map(($) => $.textContent).join(""),
+        })``,
+      );
+    } else {
+      /* do nothing */
+    }
+    head.appendChild(
+      LMN.link
+        .rel("alternate")
+        .type("application/atom+xml")
+        .href("../../feed.atom")``,
+    );
+  } else {
+    // The provided document is not an entry document.
+    if (subtitle) {
+      // The entry has a subtitle.
+      head.appendChild(
+        LMN.meta({
+          name: "description",
+          content: Object(subtitle) instanceof String
+            ? summary
+            : Array.from(subtitle).map(($) => $.textContent).join(""),
+        })``,
+      );
+    } else {
+      /* do nothing */
+    }
+    head.appendChild(
+      LMN.link
+        .rel("alternate")
+        .type("application/atom+xml")
+        .href("./feed.atom")``,
+    );
+  }
+  globalThis.bjørnTransformHead?.(head, metadata, type);
+};
+
 /**
  * Returns the language of the provided node, or null if it has no
  * language.
@@ -807,6 +868,7 @@ const metadataFromDocument = (
       /* do nothing */
     }
   }
+  globalThis.bjørnTransformMetadata?.(result, documentType);
   return validateMetadata(result, documentType);
 };
 
@@ -847,27 +909,72 @@ const validateMetadata = (metadata, type) => {
   }
 };
 
+{ // Set up global variables for use in hooks.
+  //
+  // Bjørn is principally built to be run from the command line (as a
+  // shell script) rather than conforming to typical Ecmascript module
+  // patterns. However, it recognizes hooks through various
+  // specially‐named properties on `globalThis`. After defining these
+  // hooks, a script can use a *dynamic* `import("./path/to/build.js")`
+  // to run the Bjørn build steps.
+  //
+  // To make writing scripts which make use of these hooks easier,
+  // infrastructural dependencies and useful functions are provided on
+  // `globalThis` so that wrapping scripts don’t have to attempt to
+  // manage the dependencies themselves.
+  //
+  // Note that the `Lemon/window` polyfill will already have
+  // established some D·O·M‐related global properties by the time this
+  // runs, so they don’t need to be redeclared here.
+  globalThis.Lemon = Lemon;
+  globalThis.Bjørn = {
+    addContent,
+    getLanguage,
+    setLanguage,
+  };
+}
+
 await (async () => { // this is the run script
   const writes = [];
 
-  // Set up the Atom feed.
+  // Set up the feed metadata and Atom feed document.
+  const feedDocument = parser.parseFromString(
+    await Deno.readTextFile(`${basePath}/#feed.rdf`),
+    "application/xml",
+  );
+  const feedMetadata = metadataFromDocument(feedDocument);
+  const feedURI = new URL(feedMetadata.id);
   const document = parser.parseFromString(
     `<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom"></feed>`,
+<feed xmlns="http://www.w3.org/2005/Atom"><generator>🧸📔 Bjørn</generator><link rel="self" type="application/atom+xml" href="${new URL(
+      "./feed.atom",
+      feedURI,
+    )}"/></feed>`,
     "application/xml",
   );
   const { documentElement: feed } = document;
-  const feedMetadata = metadataFromDocument(
-    parser.parseFromString(
-      await Deno.readTextFile(`${basePath}/#feed.rdf`),
-      "application/xml",
-    ),
-  );
-  const feedURI = new URL(feedMetadata.id);
+  const feedLanguage = getLanguage(feedDocument);
+  if (feedLanguage) {
+    // The feed element has a language.
+    setLanguage(feed, feedLanguage);
+  } else {
+    // There is no language for the feed.
+    /* do nothing */
+  }
   applyMetadata(feed, feedMetadata);
 
   // Set up the index page.
   const feedTemplate = await documentFromTemplate("feed");
+  const { documentElement: feedTemplateRoot } = feedTemplate;
+  if (feedLanguage && !getLanguage(feedTemplateRoot)) {
+    // The root element of the template does not have an
+    // assigned language, but the feed does.
+    setLanguage(feedTemplateRoot, feedLanguage);
+  } else {
+    // Either the template root already has a language, or the
+    // entry doesn’t either.
+    /* do nothing */
+  }
   const feedEntries = feedTemplate.createDocumentFragment();
 
   // Process entries and save the resulting index files.
@@ -883,9 +990,11 @@ await (async () => { // this is the run script
       continue;
     } else {
       // This is a dated directory.
-      for await (
-        const { name: entryName, isDirectory } of Deno.readDir(
-          `${basePath}/${date}/`,
+      for (
+        const { name: entryName, isDirectory } of Array.from(
+          Deno.readDirSync(`${basePath}/${date}/`),
+        ).sort(({ name: a }, { name: b }) =>
+          a < b ? -1 : a > b ? 1 : 0
         )
       ) {
         // Iterate over each directory and process the ones which look
@@ -958,43 +1067,23 @@ await (async () => { // this is the run script
 
   // Apply the feed metadata to the feed template and save the
   // resulting index file.
-  const { documentElement: feedRoot } = feedTemplate;
-  if (hasExpandedName(feedRoot, XHTML, "html")) {
+  if (hasExpandedName(feedTemplateRoot, XHTML, "html")) {
     // This is an XHTML template.
     const LMN = Lemon.bind({ document: feedTemplate });
-    const head = Array.from(feedRoot.childNodes).find(($) =>
-      hasExpandedName($, XHTML, "head")
-    ) ?? feedRoot.insertBefore(
-      LMN.head``,
-      feedRoot.childNodes.item(0),
-    );
-    const titleElement = Array.from(head.childNodes).find(($) =>
-      hasExpandedName($, XHTML, "title")
-    ) ?? head.appendChild(LMN.title``);
     const {
       id,
       title,
-      author,
+      subtitle,
       rights,
       updated,
     } = feedMetadata;
-    titleElement.textContent = Object(title) instanceof String
-      ? title
-      : Array.from(title ?? []).map(($) =>
-        $.textContent
-      ).join("");
-    for (const person of author) {
-      // Iterate over authors and add appropriate meta tags.
-      head.appendChild(
-        LMN.meta({ name: "author" })
-          .content(`${person.name ?? person.uri}`)``,
-      );
-    }
+    fillOutHead(feedTemplate, feedMetadata, "feed");
     const contentPlaceholder = feedTemplate.getElementsByTagNameNS(
       XHTML,
       "bjørn-content",
     ).item(0);
     if (contentPlaceholder != null) {
+      // There is a content placeholder.
       const { parentNode: contentParent } = contentPlaceholder;
       const contentElement = contentParent.insertBefore(
         LMN.nav.about(`${id}`)`${"\n"}`,
@@ -1008,6 +1097,17 @@ await (async () => { // this is the run script
         title,
       );
       addContent(contentHeader, "\n");
+      if (subtitle) {
+        // The feed has a subtitle.
+        addContent(
+          contentHeader.appendChild(LMN.p.property(`${RDFS}label`)``),
+          subtitle,
+        );
+        addContent(contentHeader, "\n");
+      } else {
+        // The feed has no subtitle.
+        /* do nothing */
+      }
       addContent(contentElement, "\n");
       const entriesElement = contentElement.appendChild(
         LMN.ul.rel(`${AWOL}entry`)`${"\n"}`,
@@ -1018,6 +1118,7 @@ await (async () => { // this is the run script
         LMN.footer`${"\n\t"}`,
       );
       if (rights) {
+        // The feed has a rights statement.
         addContent(
           contentFooter.appendChild(
             LMN.div.property(`${DC11}rights`)``,
@@ -1025,6 +1126,9 @@ await (async () => { // this is the run script
           rights,
         );
         addContent(contentFooter, "\n\t");
+      } else {
+        // The feed has no rights statement.
+        /* do nothing */
       }
       contentFooter.appendChild(
         LMN.p.id("entry.updated")`Last updated: ${LMN.time.property(
@@ -1038,6 +1142,7 @@ await (async () => { // this is the run script
       /* do nothing */
     }
   }
+  globalThis.bjørnTransformFeedHTML?.(feedTemplate, feedMetadata);
   writes.push(
     Deno.writeTextFile(
       "index.xhtml",
@@ -1046,6 +1151,7 @@ await (async () => { // this is the run script
   );
 
   // Save the feed Atom file.
+  globalThis.bjørnTransformFeedAtom?.(document, feedMetadata);
   writes.push(
     Deno.writeTextFile(
       "feed.atom",
This page took 0.030558 seconds and 4 git commands to generate.