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 
}); 
 214       fillOutHead(document
, metadata
, "entry"); 
 215       const contentPlaceholder 
= document
.getElementsByTagNameNS( 
 219       if (contentPlaceholder 
!= null) { 
 220         // The content placeholder exists; replace it with the content 
 222         const { parentNode: contentParent 
} = contentPlaceholder
; 
 223         const contentElement 
= contentParent
.insertBefore( 
 224           LMN
.article
.about(`${id}`)`${"\n"}`, 
 228         // Handle the entry content header. 
 229         contentElement
.appendChild( 
 230           document
.createComment(" BEGIN ENTRY HEADER "), 
 232         addContent(contentElement
, "\n"); 
 233         const contentHeader 
= contentElement
.appendChild( 
 234           LMN
.header
.id("entry.header")`${"\n\t"}`, 
 237           contentHeader
.appendChild( 
 238             LMN
.h1
.id("entry.title").property(`${DC11}title`)``, 
 242         if (author
.length 
> 0) { 
 243           // This entry has authors. 
 244           addContent(contentHeader
, "\n\t"); 
 246             contentHeader
.appendChild( 
 247               LMN
.p
.id("entry.author")``, 
 253           // This entry does not have authors. 
 256         if (contributor
.length 
> 0) { 
 257           // This entry has contributors. 
 258           addContent(contentHeader
, "\n\t"); 
 261               contentHeader
.appendChild( 
 264                 )`With contributions from `, 
 267               `${DC11}contributor`, 
 272           // This entry does not have contributors. 
 276           // This entry has a publication date. 
 277           addContent(contentHeader
, "\n\t"); 
 278           contentHeader
.appendChild( 
 279             LMN
.p
.id("entry.published")`Published: ${LMN.time.property( 
 284           // This entry does not have a publication date. 
 287         addContent(contentHeader
, "\n"); 
 288         addContent(contentElement
, "\n"); 
 289         contentElement
.appendChild( 
 290           document
.createComment(" END ENTRY HEADER "), 
 292         addContent(contentElement
, "\n"); 
 294         // Handle the entry content. 
 295         contentElement
.appendChild( 
 296           document
.createComment(" BEGIN ENTRY CONTENT "), 
 298         addContent(contentElement
, "\n"); 
 300           contentElement
.appendChild( 
 301             LMN
.div
.id("entry.content").property(`${SIOC}content`)``, 
 305         addContent(contentElement
, "\n"); 
 306         contentElement
.appendChild( 
 307           document
.createComment(" END ENTRY CONTENT "), 
 309         addContent(contentElement
, "\n"); 
 311         // Handle the entry content footer. 
 312         contentElement
.appendChild( 
 313           document
.createComment(" BEGIN ENTRY FOOTER "), 
 315         addContent(contentElement
, "\n"); 
 316         const contentFooter 
= contentElement
.appendChild( 
 317           LMN
.footer
.id("entry.footer")`${"\n\t"}`, 
 321             contentFooter
.appendChild( 
 322               LMN
.div
.id("entry.rights").property(`${DC11}rights`)``, 
 326           addContent(contentFooter
, "\n\t"); 
 328         contentFooter
.appendChild( 
 329           LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property( 
 333         addContent(contentFooter
, "\n"); 
 334         addContent(contentElement
, "\n"); 
 335         contentElement
.appendChild( 
 336           document
.createComment(" END ENTRY FOOTER "), 
 338         addContent(contentElement
, "\n"); 
 340         // Remove the placeholder. 
 341         contentParent
.removeChild(contentPlaceholder
); 
 343         // There is no content placeholder. 
 347       // This is not an XHTML template. 
 350   } else if (node
.nodeType 
== 11) { 
 351     // The provided node is a document fragment. 
 353     // Assume it is collecting H·T·M·L feed entry links and insert a 
 354     // new one for the provided metadata. 
 355     const { ownerDocument: document 
} = node
; 
 356     const LMN 
= Lemon
.bind({ document 
}); 
 364     // The content placeholder exists; replace it with the content 
 367       document
.createComment(` <${id}> `), 
 369     addContent(node
, "\n"); 
 370     const contentElement 
= node
.appendChild( 
 371       LMN
.li
.resource(`${id}`)`${"\n"}`, 
 374       contentElement
.appendChild( 
 375         LMN
.a
.href(`${id}`)``, 
 377         LMN
.h3
.property(`${DC11}title`)``, 
 381     if (author
.length 
> 0) { 
 382       // This entry has authors. 
 383       addContent(contentElement
, "\n"); 
 385         contentElement
.appendChild( 
 392       // This entry does not have authors. 
 396       // This entry has a publication date. 
 397       addContent(contentElement
, "\n"); 
 398       contentElement
.appendChild( 
 399         LMN
.time
.property(`${DC11}date`)`${published}`, 
 402       // This entry does not have a publication date. 
 405     addContent(contentElement
, "\n"); 
 407       contentElement
.appendChild( 
 408         LMN
.div
.property(`${DC11}abstract`)``, 
 412     addContent(contentElement
, "\n"); 
 413     addContent(node
, "\n"); 
 415     // The provided node is not a document or document fragment. 
 417     // Assume it is an Atom element of some sort and add the 
 418     // the appropriate metadata as child elements. 
 419     const { ownerDocument: document 
} = node
; 
 420     const alternateLink 
= node
.appendChild( 
 421       document
.createElement("link"), 
 423     alternateLink
.setAttribute("rel", "alternate"); 
 424     alternateLink
.setAttribute("type", "application/xhtml+xml"); 
 425     alternateLink
.setAttribute("href", metadata
.id
); 
 426     for (const [property
, values
] of Object
.entries(metadata
)) { 
 427       for (const value 
of Array
.isArray(values
) ? values : [values
]) { 
 428         const propertyNode 
= document
.createElement(property
); 
 429         switch (context
[property
]?.type
) { 
 431             // The property describes a person. 
 432             const { name
, uri 
} = value
; 
 434               // The person has a U·R·I. 
 435               const subnode 
= document
.createElement("uri"); 
 436               subnode
.textContent 
= uri
; 
 437               propertyNode
.appendChild(subnode
); 
 439               // The person does not have a U·R·I. 
 443               // The person has a name. 
 444               const subnode 
= document
.createElement("name"); 
 445               subnode
.textContent 
= name
; 
 446               propertyNode
.appendChild(subnode
); 
 448               // The person does not have a name. 
 451             if (propertyNode
.childNodes
.length 
== 0) { 
 452               // Neither a U·R·I nor a name was added; skip adding this 
 460             // The property describes (potentially rich) text. 
 462               // The property has no value; skip appending it to the node. 
 464             } else if (Object(value
) instanceof String
) { 
 465               // The property has a string value. 
 466               propertyNode
.textContent 
= value
; 
 469               // The property value is a list of nodes. 
 470               propertyNode
.setAttribute("type", "xhtml"); 
 471               const div 
= document
.createElementNS(XHTML
, "div"); 
 472               for (const child 
of Array
.from(value
)) { 
 473                 div
.appendChild(document
.importNode(child
, true)); 
 475               propertyNode
.appendChild(div
); 
 480         node
.appendChild(propertyNode
); 
 488  * The base path from which to pull files and generate resulting 
 491 const basePath 
= `./${Deno.args[0] ?? ""}`; 
 494  * Mappings from Atom concepts to R·D·F∕X·M·L ones. 
 496  * `namespace` and `localName` give the R·D·F representation for the 
 497  * concept. Three `type`s are supported :— 
 499  * - "person": Has a `name` (`foaf:name`) and an `iri` (`rdf:about`). 
 500  *   Emails are NOT supported. 
 502  * - "text": Can be string, markdown, or X·M·L content. 
 504  * - "literal": This is a plaintext field. 
 507   author: { namespace: DC11
, localName: "creator", type: "person" }, 
 508   // category is not supported at this time 
 509   content: { namespace: SIOC
, localName: "content", type: "text" }, 
 512     localName: "contributor", 
 515   // generator is provided by the build script 
 516   icon: { namespace: AWOL
, localName: "icon", type: "literal" }, 
 517   // link is provided by the build script 
 518   logo: { namespace: AWOL
, localName: "logo", type: "literal" }, 
 524   rights: { namespace: DC11
, localName: "rights", type: "text" }, 
 525   // source is not supported at this time 
 526   // subtitle is not supported at this time 
 527   summary: { namespace: DC11
, localName: "abstract", type: "text" }, 
 528   title: { namespace: DC11
, localName: "title", type: "text" }, 
 529   // updated is provided by the build script 
 534    * Returns a new document created from the source code of the named 
 537   documentFromTemplate
, 
 539   const cache 
= Object
.create(null); 
 541     documentFromTemplate: async (name
) => 
 542       parser
.parseFromString( 
 543         name 
in cache 
? cache
[name
] : ( 
 544           cache
[name
] = await Deno
.readTextFile( 
 545             `${basePath}/index#${name}.xhtml`, 
 554  * Fills out the `head` of the provided H·T·M·L document with the 
 555  * appropriate metadata. 
 557 const fillOutHead 
= (document
, metadata
, type
) => { 
 558   const { documentElement 
} = document
; 
 559   const LMN 
= Lemon
.bind({ document 
}); 
 561     Array
.from(documentElement
.childNodes
).find(($) => 
 562       hasExpandedName($, XHTML
, "head") 
 563     ) ?? documentElement
.insertBefore( 
 565       documentElement
.childNodes
.item(0), 
 568     Array
.from(head
.childNodes
).find(($) => 
 569       hasExpandedName($, XHTML
, "title") 
 570     ) ?? head
.appendChild(LMN
.title
``); 
 576   titleElement
.textContent 
= Object(title
) instanceof String
 
 578     : Array
.from(title 
?? []).map(($) => $.textContent
).join(""); 
 579   for (const person 
of author
) { 
 580     // Iterate over authors and add appropriate meta tags. 
 584         content: person
.name 
?? person
.uri
, 
 589     LMN
.meta({ name: "generator", content: "🧸📔 Bjørn" })``, 
 591   if (type 
== "entry") { 
 592     // The provided document is an entry document. 
 594       // The entry has a summary. 
 598           content: Object(summary
) instanceof String
 
 600             : Array
.from(summary
).map(($) => $.textContent
).join(""), 
 609         .type("application/atom+xml") 
 610         .href("../../feed.atom")``, 
 616         .type("application/atom+xml") 
 617         .href("./feed.atom")``, 
 623  * Returns the language of the provided node, or null if it has no 
 626  * ※ This function returns null regardless of whether a node has no 
 627  * language implicitly (because no parent element had a language set) 
 628  * or explicitly (because the value of the @xml:lang attribute was ""). 
 630  * ※ If no language is set, the language of the node is ambiguous. 
 631  * Because this typically occurs when a node is expected to inherit its 
 632  * language from some parent context, this lack of language information 
 633  * SHOULD NOT be preserved when inserting the node into a document 
 634  * where language information is present (instead allowing the node to 
 635  * inherit language information from the node it is being inserted 
 636  * into). There are explicit language codes which may be used if this 
 637  * behaviour is undesirable: If you want to signal that a node does not 
 638  * contain linguistic content, use the language code `zxx`. If you want 
 639  * to signal that a node’s language is undetermined in a way which will 
 640  * never inherit from a parent node, use the language code `und`. 
 642 const getLanguage 
= (node
) => { 
 643   const { nodeType 
} = node
; 
 645     // The provided node is an element. 
 647       node
.namespaceURI 
== XHTML 
&& node
.hasAttribute("lang") && 
 648         node
.getAttribute("lang") || 
 649       node
.hasAttributeNS(XML
, "lang") && 
 650         node
.getAttributeNS(XML
, "lang"); 
 652       // The element has a recognized language attribute set to a 
 655     } else if (ownLanguage 
=== "") { 
 656       // The element explicitly has no language. 
 659       // The element has no language attribute, but may inherit a 
 660       // language from its parent. 
 661       const { parentNode 
} = node
; 
 662       if (parentNode 
!= null && parentNode
.nodeType 
!= 9) { 
 663         // The provided node has a nondocument parent; get the language 
 665         return getLanguage(parentNode
); 
 667         // The provided node has no parent and consequently no language. 
 671   } else if (nodeType 
== 9) { 
 672     // The provided node is a document. 
 673     return getLanguage(node
.documentElement
); 
 674   } else if (nodeType 
== 11) { 
 675     // The provided node is a document fragment. 
 676     return getLanguage(node
.ownerDocument
.documentElement
); 
 678     // The provided node may inherit a language from a parent node. 
 679     const { parentNode 
} = node
; 
 680     if (parentNode 
!= null) { 
 681       // The provided node has a parent; get the language from there. 
 682       return getLanguage(parentNode
); 
 684       // The provided node has no parent and consequently no language. 
 691  * Returns whether the provided node has the provided namespace and 
 694 const hasExpandedName 
= (node
, namespace, localName
) => 
 695   node
.namespaceURI 
== namespace && node
.localName 
== localName
; 
 698  * Processes an RDF document and returns an object of Atom metadata. 
 700  * See `context` for the metadata properties supported. 
 702 const metadataFromDocument 
= ( 
 703   { documentElement: root
, lastModified 
}, 
 705   const contextEntries 
= [...Object
.entries(context
)]; 
 706   const documentType 
= hasExpandedName(root
, AWOL
, "Feed") 
 709   const result 
= Object
.assign(Object
.create(null), { 
 710     id: root
.getAttributeNS(RDF
, "about"), 
 711     updated: documentType 
== "feed" || lastModified 
== null 
 712       ? new Date().toISOString() 
 713       : lastModified
.toISOString(), 
 718     ...(documentType 
== "feed" 
 719       ? { // additional feed properties 
 724       : { // additional entry properties 
 732       ...Array
.from(root
.attributes
), 
 733       ...Array
.from(root
.childNodes
), 
 736     // Iterate over all child nodes and attributes, finding ones which 
 737     // correspond to Atom properties and assigning the appropriate 
 739     const [name
, { type 
}] = contextEntries
.find( 
 741         hasExpandedName(node
, value
.namespace, value
.localName
), 
 743     if (name 
!= null && name 
in result
) { 
 744       // The current node corresponds with an Atom property. 
 745       const { [name
]: existing 
} = result
; 
 746       const content 
= (() => { 
 749             // The node describes a person. 
 751               node
.getAttributeNS
?.(RDF
, "parseType") == "Resource" 
 753                 : Array
.from(node
.childNodes
).find(($) => 
 758               uri: base
.getAttributeNS
?.(RDF
, "about") || null, 
 762                 ...Array
.from(base
.attributes
), 
 763                 ...Array
.from(base
.childNodes
), 
 766               // Process child nodes and attributes for the current 
 767               // person, looking for name metadata. 
 768               if (hasExpandedName(subnode
, FOAF
, "name")) { 
 769                 // This is a name node. 
 770                 if (person
.name 
== null) { 
 771                   // No name has been set yet. 
 772                   person
.name 
= subnode
.textContent
; 
 774                   // A name has already been set. 
 776                     `Duplicate name found for person${ 
 777                       person.id != null ? ` <${person.id}
>` : "" 
 778                     } while processing <${result.id}>.`, 
 782                 // This is not a name node 
 789             // The node describes (potentially rich) textual content. 
 791             // ☡ Don’t return an Array here or it will look like a list 
 792             // of multiple values. Return the NodeList of child nodes 
 794             const parseType 
= node
.getAttributeNS
?.(RDF
, "parseType"); 
 795             if (parseType 
== "Markdown") { 
 796               // This is an element with Markdown content (which 
 797               // hopefully can be converted into X·M·L). 
 798               return parser
.parseFromString( 
 799                 `<福 xmlns="${XHTML}" lang="${ 
 800                   getLanguage(node) ?? "" 
 802                   markdownTokensToHTML( 
 803                     markdownTokens(node.textContent), 
 807               ).documentElement
.childNodes
; 
 808             } else if (parseType 
== "Literal") { 
 809               // This is an element with literal X·M·L contents. 
 810               return node
.childNodes
; 
 812               // This is an element without literal X·M·L contents. 
 817             // The node describes something in plaintext. 
 818             const text 
= new String(node
.textContent
); 
 819             const lang 
= getLanguage(node
); 
 829       if (existing 
== null) { 
 830         // The property takes at most one value, but none has been set. 
 831         result
[name
] = content
; 
 832       } else if (Array
.isArray(existing
)) { 
 833         // The property takes multiple values. 
 834         existing
.push(content
); 
 836         // The property takes at most one value, and one has already 
 839           `Duplicate content found for ${name} while processing <${result.id}>.`, 
 843       // The current node does not correspond with an Atom property. 
 847   return validateMetadata(result
, documentType
); 
 850 /** The DOMParser used by this script. */ 
 851 const parser 
= new DOMParser(); 
 853 /** The XMLSerializer used by this script. */ 
 854 const serializer 
= new XMLSerializer(); 
 857  * Sets the @xml:lang attribute of the provided element, and if it is 
 858  * an H·T·M·L element also sets the @lang. 
 860 const setLanguage 
= (element
, lang
) => { 
 861   element
.setAttributeNS(XML
, "xml:lang", lang 
?? ""); 
 862   if (element
.namespaceURI 
== XHTML
) { 
 863     element
.setAttribute("lang", lang 
?? ""); 
 870  * Throws if the provided metadata does not conform to expectations for 
 871  * the provided type, and otherwise returns it. 
 873 const validateMetadata 
= (metadata
, type
) => { 
 874   if (metadata
.id 
== null) { 
 875     throw new TypeError("Missing id."); 
 876   } else if (metadata
.title 
== null) { 
 877     throw new TypeError(`Missing title for item <${metadata.id}>.`); 
 878   } else if (type 
== "feed" && metadata
.author 
== null) { 
 879     throw new TypeError(`Missing author for feed <${metadata.id}>.`); 
 880   } else if (type 
== "entry" && metadata
.content 
== null) { 
 881     throw new TypeError(`Missing content for entry <${metadata.id}>.`); 
 887 await (async () => { // this is the run script 
 890   // Set up the feed metadata and Atom feed document. 
 891   const feedMetadata 
= metadataFromDocument( 
 892     parser
.parseFromString( 
 893       await Deno
.readTextFile(`${basePath}/#feed.rdf`), 
 897   const feedURI 
= new URL(feedMetadata
.id
); 
 898   const document 
= parser
.parseFromString( 
 899     `<?xml version="1.0" encoding="utf-8"?> 
 900 <feed xmlns="http://www.w3.org/2005/Atom"><generator>🧸📔 Bjørn</generator><link rel="self" type="application/atom+xml" href="${new URL( 
 906   const { documentElement: feed 
} = document
; 
 907   applyMetadata(feed
, feedMetadata
); 
 909   // Set up the index page. 
 910   const feedTemplate 
= await 
documentFromTemplate("feed"); 
 911   const feedEntries 
= feedTemplate
.createDocumentFragment(); 
 913   // Process entries and save the resulting index files. 
 915     const { name: date
, isDirectory 
} of Deno
.readDir( 
 919     // Iterate over each directory and process the ones which are 
 921     if (!isDirectory 
|| !/[0-9]{4}-[0-9]{2}-[0-9]{2}/u.test(date
)) { 
 922       // This isn’t a dated directory. 
 925       // This is a dated directory. 
 927         const { name: entryName
, isDirectory 
} of Array
.from( 
 928           Deno
.readDirSync(`${basePath}/${date}/`), 
 929         ).sort(({ name: a 
}, { name: b 
}) => 
 930           a 
< b 
? -1 : a 
> b 
? 1 : 0 
 933         // Iterate over each directory and process the ones which look 
 937           //deno-lint-ignore no-control-regex 
 938           /[\x00-\x20\x22#%/<>?\\^\x60{|}\x7F]/u
.test(entryName
) 
 940           // This isn’t an entry directory. 
 943           // Process the entry. 
 944           const entry 
= document
.createElement("entry"); 
 946             `${basePath}/${date}/${entryName}/#entry.rdf`; 
 947           const entryDocument 
= parser
.parseFromString( 
 948             await Deno
.readTextFile(entryPath
), 
 951           const { documentElement: entryRoot 
} = entryDocument
; 
 952           entryDocument
.lastModified 
= 
 953             (await Deno
.lstat(entryPath
)).mtime
; 
 954           if (!entryRoot
.hasAttributeNS(RDF
, "about")) { 
 955             // The entry doesn’t have an identifier; let’s give it one. 
 956             entryRoot
.setAttributeNS( 
 959               new URL(`./${date}/${entryName}/`, feedURI
), 
 962             // The entry already has an identifier. 
 965           const entryMetadata 
= metadataFromDocument(entryDocument
); 
 966           if (entryMetadata
.author
.length 
== 0) { 
 967             // The entry metadata did not supply an author. 
 968             entryMetadata
.author 
= feedMetadata
.author
; 
 970             // The entry metadata supplied its own author. 
 973           const entryTemplate 
= await 
documentFromTemplate("entry"); 
 974           const { documentElement: templateRoot 
} = entryTemplate
; 
 975           const lang 
= getLanguage(entryRoot
); 
 976           if (lang 
&& !getLanguage(templateRoot
)) { 
 977             // The root element of the template does not have an 
 978             // assigned language, but the entry does. 
 979             setLanguage(templateRoot
, lang
); 
 981             // Either the template root already has a language, or the 
 982             // entry doesn’t either. 
 987               `${basePath}/${date}/${entryName}/index.xhtml`, 
 988               serializer
.serializeToString( 
 989                 applyMetadata(entryTemplate
, entryMetadata
), 
 993           applyMetadata(entry
, entryMetadata
); 
 994           applyMetadata(feedEntries
, entryMetadata
); 
 995           feed
.appendChild(entry
); 
1001   // Apply the feed metadata to the feed template and save the 
1002   // resulting index file. 
1003   const { documentElement: feedRoot 
} = feedTemplate
; 
1004   if (hasExpandedName(feedRoot
, XHTML
, "html")) { 
1005     // This is an XHTML template. 
1006     const LMN 
= Lemon
.bind({ document: feedTemplate 
}); 
1013     fillOutHead(feedTemplate
, feedMetadata
, "feed"); 
1014     const contentPlaceholder 
= feedTemplate
.getElementsByTagNameNS( 
1018     if (contentPlaceholder 
!= null) { 
1019       const { parentNode: contentParent 
} = contentPlaceholder
; 
1020       const contentElement 
= contentParent
.insertBefore( 
1021         LMN
.nav
.about(`${id}`)`${"\n"}`, 
1024       const contentHeader 
= contentElement
.appendChild( 
1025         LMN
.header
`${"\n\t"}`, 
1028         contentHeader
.appendChild(LMN
.h1
.property(`${DC11}title`)``), 
1031       addContent(contentHeader
, "\n"); 
1032       addContent(contentElement
, "\n"); 
1033       const entriesElement 
= contentElement
.appendChild( 
1034         LMN
.ul
.rel(`${AWOL}entry`)`${"\n"}`, 
1036       entriesElement
.appendChild(feedEntries
); 
1037       addContent(contentElement
, "\n"); 
1038       const contentFooter 
= contentElement
.appendChild( 
1039         LMN
.footer
`${"\n\t"}`, 
1043           contentFooter
.appendChild( 
1044             LMN
.div
.property(`${DC11}rights`)``, 
1048         addContent(contentFooter
, "\n\t"); 
1050       contentFooter
.appendChild( 
1051         LMN
.p
.id("entry.updated")`Last updated: ${LMN.time.property( 
1055       addContent(contentFooter
, "\n"); 
1056       addContent(contentElement
, "\n"); 
1057       contentParent
.removeChild(contentPlaceholder
); 
1065       serializer
.serializeToString(feedTemplate
) + "\n", 
1069   // Save the feed Atom file. 
1073       serializer
.serializeToString(document
) + "\n", 
1077   // Await all writes. 
1078   await Promise
.all(writes
);