// 🧸📔 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
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";
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",
// There is no content placeholder.
/* do nothing */
}
+ globalThis.bjørnTransformEntryHTML?.(document, metadata);
} else {
// This is not an XHTML template.
/* do nothing */
// 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 {
// 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);
},
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
};
})();
+/**
+ * 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.
/* 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,
+ 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.
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
// 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"}`,
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"}`,
LMN.footer`${"\n\t"}`,
);
if (rights) {
+ // The feed has a rights statement.
addContent(
contentFooter.appendChild(
LMN.div.property(`${DC11}rights`)``,
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(
/* 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",