X-Git-Url: https://git.ladys.computer/Beorn/blobdiff_plain/091b363982b956dfc7b0395499a6f8dfd7050d7a..972b0f91082301611d0a5efeb2bb9219467a2692:/build.js diff --git a/build.js b/build.js index af75d5b..7299dc0 100755 --- 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( ` @@ -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",