// 🧸📔 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
// There is no content placeholder.
/* do nothing */
}
+ globalThis.bjørnTransformEntryHTML?.(document, metadata);
} else {
// This is not an XHTML template.
/* do nothing */
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.
*
* - "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 {
/**
.href("./feed.atom")``,
);
}
+ globalThis.bjørnTransformHead?.(head, metadata, type);
};
/**
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 =
/* do nothing */
}
}
+ globalThis.bjørnTransformMetadata?.(result, documentType);
return validateMetadata(result, documentType);
};
}
};
+{ // 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"?>
"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 {
/* do nothing */
}
}
+ globalThis.bjørnTransformFeedHTML?.(feedTemplate, feedMetadata);
writes.push(
Deno.writeTextFile(
"index.xhtml",
);
// Save the feed Atom file.
+ globalThis.bjørnTransformFeedAtom?.(document, feedMetadata);
writes.push(
Deno.writeTextFile(
"feed.atom",