]> Lady’s Gitweb - Beorn/blobdiff - build.js
Fix icon and logo to point to nodes
[Beorn] / build.js
index af75d5b06308e62687cf8d8f6631150eaf7f0766..7299dc03874219b5eb42fe6a712445391577a8da 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
@@ -344,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 */
@@ -466,7 +467,8 @@ const applyMetadata = (node, metadata) => {
           default: {
             // The property describes (potentially rich) text.
             if (value == null) {
-              // The property has no value; skip appending it to the node.
+              // The property has no value; skip appending it to the
+              // node.
               continue;
             } else if (Object(value) instanceof String) {
               // The property has a string value.
@@ -510,31 +512,63 @@ const basePath = `./${Deno.args[0] ?? ""}`;
  *
  * - "literal": This is a plaintext field.
  */
-const context = {
-  author: { namespace: DC11, localName: "creator", type: "person" },
+const context = Object.freeze({
+  author: Object.freeze({
+    namespace: DC11,
+    localName: "creator",
+    type: "person",
+  }),
   // category is not supported at this time
-  content: { namespace: SIOC, localName: "content", type: "text" },
-  contributor: {
+  content: Object.freeze({
+    namespace: SIOC,
+    localName: "content",
+    type: "text",
+  }),
+  contributor: Object.freeze({
     namespace: DC11,
     localName: "contributor",
     type: "person",
-  },
+  }),
   // generator is provided by the build script
-  icon: { namespace: AWOL, localName: "icon", type: "literal" },
+  icon: Object.freeze({
+    namespace: AWOL,
+    localName: "icon",
+    type: "node",
+  }),
   // link is provided by the build script
-  logo: { namespace: AWOL, localName: "logo", type: "literal" },
-  published: {
+  logo: Object.freeze({
+    namespace: AWOL,
+    localName: "logo",
+    type: "node",
+  }),
+  published: Object.freeze({
     namespace: DC11,
     localName: "date",
     type: "literal",
-  },
-  rights: { namespace: DC11, localName: "rights", type: "text" },
+  }),
+  rights: Object.freeze({
+    namespace: DC11,
+    localName: "rights",
+    type: "text",
+  }),
   // source 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" },
+  subtitle: Object.freeze({
+    namespace: RDFS,
+    localName: "label",
+    type: "text",
+  }),
+  summary: Object.freeze({
+    namespace: DC11,
+    localName: "abstract",
+    type: "text",
+  }),
+  title: Object.freeze({
+    namespace: DC11,
+    localName: "title",
+    type: "text",
+  }),
   // updated is provided by the build script
-};
+});
 
 const {
   /**
@@ -639,6 +673,7 @@ const fillOutHead = (document, metadata, type) => {
         .href("./feed.atom")``,
     );
   }
+  globalThis.bjørnTransformHead?.(head, metadata, type);
 };
 
 /**
@@ -767,6 +802,10 @@ const metadataFromDocument = (
       const { [name]: existing } = result;
       const content = (() => {
         switch (type) {
+          case "node": {
+            // The node points to another node.
+            return node.getAttributeNS(RDF, "resource");
+          }
           case "person": {
             // The node describes a person.
             const base =
@@ -866,6 +905,7 @@ const metadataFromDocument = (
       /* do nothing */
     }
   }
+  globalThis.bjørnTransformMetadata?.(result, documentType);
   return validateMetadata(result, documentType);
 };
 
@@ -906,16 +946,41 @@ 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,
+    context,
+    getLanguage,
+    setLanguage,
+  };
+}
+
 await (async () => { // this is the run script
   const writes = [];
 
   // Set up the feed metadata and Atom feed document.
-  const feedMetadata = metadataFromDocument(
-    parser.parseFromString(
-      await Deno.readTextFile(`${basePath}/#feed.rdf`),
-      "application/xml",
-    ),
+  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"?>
@@ -926,104 +991,113 @@ await (async () => { // this is the run script
     "application/xml",
   );
   const { documentElement: feed } = document;
+  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.
-  for await (
-    const { name: date, isDirectory } of Deno.readDir(
-      `${basePath}/`,
+  for (
+    const { name: date } of Array.from(
+      Deno.readDirSync(`${basePath}/`),
+    ).filter(({ name: date, isDirectory }) =>
+      // Exclude non‐dated directories.
+      isDirectory && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u.test(date)
+    ).sort(({ name: a }, { name: b }) =>
+      // Sort the directories.
+      a < b ? 1 : a > b ? -1 : 0
     )
   ) {
-    // Iterate over each directory and process the ones which are
-    // dates.
-    if (!isDirectory || !/[0-9]{4}-[0-9]{2}-[0-9]{2}/u.test(date)) {
-      // This isn’t a dated directory.
-      continue;
-    } else {
-      // This is a dated directory.
-      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
-        // like entries.
-        if (
-          !isDirectory ||
-          //deno-lint-ignore no-control-regex
-          /[\x00-\x20\x22#%/<>?\\^\x60{|}\x7F]/u.test(entryName)
-        ) {
-          // This isn’t an entry directory.
-          continue;
-        } else {
-          // Process the entry.
-          const entry = document.createElement("entry");
-          const entryPath =
-            `${basePath}/${date}/${entryName}/#entry.rdf`;
-          const entryDocument = parser.parseFromString(
-            await Deno.readTextFile(entryPath),
-            "application/xml",
-          );
-          const { documentElement: entryRoot } = entryDocument;
-          entryDocument.lastModified =
-            (await Deno.lstat(entryPath)).mtime;
-          if (!entryRoot.hasAttributeNS(RDF, "about")) {
-            // The entry doesn’t have an identifier; let’s give it one.
-            entryRoot.setAttributeNS(
-              RDF,
-              "about",
-              new URL(`./${date}/${entryName}/`, feedURI),
-            );
-          } else {
-            // The entry already has an identifier.
-            /* do nothing */
-          }
-          const entryMetadata = metadataFromDocument(entryDocument);
-          if (entryMetadata.author.length == 0) {
-            // The entry metadata did not supply an author.
-            entryMetadata.author = feedMetadata.author;
-          } else {
-            // The entry metadata supplied its own author.
-            /* do nothing */
-          }
-          const entryTemplate = await documentFromTemplate("entry");
-          const { documentElement: templateRoot } = entryTemplate;
-          const lang = getLanguage(entryRoot);
-          if (lang && !getLanguage(templateRoot)) {
-            // The root element of the template does not have an
-            // assigned language, but the entry does.
-            setLanguage(templateRoot, lang);
-          } else {
-            // Either the template root already has a language, or the
-            // entry doesn’t either.
-            /* do nothing */
-          }
-          writes.push(
-            Deno.writeTextFile(
-              `${basePath}/${date}/${entryName}/index.xhtml`,
-              serializer.serializeToString(
-                applyMetadata(entryTemplate, entryMetadata),
-              ) + "\n",
-            ),
-          );
-          applyMetadata(entry, entryMetadata);
-          applyMetadata(feedEntries, entryMetadata);
-          feed.appendChild(entry);
-        }
+    // Iterate over each dated directory and process its entries.
+    for (
+      const { name: entryName } of Array.from(
+        Deno.readDirSync(`${basePath}/${date}/`),
+      ).filter(({ name: entryName, isDirectory }) =>
+        // Exclude non‐entry directories.
+        isDirectory &&
+        //deno-lint-ignore no-control-regex
+        !/[\x00-\x20\x22#%/<>?\\^\x60{|}\x7F]/u.test(entryName)
+      ).sort(({ name: a }, { name: b }) =>
+        // Sort the directories.
+        a < b ? 1 : a > b ? -1 : 0
+      )
+    ) {
+      // Iterate over each entry directory and process its contents.
+      const entry = document.createElement("entry");
+      const entryPath = `${basePath}/${date}/${entryName}/#entry.rdf`;
+      const entryDocument = parser.parseFromString(
+        await Deno.readTextFile(entryPath),
+        "application/xml",
+      );
+      const { documentElement: entryRoot } = entryDocument;
+      entryDocument.lastModified = (await Deno.lstat(entryPath)).mtime;
+      if (!entryRoot.hasAttributeNS(RDF, "about")) {
+        // The entry doesn’t have an identifier; let’s give it one.
+        entryRoot.setAttributeNS(
+          RDF,
+          "about",
+          new URL(`./${date}/${entryName}/`, feedURI),
+        );
+      } else {
+        // The entry already has an identifier.
+        /* do nothing */
+      }
+      const entryMetadata = metadataFromDocument(entryDocument);
+      if (entryMetadata.author.length == 0) {
+        // The entry metadata did not supply an author.
+        entryMetadata.author = feedMetadata.author;
+      } else {
+        // The entry metadata supplied its own author.
+        /* do nothing */
       }
+      const entryTemplate = await documentFromTemplate("entry");
+      const { documentElement: templateRoot } = entryTemplate;
+      const lang = getLanguage(entryRoot);
+      if (lang && !getLanguage(templateRoot)) {
+        // The root element of the template does not have an
+        // assigned language, but the entry does.
+        setLanguage(templateRoot, lang);
+      } else {
+        // Either the template root already has a language, or the
+        // entry doesn’t either.
+        /* do nothing */
+      }
+      writes.push(
+        Deno.writeTextFile(
+          `${basePath}/${date}/${entryName}/index.xhtml`,
+          serializer.serializeToString(
+            applyMetadata(entryTemplate, entryMetadata),
+          ) + "\n",
+        ),
+      );
+      applyMetadata(entry, entryMetadata);
+      applyMetadata(feedEntries, entryMetadata);
+      feed.appendChild(entry);
     }
   }
 
   // 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 {
@@ -1098,6 +1172,7 @@ await (async () => { // this is the run script
       /* do nothing */
     }
   }
+  globalThis.bjørnTransformFeedHTML?.(feedTemplate, feedMetadata);
   writes.push(
     Deno.writeTextFile(
       "index.xhtml",
@@ -1106,6 +1181,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.03313 seconds and 4 git commands to generate.