1 #!/usr/bin
/env
-S deno run
--allow
-read
--allow
-write
3 // ====================================================================
5 // Copyright © 2022 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 SIOC
= "http://rdfs.org/sioc/ns#";
58 const XML
= "http://www.w3.org/XML/1998/namespace";
59 const XHTML
= "http://www.w3.org/1999/xhtml";
62 * Adds the provided content to the provided element.
64 * Content may be either nullish (in which case nothing is added), a
65 * string (object or literal), a NodeList, or an array of zero or more
66 * of these values. Nodes need not belong to the same document as the
67 * provided element; they will be imported. During this process,
68 * special care is taken to ensure that the resulting content is
69 * correctly language‐tagged for its new context.
71 * ☡ If the provided element is not attached to anything, then it won’t
72 * be possible to walk its parent nodes in search of language
73 * information. Generally, it is best to attach elements to a document
74 * BEFORE calling this function.
76 const addContent
= (element
, content
) => {
77 const { ownerDocument
: document
} = element
;
78 const LMN
= Lemon
.bind({ document
});
79 if (content
== null) {
80 // The provided content is nullish.
82 } else if (Array
.isArray(content
)) {
83 // The provided content is an array.
84 content
.forEach(addContent
.bind(null, element
));
85 } else if (Object(content
) instanceof String
) {
86 // The provided content is a string (object or literal).
87 const { lang
} = content
;
88 if (lang
&& lang
!= getLanguage(element
)) {
89 const newChild
= element
.appendChild(LMN
.span
`${content}`);
90 setLanguage(newChild
, lang
);
92 element
.appendChild(document
.createTextNode(`${content}`));
95 // Assume the provided content is a NodeList.
96 if (element
.hasAttribute("property")) {
97 // The provided element has an R·D·F∕A property; note that its
98 // datatype is `XMLLiteral`.
99 element
.setAttribute("datatype", "XMLLiteral");
101 // The provided element does not have an R·D·F∕A property.
104 for (const child
of Array
.from(content
)) {
105 // Iterate over the nodes in the provided NodeList and handle
106 // them appropriately.
107 const lang
= getLanguage(child
);
108 const newChild
= (() => {
109 const imported
= document
.importNode(child
, true);
110 if (lang
&& lang
!= getLanguage(element
)) {
111 // The imported node’s language would change if it were just
112 // imported directly into the provided element.
113 if (imported
.nodeType
== 1) {
114 // The imported node is an element node.
115 setLanguage(imported
, lang
);
117 } else if (imported
.nodeType
<= 4) {
118 // The imported node is a text node.
119 const result
= LMN
.span
`${imported}`;
120 setLanguage(result
, lang
);
123 // The imported node is not an element or text.
127 // The imported node’s language will not change if imported
128 // directly into the provided element.
132 element
.appendChild(newChild
);
139 * Adds HTML for the provided people to the provided element, tagging
140 * them with the provided property.
142 * ☡ As with `addContent`, it is best to attach elements to a document
143 * PRIOR to providing them to this function, for language‐detection
146 const addPeople
= (element
, people
, property
) => {
147 const { ownerDocument
: document
} = element
;
148 const LMN
= Lemon
.bind({ document
});
149 const { length
} = people
;
150 for (const [index
, { uri
, name
}] of people
.entries()) {
151 const personElement
= element
.appendChild(
153 ? LMN
.a
.rel(`${property}`).href(`${uri}`)``
154 : LMN
.span
.rel(`${property}`)``,
157 // The current person has no name; provide its `uri`.
158 personElement
.appendChild(LMN
.code
`${uri}`);
160 // The current person has a name.
162 personElement
.appendChild(
163 LMN
.span
.property(`${FOAF}name`)``,
168 if (index
< length
- 2) {
169 // The current person is two or greater from the end.
170 addContent(element
, ", ");
171 } else if (index
< length
- 1) {
172 // The current person is one from the end.
173 addContent(element
, " & ");
175 // The current person is the last.
183 * Applies the provided metadata to the provided node by creating and
184 * inserting the appropriate elements, then returns the provided node.
186 * If the provided node is a document, it is assumed to be an entry
187 * template, and full entry H·T·M·L is generated. If it is a document
188 * fragment, it is assumed to be a document fragment collecting entries
189 * for the H·T·M·L feed index page, and entry H·T·M·L links are
190 * generated. Otherwise, the provided node is assumed to be an Atom
191 * element, and Atom metadata elements are generated.
193 const applyMetadata
= (node
, metadata
) => {
194 if (node
.nodeType
== 9) {
195 // The provided node is a document.
197 // Assume it is an entry template document and insert the full
198 // entry H·T·M·L accordingly.
199 const document
= node
;
200 const { documentElement
} = document
;
201 if (hasExpandedName(documentElement
, XHTML
, "html")) {
202 // This is an XHTML template.
203 const LMN
= Lemon
.bind({ document
});
204 const head
= Array
.from(documentElement
.childNodes
).find(($) =>
205 hasExpandedName($, XHTML
, "head")
206 ) ?? documentElement
.insertBefore(
208 documentElement
.childNodes
.item(0),
210 const titleElement
= Array
.from(head
.childNodes
).find(($) =>
211 hasExpandedName($, XHTML
, "title")
212 ) ?? head
.appendChild(LMN
.title
``);
224 titleElement
.textContent
= Object(title
) instanceof String
226 : Array
.from(title
?? []).map(($) =>
229 for (const person
of author
) {
230 // Iterate over authors and add appropriate meta tags.
232 LMN
.meta({ name
: "author" })
233 .content(`${person.name ?? person.uri}`)``,
237 // The entry has a summary.
239 LMN
.meta({ name
: "description" })
242 Object(summary) instanceof String
244 : Array.from(summary).map(($) => $.textContent).join(
253 const contentPlaceholder
= document
.getElementsByTagNameNS(
257 if (contentPlaceholder
!= null) {
258 // The content placeholder exists; replace it with the content
260 const { parentNode
: contentParent
} = contentPlaceholder
;
261 const contentElement
= contentParent
.insertBefore(
262 LMN
.article
.about(`${id}`)`${"\n"}`,
266 // Handle the entry content header.
267 contentElement
.appendChild(
268 document
.createComment(" BEGIN ENTRY HEADER "),
270 addContent(contentElement
, "\n");
271 const contentHeader
= contentElement
.appendChild(
272 LMN
.header
.id("entry.header")`${"\n\t"}`,
275 contentHeader
.appendChild(
276 LMN
.h1
.id("entry.title").property(`${DC11}title`)``,
280 if (author
.length
> 0) {
281 // This entry has authors.
282 addContent(contentHeader
, "\n\t");
284 contentHeader
.appendChild(
285 LMN
.p
.id("entry.author")``,
291 // This entry does not have authors.
294 if (contributor
.length
> 0) {
295 // This entry has contributors.
296 addContent(contentHeader
, "\n\t");
299 contentHeader
.appendChild(
302 )`With contributions from `,
305 `${DC11}contributor`,
310 // This entry does not have contributors.
314 // This entry has a publication date.
315 addContent(contentHeader
, "\n\t");
316 contentHeader
.appendChild(
317 LMN
.p
.id("entry.published")`Published: ${LMN.time.property(
322 // This entry does not have a publication date.
325 addContent(contentHeader
, "\n");
326 addContent(contentElement
, "\n");
327 contentElement
.appendChild(
328 document
.createComment(" END ENTRY HEADER "),
330 addContent(contentElement
, "\n");
332 // Handle the entry content.
333 contentElement
.appendChild(
334 document
.createComment(" BEGIN ENTRY CONTENT "),
336 addContent(contentElement
, "\n");
338 contentElement
.appendChild(
339 LMN
.div
.id("entry.content").property(`${SIOC}content`)``,
343 addContent(contentElement
, "\n");
344 contentElement
.appendChild(
345 document
.createComment(" END ENTRY CONTENT "),
347 addContent(contentElement
, "\n");
349 // Handle the entry content footer.
350 contentElement
.appendChild(
351 document
.createComment(" BEGIN ENTRY FOOTER "),
353 addContent(contentElement
, "\n");
354 const contentFooter
= contentElement
.appendChild(
355 LMN
.footer
.id("entry.footer")`${"\n\t"}`,
359 contentFooter
.appendChild(
360 LMN
.div
.id("entry.rights").property(`${DC11}rights`)``,
364 addContent(contentFooter
, "\n\t");
366 contentFooter
.appendChild(
367 LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property(
371 addContent(contentFooter
, "\n");
372 addContent(contentElement
, "\n");
373 contentElement
.appendChild(
374 document
.createComment(" END ENTRY FOOTER "),
376 addContent(contentElement
, "\n");
378 // Remove the placeholder.
379 contentParent
.removeChild(contentPlaceholder
);
381 // There is no content placeholder.
385 // This is not an XHTML template.
388 } else if (node
.nodeType
== 11) {
389 // The provided node is a document fragment.
391 // Assume it is collecting H·T·M·L feed entry links and insert a
392 // new one for the provided metadata.
393 const { ownerDocument
: document
} = node
;
394 const LMN
= Lemon
.bind({ document
});
402 // The content placeholder exists; replace it with the content
405 document
.createComment(` <${id}> `),
407 addContent(node
, "\n");
408 const contentElement
= node
.appendChild(
409 LMN
.li
.resource(`${id}`)`${"\n"}`,
412 contentElement
.appendChild(
413 LMN
.a
.href(`${id}`)``,
415 LMN
.h3
.property(`${DC11}title`)``,
419 if (author
.length
> 0) {
420 // This entry has authors.
421 addContent(contentElement
, "\n");
423 contentElement
.appendChild(
430 // This entry does not have authors.
434 // This entry has a publication date.
435 addContent(contentElement
, "\n");
436 contentElement
.appendChild(
437 LMN
.time
.property(`${DC11}date`)`${published}`,
440 // This entry does not have a publication date.
443 addContent(contentElement
, "\n");
445 contentElement
.appendChild(
446 LMN
.div
.property(`${DC11}abstract`)``,
450 addContent(contentElement
, "\n");
451 addContent(node
, "\n");
453 // The provided node is not a document or document fragment.
455 // Assume it is an Atom element of some sort and add the
456 // the appropriate metadata as child elements.
457 const { ownerDocument
: document
} = node
;
458 for (const [property
, values
] of Object
.entries(metadata
)) {
459 for (const value
of Array
.isArray(values
) ? values
: [values
]) {
460 const propertyNode
= document
.createElement(property
);
461 switch (context
[property
]?.type
) {
463 // The property describes a person.
464 const { name
, uri
} = value
;
466 // The person has a U·R·I.
467 const subnode
= document
.createElement("uri");
468 subnode
.textContent
= uri
;
469 propertyNode
.appendChild(subnode
);
471 // The person does not have a U·R·I.
475 // The person has a name.
476 const subnode
= document
.createElement("name");
477 subnode
.textContent
= name
;
478 propertyNode
.appendChild(subnode
);
480 // The person does not have a name.
483 if (propertyNode
.childNodes
.length
== 0) {
484 // Neither a U·R·I nor a name was added; skip adding this
492 // The property describes (potentially rich) text.
494 // The property has no value; skip appending it to the node.
496 } else if (Object(value
) instanceof String
) {
497 // The property has a string value.
498 propertyNode
.textContent
= value
;
501 // The property value is a list of nodes.
502 propertyNode
.setAttribute("type", "xhtml");
503 const div
= document
.createElementNS(XHTML
, "div");
504 for (const child
of Array
.from(value
)) {
505 div
.appendChild(document
.importNode(child
, true));
507 propertyNode
.appendChild(div
);
512 node
.appendChild(propertyNode
);
520 * The base path from which to pull files and generate resulting
523 const basePath
= `./${Deno.args[0] ?? ""}`;
526 * Mappings from Atom concepts to R·D·F∕X·M·L ones.
528 * `namespace` and `localName` give the R·D·F representation for the
529 * concept. Three `type`s are supported :—
531 * - "person": Has a `name` (`foaf:name`) and an `iri` (`rdf:about`).
532 * Emails are NOT supported.
534 * - "text": Can be string, markdown, or X·M·L content.
536 * - "literal": This is a plaintext field.
539 author
: { namespace: DC11
, localName
: "creator", type
: "person" },
540 // category is not supported at this time
541 content
: { namespace: SIOC
, localName
: "content", type
: "text" },
544 localName
: "contributor",
547 // generator is provided by the build script
548 icon
: { namespace: AWOL
, localName
: "icon", type
: "literal" },
549 // link is provided by the build script
550 logo
: { namespace: AWOL
, localName
: "logo", type
: "literal" },
556 rights
: { namespace: DC11
, localName
: "rights", type
: "text" },
557 // source is not supported at this time
558 // subtitle is not supported at this time
559 summary
: { namespace: DC11
, localName
: "abstract", type
: "text" },
560 title
: { namespace: DC11
, localName
: "title", type
: "text" },
561 // updated is provided by the build script
566 * Returns a new document created from the source code of the named
569 documentFromTemplate
,
571 const cache
= Object
.create(null);
573 documentFromTemplate
: async (name
) =>
574 parser
.parseFromString(
575 name
in cache
? cache
[name
] : (
576 cache
[name
] = await Deno
.readTextFile(
577 `${basePath}/index#${name}.xhtml`,
586 * Returns the language of the provided node, or null if it has no
589 * ※ This function returns null regardless of whether a node has no
590 * language implicitly (because no parent element had a language set)
591 * or explicitly (because the value of the @xml:lang attribute was "").
593 * ※ If no language is set, the language of the node is ambiguous.
594 * Because this typically occurs when a node is expected to inherit its
595 * language from some parent context, this lack of language information
596 * SHOULD NOT be preserved when inserting the node into a document
597 * where language information is present (instead allowing the node to
598 * inherit language information from the node it is being inserted
599 * into). There are explicit language codes which may be used if this
600 * behaviour is undesirable: If you want to signal that a node does not
601 * contain linguistic content, use the language code `zxx`. If you want
602 * to signal that a node’s language is undetermined in a way which will
603 * never inherit from a parent node, use the language code `und`.
605 const getLanguage
= (node
) => {
606 const { nodeType
} = node
;
608 // The provided node is an element.
610 node
.namespaceURI
== XHTML
&& node
.hasAttribute("lang") &&
611 node
.getAttribute("lang") ||
612 node
.hasAttributeNS(XML
, "lang") &&
613 node
.getAttributeNS(XML
, "lang");
615 // The element has a recognized language attribute set to a
618 } else if (ownLanguage
=== "") {
619 // The element explicitly has no language.
622 // The element has no language attribute, but may inherit a
623 // language from its parent.
624 const { parentNode
} = node
;
625 if (parentNode
!= null && parentNode
.nodeType
!= 9) {
626 // The provided node has a nondocument parent; get the language
628 return getLanguage(parentNode
);
630 // The provided node has no parent and consequently no language.
634 } else if (nodeType
== 9) {
635 // The provided node is a document.
636 return getLanguage(node
.documentElement
);
637 } else if (nodeType
== 11) {
638 // The provided node is a document fragment.
639 return getLanguage(node
.ownerDocument
.documentElement
);
641 // The provided node may inherit a language from a parent node.
642 const { parentNode
} = node
;
643 if (parentNode
!= null) {
644 // The provided node has a parent; get the language from there.
645 return getLanguage(parentNode
);
647 // The provided node has no parent and consequently no language.
654 * Returns whether the provided node has the provided namespace and
657 const hasExpandedName
= (node
, namespace, localName
) =>
658 node
.namespaceURI
== namespace && node
.localName
== localName
;
661 * Processes an RDF document and returns an object of Atom metadata.
663 * See `context` for the metadata properties supported.
665 const metadataFromDocument
= (
666 { documentElement
: root
, lastModified
},
668 const contextEntries
= [...Object
.entries(context
)];
669 const documentType
= hasExpandedName(root
, AWOL
, "Feed")
672 const result
= Object
.assign(Object
.create(null), {
673 id
: root
.getAttributeNS(RDF
, "about"),
674 updated
: documentType
== "feed" || lastModified
== null
675 ? new Date().toISOString()
676 : lastModified
.toISOString(),
681 ...(documentType
== "feed"
682 ? { // additional feed properties
687 : { // additional entry properties
695 ...Array
.from(root
.attributes
),
696 ...Array
.from(root
.childNodes
),
699 // Iterate over all child nodes and attributes, finding ones which
700 // correspond to Atom properties and assigning the appropriate
702 const [name
, { type
}] = contextEntries
.find(
704 hasExpandedName(node
, value
.namespace, value
.localName
),
706 if (name
!= null && name
in result
) {
707 // The current node corresponds with an Atom property.
708 const { [name
]: existing
} = result
;
709 const content
= (() => {
712 // The node describes a person.
714 node
.getAttributeNS
?.(RDF
, "parseType") == "Resource"
716 : Array
.from(node
.childNodes
).find(($) =>
721 uri
: base
.getAttributeNS
?.(RDF
, "about") || null,
725 ...Array
.from(base
.attributes
),
726 ...Array
.from(base
.childNodes
),
729 // Process child nodes and attributes for the current
730 // person, looking for name metadata.
731 if (hasExpandedName(subnode
, FOAF
, "name")) {
732 // This is a name node.
733 if (person
.name
== null) {
734 // No name has been set yet.
735 person
.name
= subnode
.textContent
;
737 // A name has already been set.
739 `Duplicate name found for person${
740 person.id != null ? ` <${person.id}
>` : ""
741 } while processing <${result.id}>.`,
745 // This is not a name node
752 // The node describes (potentially rich) textual content.
754 // ☡ Don’t return an Array here or it will look like a list
755 // of multiple values. Return the NodeList of child nodes
757 const parseType
= node
.getAttributeNS
?.(RDF
, "parseType");
758 if (parseType
== "Markdown") {
759 // This is an element with Markdown content (which
760 // hopefully can be converted into X·M·L).
761 return parser
.parseFromString(
762 `<福 xmlns="${XHTML}" lang="${
763 getLanguage(node) ?? ""
765 markdownTokensToHTML(
766 markdownTokens(node.textContent),
770 ).documentElement
.childNodes
;
771 } else if (parseType
== "Literal") {
772 // This is an element with literal X·M·L contents.
773 return node
.childNodes
;
775 // This is an element without literal X·M·L contents.
780 // The node describes something in plaintext.
781 const text
= new String(node
.textContent
);
782 const lang
= getLanguage(node
);
792 if (existing
== null) {
793 // The property takes at most one value, but none has been set.
794 result
[name
] = content
;
795 } else if (Array
.isArray(existing
)) {
796 // The property takes multiple values.
797 existing
.push(content
);
799 // The property takes at most one value, and one has already
802 `Duplicate content found for ${name} while processing <${result.id}>.`,
806 // The current node does not correspond with an Atom property.
810 return validateMetadata(result
, documentType
);
813 /** The DOMParser used by this script. */
814 const parser
= new DOMParser();
816 /** The XMLSerializer used by this script. */
817 const serializer
= new XMLSerializer();
820 * Sets the @xml:lang attribute of the provided element, and if it is
821 * an H·T·M·L element also sets the @lang.
823 const setLanguage
= (element
, lang
) => {
824 element
.setAttributeNS(XML
, "xml:lang", lang
?? "");
825 if (element
.namespaceURI
== XHTML
) {
826 element
.setAttribute("lang", lang
?? "");
833 * Throws if the provided metadata does not conform to expectations for
834 * the provided type, and otherwise returns it.
836 const validateMetadata
= (metadata
, type
) => {
837 if (metadata
.id
== null) {
838 throw new TypeError("Missing id.");
839 } else if (metadata
.title
== null) {
840 throw new TypeError(`Missing title for item <${metadata.id}>.`);
841 } else if (type
== "feed" && metadata
.author
== null) {
842 throw new TypeError(`Missing author for feed <${metadata.id}>.`);
843 } else if (type
== "entry" && metadata
.content
== null) {
844 throw new TypeError(`Missing content for entry <${metadata.id}>.`);
850 await (async () => { // this is the run script
853 // Set up the Atom feed.
854 const document
= parser
.parseFromString(
855 `<?xml version="1.0" encoding="utf-8"?>
856 <feed xmlns="http://www.w3.org/2005/Atom"></feed>`,
859 const { documentElement
: feed
} = document
;
860 const feedMetadata
= metadataFromDocument(
861 parser
.parseFromString(
862 await Deno
.readTextFile(`${basePath}/#feed.rdf`),
866 const feedURI
= new URL(feedMetadata
.id
);
867 applyMetadata(feed
, feedMetadata
);
869 // Set up the index page.
870 const feedTemplate
= await
documentFromTemplate("feed");
871 const feedEntries
= feedTemplate
.createDocumentFragment();
873 // Process entries and save the resulting index files.
875 const { name
: date
, isDirectory
} of Deno
.readDir(
879 // Iterate over each directory and process the ones which are
881 if (!isDirectory
|| !/[0-9]{4}-[0-9]{2}-[0-9]{2}/u.test(date
)) {
882 // This isn’t a dated directory.
885 // This is a dated directory.
887 const { name
: entryName
, isDirectory
} of Deno
.readDir(
888 `${basePath}/${date}/`,
891 // Iterate over each directory and process the ones which look
895 //deno-lint-ignore no-control-regex
896 /[\x00-\x20\x22#%/<>?\\^\x60{|}\x7F]/u
.test(entryName
)
898 // This isn’t an entry directory.
901 // Process the entry.
902 const entry
= document
.createElement("entry");
904 `${basePath}/${date}/${entryName}/#entry.rdf`;
905 const entryDocument
= parser
.parseFromString(
906 await Deno
.readTextFile(entryPath
),
909 const { documentElement
: entryRoot
} = entryDocument
;
910 entryDocument
.lastModified
=
911 (await Deno
.lstat(entryPath
)).mtime
;
912 if (!entryRoot
.hasAttributeNS(RDF
, "about")) {
913 // The entry doesn’t have an identifier; let’s give it one.
914 entryRoot
.setAttributeNS(
917 new URL(`./${date}/${entryName}/`, feedURI
),
920 // The entry already has an identifier.
923 const entryMetadata
= metadataFromDocument(entryDocument
);
924 if (entryMetadata
.author
.length
== 0) {
925 // The entry metadata did not supply an author.
926 entryMetadata
.author
= feedMetadata
.author
;
928 // The entry metadata supplied its own author.
931 const entryTemplate
= await
documentFromTemplate("entry");
932 const { documentElement
: templateRoot
} = entryTemplate
;
933 const lang
= getLanguage(entryRoot
);
934 if (lang
&& !getLanguage(templateRoot
)) {
935 // The root element of the template does not have an
936 // assigned language, but the entry does.
937 setLanguage(templateRoot
, lang
);
939 // Either the template root already has a language, or the
940 // entry doesn’t either.
945 `${basePath}/${date}/${entryName}/index.xhtml`,
946 serializer
.serializeToString(
947 applyMetadata(entryTemplate
, entryMetadata
),
951 applyMetadata(entry
, entryMetadata
);
952 applyMetadata(feedEntries
, entryMetadata
);
953 feed
.appendChild(entry
);
959 // Apply the feed metadata to the feed template and save the
960 // resulting index file.
961 const { documentElement
: feedRoot
} = feedTemplate
;
962 if (hasExpandedName(feedRoot
, XHTML
, "html")) {
963 // This is an XHTML template.
964 const LMN
= Lemon
.bind({ document
: feedTemplate
});
965 const head
= Array
.from(feedRoot
.childNodes
).find(($) =>
966 hasExpandedName($, XHTML
, "head")
967 ) ?? feedRoot
.insertBefore(
969 feedRoot
.childNodes
.item(0),
971 const titleElement
= Array
.from(head
.childNodes
).find(($) =>
972 hasExpandedName($, XHTML
, "title")
973 ) ?? head
.appendChild(LMN
.title
``);
981 titleElement
.textContent
= Object(title
) instanceof String
983 : Array
.from(title
?? []).map(($) =>
986 for (const person
of author
) {
987 // Iterate over authors and add appropriate meta tags.
989 LMN
.meta({ name
: "author" })
990 .content(`${person.name ?? person.uri}`)``,
993 const contentPlaceholder
= feedTemplate
.getElementsByTagNameNS(
997 if (contentPlaceholder
!= null) {
998 const { parentNode
: contentParent
} = contentPlaceholder
;
999 const contentElement
= contentParent
.insertBefore(
1000 LMN
.nav
.about(`${id}`)`${"\n"}`,
1003 const contentHeader
= contentElement
.appendChild(
1004 LMN
.header
`${"\n\t"}`,
1007 contentHeader
.appendChild(LMN
.h1
.property(`${DC11}title`)``),
1010 addContent(contentHeader
, "\n");
1011 addContent(contentElement
, "\n");
1012 const entriesElement
= contentElement
.appendChild(
1013 LMN
.ul
.rel(`${AWOL}entry`)`${"\n"}`,
1015 entriesElement
.appendChild(feedEntries
);
1016 addContent(contentElement
, "\n");
1017 const contentFooter
= contentElement
.appendChild(
1018 LMN
.footer
`${"\n\t"}`,
1022 contentFooter
.appendChild(
1023 LMN
.div
.property(`${DC11}rights`)``,
1027 addContent(contentFooter
, "\n\t");
1029 contentFooter
.appendChild(
1030 LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property(
1034 addContent(contentFooter
, "\n");
1035 addContent(contentElement
, "\n");
1036 contentParent
.removeChild(contentPlaceholder
);
1044 serializer
.serializeToString(feedTemplate
) + "\n",
1048 // Save the feed Atom file.
1052 serializer
.serializeToString(document
) + "\n",
1056 // Await all writes.
1057 await Promise
.all(writes
);