1 #!/usr/bin
/env
-S deno run
--allow
-read
--allow
-write
3 // ====================================================================
5 // Copyright © 2022‐2023 Lady [@ Lady’s Computer].
7 // This Source Code Form is subject to the terms of the Mozilla Public
8 // License, v. 2.0. If a copy of the MPL was not distributed with this
9 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
11 // As the shebang at the top of this file indicates, it must be run via
12 // Deno with read and write permissions. You can get Deno from
13 // <https://deno.land/>.
15 // This file generates a minimal, Atom‐supporting blog from a handful
16 // of R·D·F∕X·M·L files and a couple of templates. It will silently
17 // overwrite the files it generates. This is usually what you want.
19 // ※ The actual run script is at the very end of this file. It is
20 // preceded by a number of helper function definitions, of which the
21 // most important is `applyMetadata` (which is used to generate the
22 // HTML and Atom elements from the processed metadata).
24 // ※ The list of supported metadata properties and their R·D·F
25 // representations is provided by `context`. You can add support for
26 // new metadata fields simply by adding them to the `context` and then
27 // handling them appropriately in `applyMetadata`.
29 // This script is a bit of a mess and you shouldn’t bother trying to
30 // understand the whole thing before you start hacking on it. Just find
31 // the parts relevant to whatever you want to change, and assume that
32 // things will work in sensible (if cumbersome) browser‐like ways.
34 // This import polyfills a (mediocre but sufficient) D·O·M environment
35 // onto the global object.
36 import "https://git.ladys.computer/Lemon/blob_plain/0.2.2:/window/mod.js";
38 // Much of the H·T·M·L generation in this script uses the 🍋🏷 Lemon
39 // library for convenience (<https://git.ladys.computer/Lemon>).
41 // Frequently this will be bound to a different document than the
42 // global one and called as `LMN`.
43 import Lemon
from "https://git.ladys.computer/Lemon/blob_plain/0.2.2:/mod.js";
45 // Markdown processing uses rusty_markdown, which uses Rust’s
46 // pulldown-cmark behind the scenes via WebAssembly.
48 html as markdownTokensToHTML
,
49 tokens as markdownTokens
,
50 } from "https://deno.land/x/rusty_markdown@v0.4.1/mod.ts";
52 // Various namespaces.
53 const AWOL
= "http://bblfish.net/work/atom-owl/2006-06-06/";
54 const DC11
= "http://purl.org/dc/elements/1.1/";
55 const FOAF
= "http://xmlns.com/foaf/0.1/";
56 const RDF
= "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
57 const RDFS
= "http://www.w3.org/2000/01/rdf-schema#";
58 const SIOC
= "http://rdfs.org/sioc/ns#";
59 const XML
= "http://www.w3.org/XML/1998/namespace";
60 const XHTML
= "http://www.w3.org/1999/xhtml";
63 * Adds the provided content to the provided element.
65 * Content may be either nullish (in which case nothing is added), a
66 * string (object or literal), a NodeList, or an array of zero or more
67 * of these values. Nodes need not belong to the same document as the
68 * provided element; they will be imported. During this process,
69 * special care is taken to ensure that the resulting content is
70 * correctly language‐tagged for its new context.
72 * ☡ If the provided element is not attached to anything, then it won’t
73 * be possible to walk its parent nodes in search of language
74 * information. Generally, it is best to attach elements to a document
75 * BEFORE calling this function.
77 const addContent
= (element
, content
) => {
78 const { ownerDocument
: document
} = element
;
79 const LMN
= Lemon
.bind({ document
});
80 if (content
== null) {
81 // The provided content is nullish.
83 } else if (Array
.isArray(content
)) {
84 // The provided content is an array.
85 content
.forEach(addContent
.bind(null, element
));
86 } else if (Object(content
) instanceof String
) {
87 // The provided content is a string (object or literal).
88 const { lang
} = content
;
89 if (lang
&& lang
!= getLanguage(element
)) {
90 const newChild
= element
.appendChild(LMN
.span
`${content}`);
91 setLanguage(newChild
, lang
);
93 element
.appendChild(document
.createTextNode(`${content}`));
96 // Assume the provided content is a NodeList.
97 if (element
.hasAttribute("property")) {
98 // The provided element has an R·D·F∕A property; note that its
99 // datatype is `XMLLiteral`.
100 element
.setAttribute("datatype", "XMLLiteral");
102 // The provided element does not have an R·D·F∕A property.
105 for (const child
of Array
.from(content
)) {
106 // Iterate over the nodes in the provided NodeList and handle
107 // them appropriately.
108 const lang
= getLanguage(child
);
109 const newChild
= (() => {
110 const imported
= document
.importNode(child
, true);
111 if (lang
&& lang
!= getLanguage(element
)) {
112 // The imported node’s language would change if it were just
113 // imported directly into the provided element.
114 if (imported
.nodeType
== 1) {
115 // The imported node is an element node.
116 setLanguage(imported
, lang
);
118 } else if (imported
.nodeType
<= 4) {
119 // The imported node is a text node.
120 const result
= LMN
.span
`${imported}`;
121 setLanguage(result
, lang
);
124 // The imported node is not an element or text.
128 // The imported node’s language will not change if imported
129 // directly into the provided element.
133 element
.appendChild(newChild
);
140 * Adds HTML for the provided people to the provided element, tagging
141 * them with the provided property.
143 * ☡ As with `addContent`, it is best to attach elements to a document
144 * PRIOR to providing them to this function, for language‐detection
147 const addPeople
= (element
, people
, property
) => {
148 const { ownerDocument
: document
} = element
;
149 const LMN
= Lemon
.bind({ document
});
150 const { length
} = people
;
151 for (const [index
, { uri
, name
}] of people
.entries()) {
152 const personElement
= element
.appendChild(
154 ? LMN
.a
.rel(`${property}`).href(`${uri}`)``
155 : LMN
.span
.rel(`${property}`)``,
158 // The current person has no name; provide its `uri`.
159 personElement
.appendChild(LMN
.code
`${uri}`);
161 // The current person has a name.
163 personElement
.appendChild(
164 LMN
.span
.property(`${FOAF}name`)``,
169 if (index
< length
- 2) {
170 // The current person is two or greater from the end.
171 addContent(element
, ", ");
172 } else if (index
< length
- 1) {
173 // The current person is one from the end.
174 addContent(element
, " & ");
176 // The current person is the last.
184 * Applies the provided metadata to the provided node by creating and
185 * inserting the appropriate elements, then returns the provided node.
187 * If the provided node is a document, it is assumed to be an entry
188 * template, and full entry H·T·M·L is generated. If it is a document
189 * fragment, it is assumed to be a document fragment collecting entries
190 * for the H·T·M·L feed index page, and entry H·T·M·L links are
191 * generated. Otherwise, the provided node is assumed to be an Atom
192 * element, and Atom metadata elements are generated.
194 const applyMetadata
= (node
, metadata
) => {
195 if (node
.nodeType
== 9) {
196 // The provided node is a document.
198 // Assume it is an entry template document and insert the full
199 // entry H·T·M·L accordingly.
200 const document
= node
;
201 const { documentElement
} = document
;
202 if (hasExpandedName(documentElement
, XHTML
, "html")) {
203 // This is an XHTML template.
204 const LMN
= Lemon
.bind({ document
});
215 fillOutHead(document
, metadata
, "entry");
216 const contentPlaceholder
= document
.getElementsByTagNameNS(
220 if (contentPlaceholder
!= null) {
221 // The content placeholder exists; replace it with the content
223 const { parentNode
: contentParent
} = contentPlaceholder
;
224 const contentElement
= contentParent
.insertBefore(
225 LMN
.article
.about(`${id}`)`${"\n"}`,
229 // Handle the entry content header.
230 contentElement
.appendChild(
231 document
.createComment(" BEGIN ENTRY HEADER "),
233 addContent(contentElement
, "\n");
234 const contentHeader
= contentElement
.appendChild(
235 LMN
.header
.id("entry.header")`${"\n\t"}`,
238 contentHeader
.appendChild(
239 LMN
.h1
.id("entry.title").property(`${DC11}title`)``,
243 if (author
.length
> 0) {
244 // This entry has authors.
245 addContent(contentHeader
, "\n\t");
247 contentHeader
.appendChild(
248 LMN
.p
.id("entry.author")``,
254 // This entry does not have authors.
257 if (contributor
.length
> 0) {
258 // This entry has contributors.
259 addContent(contentHeader
, "\n\t");
262 contentHeader
.appendChild(
265 )`With contributions from `,
268 `${DC11}contributor`,
273 // This entry does not have contributors.
277 // This entry has a publication date.
278 addContent(contentHeader
, "\n\t");
279 contentHeader
.appendChild(
280 LMN
.p
.id("entry.published")`Published: ${LMN.time.property(
285 // This entry does not have a publication date.
288 addContent(contentHeader
, "\n");
289 addContent(contentElement
, "\n");
290 contentElement
.appendChild(
291 document
.createComment(" END ENTRY HEADER "),
293 addContent(contentElement
, "\n");
295 // Handle the entry content.
296 contentElement
.appendChild(
297 document
.createComment(" BEGIN ENTRY CONTENT "),
299 addContent(contentElement
, "\n");
301 contentElement
.appendChild(
302 LMN
.div
.id("entry.content").property(`${SIOC}content`)``,
306 addContent(contentElement
, "\n");
307 contentElement
.appendChild(
308 document
.createComment(" END ENTRY CONTENT "),
310 addContent(contentElement
, "\n");
312 // Handle the entry content footer.
313 contentElement
.appendChild(
314 document
.createComment(" BEGIN ENTRY FOOTER "),
316 addContent(contentElement
, "\n");
317 const contentFooter
= contentElement
.appendChild(
318 LMN
.footer
.id("entry.footer")`${"\n\t"}`,
322 contentFooter
.appendChild(
323 LMN
.div
.id("entry.rights").property(`${DC11}rights`)``,
327 addContent(contentFooter
, "\n\t");
329 contentFooter
.appendChild(
330 LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property(
334 addContent(contentFooter
, "\n");
335 addContent(contentElement
, "\n");
336 contentElement
.appendChild(
337 document
.createComment(" END ENTRY FOOTER "),
339 addContent(contentElement
, "\n");
341 // Remove the placeholder.
342 contentParent
.removeChild(contentPlaceholder
);
344 // There is no content placeholder.
347 globalThis
.bj
ørnTransformEntryHTML
?.(document
, metadata
);
349 // This is not an XHTML template.
352 } else if (node
.nodeType
== 11) {
353 // The provided node is a document fragment.
355 // Assume it is collecting H·T·M·L feed entry links and insert a
356 // new one for the provided metadata.
357 const { ownerDocument
: document
} = node
;
358 const LMN
= Lemon
.bind({ document
});
366 // The content placeholder exists; replace it with the content
369 document
.createComment(` <${id}> `),
371 addContent(node
, "\n");
372 const contentElement
= node
.appendChild(
373 LMN
.li
.resource(`${id}`)`${"\n"}`,
376 contentElement
.appendChild(
377 LMN
.a
.href(`${id}`)``,
379 LMN
.h3
.property(`${DC11}title`)``,
383 if (author
.length
> 0) {
384 // This entry has authors.
385 addContent(contentElement
, "\n");
387 contentElement
.appendChild(
394 // This entry does not have authors.
398 // This entry has a publication date.
399 addContent(contentElement
, "\n");
400 contentElement
.appendChild(
401 LMN
.time
.property(`${DC11}date`)`${published}`,
404 // This entry does not have a publication date.
408 // This entry has a summary.
409 addContent(contentElement
, "\n");
411 contentElement
.appendChild(
412 LMN
.div
.property(`${DC11}abstract`)``,
417 // This entry does not have a summary.
420 addContent(contentElement
, "\n");
421 addContent(node
, "\n");
423 // The provided node is not a document or document fragment.
425 // Assume it is an Atom element of some sort and add the
426 // the appropriate metadata as child elements.
427 const { ownerDocument
: document
} = node
;
428 const alternateLink
= node
.appendChild(
429 document
.createElement("link"),
431 alternateLink
.setAttribute("rel", "alternate");
432 alternateLink
.setAttribute("type", "application/xhtml+xml");
433 alternateLink
.setAttribute("href", metadata
.id
);
434 for (const [property
, values
] of Object
.entries(metadata
)) {
435 for (const value
of Array
.isArray(values
) ? values
: [values
]) {
436 const propertyNode
= document
.createElement(property
);
437 switch (context
[property
]?.type
) {
439 // The property describes a person.
440 const { name
, uri
} = value
;
442 // The person has a U·R·I.
443 const subnode
= document
.createElement("uri");
444 subnode
.textContent
= uri
;
445 propertyNode
.appendChild(subnode
);
447 // The person does not have a U·R·I.
451 // The person has a name.
452 const subnode
= document
.createElement("name");
453 subnode
.textContent
= name
;
454 propertyNode
.appendChild(subnode
);
456 // The person does not have a name.
459 if (propertyNode
.childNodes
.length
== 0) {
460 // Neither a U·R·I nor a name was added; skip adding this
468 // The property describes (potentially rich) text.
470 // The property has no value; skip appending it to the
473 } else if (Object(value
) instanceof String
) {
474 // The property has a string value.
475 propertyNode
.textContent
= value
;
478 // The property value is a list of nodes.
479 propertyNode
.setAttribute("type", "xhtml");
480 const div
= document
.createElementNS(XHTML
, "div");
481 for (const child
of Array
.from(value
)) {
482 div
.appendChild(document
.importNode(child
, true));
484 propertyNode
.appendChild(div
);
489 node
.appendChild(propertyNode
);
497 * The base path from which to pull files and generate resulting
500 const basePath
= `./${Deno.args[0] ?? ""}`;
503 * Mappings from Atom concepts to R·D·F∕X·M·L ones.
505 * `namespace` and `localName` give the R·D·F representation for the
506 * concept. Three `type`s are supported :—
508 * - "person": Has a `name` (`foaf:name`) and an `iri` (`rdf:about`).
509 * Emails are NOT supported.
511 * - "text": Can be string, markdown, or X·M·L content.
513 * - "literal": This is a plaintext field.
515 const context
= Object
.freeze({
516 author
: Object
.freeze({
518 localName
: "creator",
521 // category is not supported at this time
522 content
: Object
.freeze({
524 localName
: "content",
527 contributor
: Object
.freeze({
529 localName
: "contributor",
532 // generator is provided by the build script
533 icon
: Object
.freeze({
538 // link is provided by the build script
539 logo
: Object
.freeze({
544 published
: Object
.freeze({
549 rights
: Object
.freeze({
554 // source is not supported at this time
555 subtitle
: Object
.freeze({
560 summary
: Object
.freeze({
562 localName
: "abstract",
565 title
: Object
.freeze({
570 // updated is provided by the build script
575 * Returns a new document created from the source code of the named
578 documentFromTemplate
,
580 const cache
= Object
.create(null);
582 documentFromTemplate
: async (name
) =>
583 parser
.parseFromString(
584 name
in cache
? cache
[name
] : (
585 cache
[name
] = await Deno
.readTextFile(
586 `${basePath}/index#${name}.xhtml`,
595 * Fills out the `head` of the provided H·T·M·L document with the
596 * appropriate metadata.
598 const fillOutHead
= (document
, metadata
, type
) => {
599 const { documentElement
} = document
;
600 const LMN
= Lemon
.bind({ document
});
602 Array
.from(documentElement
.childNodes
).find(($) =>
603 hasExpandedName($, XHTML
, "head")
604 ) ?? documentElement
.insertBefore(
606 documentElement
.childNodes
.item(0),
609 Array
.from(head
.childNodes
).find(($) =>
610 hasExpandedName($, XHTML
, "title")
611 ) ?? head
.appendChild(LMN
.title
``);
618 titleElement
.textContent
= Object(title
) instanceof String
620 : Array
.from(title
?? []).map(($) => $.textContent
).join("");
621 for (const person
of author
) {
622 // Iterate over authors and add appropriate meta tags.
626 content
: person
.name
?? person
.uri
,
631 LMN
.meta({ name
: "generator", content
: "🧸📔 Bjørn" })``,
633 if (type
== "entry") {
634 // The provided document is an entry document.
636 // The entry has a summary.
640 content
: Object(summary
) instanceof String
642 : Array
.from(summary
).map(($) => $.textContent
).join(""),
651 .type("application/atom+xml")
652 .href("../../feed.atom")``,
655 // The provided document is not an entry document.
657 // The entry has a subtitle.
661 content
: Object(subtitle
) instanceof String
663 : Array
.from(subtitle
).map(($) => $.textContent
).join(""),
672 .type("application/atom+xml")
673 .href("./feed.atom")``,
676 globalThis
.bj
ørnTransformHead
?.(head
, metadata
, type
);
680 * Returns the language of the provided node, or null if it has no
683 * ※ This function returns null regardless of whether a node has no
684 * language implicitly (because no parent element had a language set)
685 * or explicitly (because the value of the @xml:lang attribute was "").
687 * ※ If no language is set, the language of the node is ambiguous.
688 * Because this typically occurs when a node is expected to inherit its
689 * language from some parent context, this lack of language information
690 * SHOULD NOT be preserved when inserting the node into a document
691 * where language information is present (instead allowing the node to
692 * inherit language information from the node it is being inserted
693 * into). There are explicit language codes which may be used if this
694 * behaviour is undesirable: If you want to signal that a node does not
695 * contain linguistic content, use the language code `zxx`. If you want
696 * to signal that a node’s language is undetermined in a way which will
697 * never inherit from a parent node, use the language code `und`.
699 const getLanguage
= (node
) => {
700 const { nodeType
} = node
;
702 // The provided node is an element.
704 node
.namespaceURI
== XHTML
&& node
.hasAttribute("lang") &&
705 node
.getAttribute("lang") ||
706 node
.hasAttributeNS(XML
, "lang") &&
707 node
.getAttributeNS(XML
, "lang");
709 // The element has a recognized language attribute set to a
712 } else if (ownLanguage
=== "") {
713 // The element explicitly has no language.
716 // The element has no language attribute, but may inherit a
717 // language from its parent.
718 const { parentNode
} = node
;
719 if (parentNode
!= null && parentNode
.nodeType
!= 9) {
720 // The provided node has a nondocument parent; get the language
722 return getLanguage(parentNode
);
724 // The provided node has no parent and consequently no language.
728 } else if (nodeType
== 9) {
729 // The provided node is a document.
730 return getLanguage(node
.documentElement
);
731 } else if (nodeType
== 11) {
732 // The provided node is a document fragment.
733 return getLanguage(node
.ownerDocument
.documentElement
);
735 // The provided node may inherit a language from a parent node.
736 const { parentNode
} = node
;
737 if (parentNode
!= null) {
738 // The provided node has a parent; get the language from there.
739 return getLanguage(parentNode
);
741 // The provided node has no parent and consequently no language.
748 * Returns whether the provided node has the provided namespace and
751 const hasExpandedName
= (node
, namespace, localName
) =>
752 node
.namespaceURI
== namespace && node
.localName
== localName
;
755 * Processes an RDF document and returns an object of Atom metadata.
757 * See `context` for the metadata properties supported.
759 const metadataFromDocument
= (
760 { documentElement
: root
, lastModified
},
762 const contextEntries
= [...Object
.entries(context
)];
763 const documentType
= hasExpandedName(root
, AWOL
, "Feed")
766 const result
= Object
.assign(Object
.create(null), {
767 id
: root
.getAttributeNS(RDF
, "about"),
768 updated
: documentType
== "feed" || lastModified
== null
769 ? new Date().toISOString()
770 : lastModified
.toISOString(),
775 ...(documentType
== "feed"
776 ? { // additional feed properties
781 : { // additional entry properties
789 ...Array
.from(root
.attributes
),
790 ...Array
.from(root
.childNodes
),
793 // Iterate over all child nodes and attributes, finding ones which
794 // correspond to Atom properties and assigning the appropriate
796 const [name
, { type
}] = contextEntries
.find(
798 hasExpandedName(node
, value
.namespace, value
.localName
),
800 if (name
!= null && name
in result
) {
801 // The current node corresponds with an Atom property.
802 const { [name
]: existing
} = result
;
803 const content
= (() => {
806 // The node points to another node.
807 return node
.getAttributeNS(RDF
, "resource");
810 // The node describes a person.
812 node
.getAttributeNS
?.(RDF
, "parseType") == "Resource"
814 : Array
.from(node
.childNodes
).find(($) =>
819 uri
: base
.getAttributeNS
?.(RDF
, "about") || null,
823 ...Array
.from(base
.attributes
),
824 ...Array
.from(base
.childNodes
),
827 // Process child nodes and attributes for the current
828 // person, looking for name metadata.
829 if (hasExpandedName(subnode
, FOAF
, "name")) {
830 // This is a name node.
831 if (person
.name
== null) {
832 // No name has been set yet.
833 person
.name
= subnode
.textContent
;
835 // A name has already been set.
837 `Duplicate name found for person${
838 person.id != null ? ` <${person.id}
>` : ""
839 } while processing <${result.id}>.`,
843 // This is not a name node
850 // The node describes (potentially rich) textual content.
852 // ☡ Don’t return an Array here or it will look like a list
853 // of multiple values. Return the NodeList of child nodes
855 const parseType
= node
.getAttributeNS
?.(RDF
, "parseType");
856 if (parseType
== "Markdown") {
857 // This is an element with Markdown content (which
858 // hopefully can be converted into X·M·L).
859 return parser
.parseFromString(
860 `<福 xmlns="${XHTML}" lang="${
861 getLanguage(node) ?? ""
863 markdownTokensToHTML(
864 markdownTokens(node.textContent),
868 ).documentElement
.childNodes
;
869 } else if (parseType
== "Literal") {
870 // This is an element with literal X·M·L contents.
871 return node
.childNodes
;
873 // This is an element without literal X·M·L contents.
878 // The node describes something in plaintext.
879 const text
= new String(node
.textContent
);
880 const lang
= getLanguage(node
);
890 if (existing
== null) {
891 // The property takes at most one value, but none has been set.
892 result
[name
] = content
;
893 } else if (Array
.isArray(existing
)) {
894 // The property takes multiple values.
895 existing
.push(content
);
897 // The property takes at most one value, and one has already
900 `Duplicate content found for ${name} while processing <${result.id}>.`,
904 // The current node does not correspond with an Atom property.
908 globalThis
.bj
ørnTransformMetadata
?.(result
, documentType
);
909 return validateMetadata(result
, documentType
);
912 /** The DOMParser used by this script. */
913 const parser
= new DOMParser();
915 /** The XMLSerializer used by this script. */
916 const serializer
= new XMLSerializer();
919 * Sets the @xml:lang attribute of the provided element, and if it is
920 * an H·T·M·L element also sets the @lang.
922 const setLanguage
= (element
, lang
) => {
923 element
.setAttributeNS(XML
, "xml:lang", lang
?? "");
924 if (element
.namespaceURI
== XHTML
) {
925 element
.setAttribute("lang", lang
?? "");
932 * Throws if the provided metadata does not conform to expectations for
933 * the provided type, and otherwise returns it.
935 const validateMetadata
= (metadata
, type
) => {
936 if (metadata
.id
== null) {
937 throw new TypeError("Missing id.");
938 } else if (metadata
.title
== null) {
939 throw new TypeError(`Missing title for item <${metadata.id}>.`);
940 } else if (type
== "feed" && metadata
.author
== null) {
941 throw new TypeError(`Missing author for feed <${metadata.id}>.`);
942 } else if (type
== "entry" && metadata
.content
== null) {
943 throw new TypeError(`Missing content for entry <${metadata.id}>.`);
949 { // Set up global variables for use in hooks.
951 // Bjørn is principally built to be run from the command line (as a
952 // shell script) rather than conforming to typical Ecmascript module
953 // patterns. However, it recognizes hooks through various
954 // specially‐named properties on `globalThis`. After defining these
955 // hooks, a script can use a *dynamic* `import("./path/to/build.js")`
956 // to run the Bjørn build steps.
958 // To make writing scripts which make use of these hooks easier,
959 // infrastructural dependencies and useful functions are provided on
960 // `globalThis` so that wrapping scripts don’t have to attempt to
961 // manage the dependencies themselves.
963 // Note that the `Lemon/window` polyfill will already have
964 // established some D·O·M‐related global properties by the time this
965 // runs, so they don’t need to be redeclared here.
966 globalThis
.Lemon
= Lemon
;
975 await (async () => { // this is the run script
978 // Set up the feed metadata and Atom feed document.
979 const feedDocument
= parser
.parseFromString(
980 await Deno
.readTextFile(`${basePath}/#feed.rdf`),
983 const feedMetadata
= metadataFromDocument(feedDocument
);
984 const feedURI
= new URL(feedMetadata
.id
);
985 const document
= parser
.parseFromString(
986 `<?xml version="1.0" encoding="utf-8"?>
987 <feed xmlns="http://www.w3.org/2005/Atom"><generator>🧸📔 Bjørn</generator><link rel="self" type="application/atom+xml" href="${new URL(
993 const { documentElement
: feed
} = document
;
994 const feedLanguage
= getLanguage(feedDocument
);
996 // The feed element has a language.
997 setLanguage(feed
, feedLanguage
);
999 // There is no language for the feed.
1002 applyMetadata(feed
, feedMetadata
);
1004 // Set up the index page.
1005 const feedTemplate
= await
documentFromTemplate("feed");
1006 const { documentElement
: feedTemplateRoot
} = feedTemplate
;
1007 if (feedLanguage
&& !getLanguage(feedTemplateRoot
)) {
1008 // The root element of the template does not have an
1009 // assigned language, but the feed does.
1010 setLanguage(feedTemplateRoot
, feedLanguage
);
1012 // Either the template root already has a language, or the
1013 // entry doesn’t either.
1016 const feedEntries
= feedTemplate
.createDocumentFragment();
1018 // Process entries and save the resulting index files.
1020 const { name
: date
} of Array
.from(
1021 Deno
.readDirSync(`${basePath}/`),
1022 ).filter(({ name
: date
, isDirectory
}) =>
1023 // Exclude non‐dated directories.
1024 isDirectory
&& /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u.test(date
)
1025 ).sort(({ name
: a
}, { name
: b
}) =>
1026 // Sort the directories.
1027 a
< b
? 1 : a
> b
? -1 : 0
1030 // Iterate over each dated directory and process its entries.
1032 const { name
: entryName
} of Array
.from(
1033 Deno
.readDirSync(`${basePath}/${date}/`),
1034 ).filter(({ name
: entryName
, isDirectory
}) =>
1035 // Exclude non‐entry directories.
1037 //deno-lint-ignore no-control-regex
1038 !/[\x00-\x20\x22#%/<>?\\^\x60{|}\x7F]/u
.test(entryName
)
1039 ).sort(({ name
: a
}, { name
: b
}) =>
1040 // Sort the directories.
1041 a
< b
? 1 : a
> b
? -1 : 0
1044 // Iterate over each entry directory and process its contents.
1045 const entry
= document
.createElement("entry");
1046 const entryPath
= `${basePath}/${date}/${entryName}/#entry.rdf`;
1047 const entryDocument
= parser
.parseFromString(
1048 await Deno
.readTextFile(entryPath
),
1051 const { documentElement
: entryRoot
} = entryDocument
;
1052 entryDocument
.lastModified
= (await Deno
.lstat(entryPath
)).mtime
;
1053 if (!entryRoot
.hasAttributeNS(RDF
, "about")) {
1054 // The entry doesn’t have an identifier; let’s give it one.
1055 entryRoot
.setAttributeNS(
1058 new URL(`./${date}/${entryName}/`, feedURI
),
1061 // The entry already has an identifier.
1064 const entryMetadata
= metadataFromDocument(entryDocument
);
1065 if (entryMetadata
.author
.length
== 0) {
1066 // The entry metadata did not supply an author.
1067 entryMetadata
.author
= feedMetadata
.author
;
1069 // The entry metadata supplied its own author.
1072 const entryTemplate
= await
documentFromTemplate("entry");
1073 const { documentElement
: templateRoot
} = entryTemplate
;
1074 const lang
= getLanguage(entryRoot
);
1075 if (lang
&& !getLanguage(templateRoot
)) {
1076 // The root element of the template does not have an
1077 // assigned language, but the entry does.
1078 setLanguage(templateRoot
, lang
);
1080 // Either the template root already has a language, or the
1081 // entry doesn’t either.
1086 `${basePath}/${date}/${entryName}/index.xhtml`,
1087 serializer
.serializeToString(
1088 applyMetadata(entryTemplate
, entryMetadata
),
1092 applyMetadata(entry
, entryMetadata
);
1093 applyMetadata(feedEntries
, entryMetadata
);
1094 feed
.appendChild(entry
);
1098 // Apply the feed metadata to the feed template and save the
1099 // resulting index file.
1100 if (hasExpandedName(feedTemplateRoot
, XHTML
, "html")) {
1101 // This is an XHTML template.
1102 const LMN
= Lemon
.bind({ document
: feedTemplate
});
1110 fillOutHead(feedTemplate
, feedMetadata
, "feed");
1111 const contentPlaceholder
= feedTemplate
.getElementsByTagNameNS(
1115 if (contentPlaceholder
!= null) {
1116 // There is a content placeholder.
1117 const { parentNode
: contentParent
} = contentPlaceholder
;
1118 const contentElement
= contentParent
.insertBefore(
1119 LMN
.nav
.about(`${id}`)`${"\n"}`,
1122 const contentHeader
= contentElement
.appendChild(
1123 LMN
.header
`${"\n\t"}`,
1126 contentHeader
.appendChild(LMN
.h1
.property(`${DC11}title`)``),
1129 addContent(contentHeader
, "\n");
1131 // The feed has a subtitle.
1133 contentHeader
.appendChild(LMN
.p
.property(`${RDFS}label`)``),
1136 addContent(contentHeader
, "\n");
1138 // The feed has no subtitle.
1141 addContent(contentElement
, "\n");
1142 const entriesElement
= contentElement
.appendChild(
1143 LMN
.ul
.rel(`${AWOL}entry`)`${"\n"}`,
1145 entriesElement
.appendChild(feedEntries
);
1146 addContent(contentElement
, "\n");
1147 const contentFooter
= contentElement
.appendChild(
1148 LMN
.footer
`${"\n\t"}`,
1151 // The feed has a rights statement.
1153 contentFooter
.appendChild(
1154 LMN
.div
.property(`${DC11}rights`)``,
1158 addContent(contentFooter
, "\n\t");
1160 // The feed has no rights statement.
1163 contentFooter
.appendChild(
1164 LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property(
1168 addContent(contentFooter
, "\n");
1169 addContent(contentElement
, "\n");
1170 contentParent
.removeChild(contentPlaceholder
);
1175 globalThis
.bj
ørnTransformFeedHTML
?.(feedTemplate
, feedMetadata
);
1179 serializer
.serializeToString(feedTemplate
) + "\n",
1183 // Save the feed Atom file.
1184 globalThis
.bj
ørnTransformFeedAtom
?.(document
, feedMetadata
);
1188 serializer
.serializeToString(document
) + "\n",
1192 // Await all writes.
1193 await Promise
.all(writes
);