eleventy.config.js 106 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166
  1. import * as cheerio from "cheerio";
  2. import * as fs from "node:fs/promises";
  3. import * as nodeChildProcess from "node:child_process";
  4. import * as path from "node:path";
  5. import * as util from "node:util";
  6. import bib from "@retorquere/bibtex-parser";
  7. import { optimize } from "svgo";
  8. import sharp from "sharp";
  9. import slugify from "@sindresorhus/slugify";
  10. import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
  11. const exec = util.promisify(nodeChildProcess.exec);
  12. const NUM_RE = /^(-?)[0-9]+/;
  13. const WS_RE = /[\s\r\n]+/u;
  14. const ANCHOR_RE = /<a(\s|>)/i;
  15. const HTML_ESC_LUT = new Map([
  16. ["&", "&amp;"],
  17. ['"', "&quot;"],
  18. ["'", "&apos;"],
  19. ["<", "&lt;"],
  20. [">", "&gt;"],
  21. ]);
  22. const PERSONAGE_TYPES = new Set(["Person", "MusicGroup"]);
  23. const ENTRY_TO_ITEM = new Map([
  24. ["article", "Article"],
  25. ["book", "Book"],
  26. ["inbook", "Chapter"],
  27. ["incollection", "Article"],
  28. ["misc", "WebPage"],
  29. ]);
  30. const MONTHS = [
  31. "January",
  32. "February",
  33. "March",
  34. "April",
  35. "May",
  36. "June",
  37. "July",
  38. "August",
  39. "September",
  40. "October",
  41. "November",
  42. "December",
  43. ];
  44. async function walk(dirPath) {
  45. return Promise.all(
  46. await fs.readdir(dirPath, { withFileTypes: true }).then(entries =>
  47. entries.map(entry => {
  48. const childPath = path.join(dirPath, entry.name);
  49. return entry.isDirectory() ? walk(childPath) : childPath;
  50. }),
  51. ),
  52. );
  53. }
  54. function personage(displayName, longName, href, ty) {
  55. if (ty && !PERSONAGE_TYPES.has(ty)) {
  56. console.error("Bad personage type: " + ty);
  57. throw "";
  58. }
  59. const b = `<b class="personage"${
  60. ty ? ' itemscope itemtype="https://schema.org/' + ty + '"' : ""
  61. }>`;
  62. const a = href ? `<a${ty ? ' itemprop="url"' : ""} href="${href}">` : "";
  63. const inner = (() => {
  64. if (ty) {
  65. if (longName) {
  66. return (
  67. `<abbr title="${longName}" itemprop="name">${displayName}</abbr>` +
  68. `<meta itemprop="alternateName" content="${longName}" />`
  69. );
  70. }
  71. return `<span itemprop="name">${displayName}</span>`;
  72. }
  73. if (longName) {
  74. return `<abbr title="${longName}">${displayName}</abbr>`;
  75. }
  76. return displayName;
  77. })();
  78. return `${b}${a}${inner}${a ? "</a>" : ""}</b>`;
  79. }
  80. function entryTypeToItemType(entryTy) {
  81. const itemTy =
  82. ENTRY_TO_ITEM.get(entryTy) ??
  83. (() => {
  84. console.error("Bad bib entry type: " + entryTy);
  85. throw "";
  86. })();
  87. return `https://schema.org/${itemTy}`;
  88. }
  89. function htmlEsc(s) {
  90. return s.replace(/[&"'<>]/g, c => HTML_ESC_LUT.get(c));
  91. }
  92. function makeHeading(content, id, level) {
  93. if (!id) {
  94. const $ = cheerio.load(content, null, false);
  95. id = slugify($.text());
  96. }
  97. const labelText = "#".repeat(level);
  98. return (
  99. '<div class="heading-wrapper">' +
  100. `<h${level} aria-labelledby="${id}--LABEL" id="${id}">` +
  101. content +
  102. `</h${level}>` +
  103. `<a id="${id}--LABEL" class="h${level}-label heading-label" href="#${id}">` +
  104. labelText +
  105. "</a>" +
  106. "</div>"
  107. );
  108. }
  109. function replaceHyphenMinuses(elem) {
  110. if (elem.type === "text") {
  111. elem.data = elem.data.replaceAll("-", "\u2010");
  112. }
  113. const title = elem.attribs?.title;
  114. if (title) {
  115. elem.attribs.title = title.replaceAll("-", "\u2010");
  116. }
  117. if (elem.children) {
  118. for (const child of elem.children) {
  119. replaceHyphenMinuses(child);
  120. }
  121. }
  122. }
  123. function replaceGs(elem) {
  124. if (elem.type === "text") {
  125. elem.data = elem.data.replaceAll("g", "\u0261");
  126. }
  127. if (elem.children) {
  128. for (const child of elem.children) {
  129. replaceGs(child);
  130. }
  131. }
  132. }
  133. function srcToReqPath(thiz, src) {
  134. return path.resolve(thiz.page.url, src);
  135. }
  136. function srcToFsPath(thiz, src) {
  137. return path.join(thiz.eleventy.env.root, "src", srcToReqPath(thiz, src));
  138. }
  139. async function makeImg(thiz, src, alt, detId) {
  140. const fsPath = srcToFsPath(thiz, src);
  141. const reqPath = srcToReqPath(thiz, src);
  142. const { width, height, hasAlpha } = await sharp(fsPath).metadata();
  143. if (!width || !height) {
  144. console.error(
  145. `Bad dimensions for ${fsPath}: ${width}\u{00d7}${height}`,
  146. );
  147. throw "";
  148. }
  149. const ly = src.endsWith(".ly.svg");
  150. const imgClass = hasAlpha
  151. ? `class="has-transparency${ly ? " ly-img" : ""}"`
  152. : "";
  153. const aClasses = (ly ? " ly-img-a" : "") + (detId ? " has-det" : "");
  154. const ariaDetails = detId ? `aria-details="${detId}"` : "";
  155. const wh = ly ? "" : `width="${width}" height="${height}"`;
  156. return (
  157. `<a href="${reqPath}" class="img-a${aClasses}">` +
  158. `<img ${ariaDetails} alt="${alt}" loading="lazy" decoding="async" ${wh} ${imgClass} src="${reqPath}">` +
  159. "</a>"
  160. );
  161. }
  162. async function imgDet(
  163. thiz,
  164. content,
  165. src,
  166. alt,
  167. summary = "Transcription of the above image",
  168. ) {
  169. const id = slugify(src) + "--DET";
  170. const img = await makeImg(thiz, src, alt, id);
  171. return (
  172. `${img}<details id="${id}" class="det">` +
  173. `<summary>${summary}</summary>${content}` +
  174. "</details>"
  175. );
  176. }
  177. async function makeLy(thiz, srcBase, alt) {
  178. const lyReqPath = srcToReqPath(thiz, `${srcBase}.ly`);
  179. const midReqPath = srcToReqPath(thiz, `${srcBase}.ly.mid`);
  180. const opusReqPath = srcToReqPath(thiz, `${srcBase}.ly.mid.opus`);
  181. const detContent =
  182. `<div class="ly-det-content"><audio preload="metadata" controls src="${opusReqPath}"></audio>` +
  183. `<div class="ly-det-a-container"><a class="ly-src" href="${lyReqPath}">LilyPond source</a>` +
  184. `<a class="midi-src" type="audio/midi" href="${midReqPath}">MIDI file (<abbr title="standard MIDI file">SMF</abbr>)</a></div></div>`;
  185. return await imgDet(
  186. thiz,
  187. detContent,
  188. `${srcBase}.ly.svg`,
  189. alt,
  190. 'Playable <abbr title="Musical Instrument Digital Interface">MIDI</abbr> rendering and LilyPond source',
  191. );
  192. }
  193. async function buildLy(inputDir) {
  194. const postDir = path.join(inputDir, "post/");
  195. const paths = (await walk(postDir)).flat(Number.POSITIVE_INFINITY);
  196. await Promise.all(
  197. paths
  198. .filter(p => p.endsWith(".ly"))
  199. .map(lyPath =>
  200. exec(
  201. `lilypond --loglevel=WARNING -dpoint-and-click=#f -ddelete-intermediate-files -dmidi-extension=mid --svg -o${lyPath} ${lyPath}`,
  202. ).then(out =>
  203. Promise.all([
  204. (async () =>
  205. out?.stderr?.trim()
  206. ? console.warn(out.stderr)
  207. : undefined)(),
  208. (async () => {
  209. const svgPath = `${lyPath}.svg`;
  210. const s = await fs.readFile(svgPath, {
  211. encoding: "utf8",
  212. });
  213. await fs.writeFile(
  214. svgPath,
  215. s.replaceAll(
  216. 'font-family="serif"',
  217. "font-family=\"'C059',serif\"",
  218. ),
  219. {
  220. mode: 0o664,
  221. },
  222. );
  223. await exec(
  224. `inkscape --export-type=svg --vacuum-defs -l -D -T --export-area-snap --export-overwrite ${svgPath}`,
  225. );
  226. const svgStr = await fs.readFile(svgPath, {
  227. encoding: "utf8",
  228. });
  229. const $ = cheerio.load(svgStr, {
  230. xml: true,
  231. });
  232. $("svg").attr("color", "#000000");
  233. $("svg").removeAttr("width");
  234. $("svg").removeAttr("height");
  235. const svgStrColored = $.xml();
  236. const res = optimize(svgStrColored, {
  237. multipass: true,
  238. path: svgPath,
  239. });
  240. await fs.writeFile(
  241. svgPath,
  242. res.data.replaceAll(
  243. 'font-family="serif"',
  244. "font-family=\"'Gentium Plus',serif\"",
  245. ),
  246. {
  247. mode: 0o664,
  248. },
  249. );
  250. })(),
  251. exec(
  252. `fluidsynth -i -q -C0 -R0 -K16 -Twav -Os16 -F ${lyPath}.mid.wav ${path.join(
  253. inputDir,
  254. "..",
  255. "scratch",
  256. "GeneralUser_GS_v2.0.0.sf2",
  257. )} ${lyPath}.mid`,
  258. )
  259. .then(() =>
  260. exec(
  261. `opusenc --quiet --bitrate 128 --vbr --comp 10 --discard-comments --discard-pictures --padding 0 ${lyPath}.mid.wav ${lyPath}.mid.opus`,
  262. ),
  263. )
  264. .then(() => fs.rm(`${lyPath}.mid.wav`)),
  265. ]),
  266. ),
  267. ),
  268. );
  269. }
  270. async function parseLibrary(inputPath) {
  271. const bibText = await fs.readFile(`${path.dirname(inputPath)}/refs.bib`, {
  272. encoding: "utf-8",
  273. });
  274. return bib.parseAsync(bibText, { english: false });
  275. }
  276. async function getCiteIdByKey(inputPath, key) {
  277. const lib = await parseLibrary(inputPath);
  278. const entry = lib.entries.find(entry => key === entry.key);
  279. if (!entry) {
  280. console.error(`Bad cite key: "${key}". Input path: ${inputPath}\n`);
  281. throw "";
  282. }
  283. return getCiteId(entry.fields);
  284. }
  285. function joinNames(xs, surnames) {
  286. if (!xs) {
  287. return;
  288. }
  289. const filtered = surnames
  290. ? xs.flatMap(a => (a.lastName ? [a.lastName] : []))
  291. : xs.filter(s => s);
  292. if (filtered.length === 1) {
  293. return filtered[0].slice(0, 3);
  294. }
  295. if (filtered.length >= 2 && filtered.length <= 4) {
  296. return filtered.reduce((accu, name) => accu + name.slice(0, 1), "");
  297. }
  298. if (filtered.length > 0) {
  299. return (
  300. filtered
  301. .slice(0, 3)
  302. .reduce((accu, name) => accu + name.slice(0, 1), "") + "+"
  303. );
  304. }
  305. return;
  306. }
  307. function getCiteId(fields) {
  308. const alpha = (() => {
  309. const authorJoined = joinNames(fields.author, true);
  310. if (authorJoined) {
  311. return authorJoined;
  312. }
  313. const bookauthorJoined = joinNames(fields.bookauthor, true);
  314. if (bookauthorJoined) {
  315. return bookauthorJoined;
  316. }
  317. const editorJoined = joinNames(fields.editor, true);
  318. if (editorJoined) {
  319. return editorJoined;
  320. }
  321. const editorsJoined = joinNames(fields.editors, true);
  322. if (editorsJoined) {
  323. return editorsJoined;
  324. }
  325. const editoraJoined = joinNames(fields.editora, true);
  326. if (editoraJoined) {
  327. return editoraJoined;
  328. }
  329. const editorbJoined = joinNames(fields.editorb, true);
  330. if (editorbJoined) {
  331. return editorbJoined;
  332. }
  333. const scriptwriterJoined = joinNames(fields.scriptwriter, true);
  334. if (scriptwriterJoined) {
  335. return scriptwriterJoined;
  336. }
  337. const directorJoined = joinNames(fields.director, true);
  338. if (directorJoined) {
  339. return directorJoined;
  340. }
  341. const commentatorJoined = joinNames(fields.commentator, true);
  342. if (commentatorJoined) {
  343. return commentatorJoined;
  344. }
  345. const collaboratorJoined = joinNames(fields.collaborator, true);
  346. if (collaboratorJoined) {
  347. return collaboratorJoined;
  348. }
  349. const organizationJoined = joinNames(fields.organization);
  350. if (organizationJoined) {
  351. return organizationJoined;
  352. }
  353. const institutionJoined = joinNames(fields.institution);
  354. if (institutionJoined) {
  355. return institutionJoined;
  356. }
  357. const translatorJoined = joinNames(fields.translator, true);
  358. if (translatorJoined) {
  359. return translatorJoined;
  360. }
  361. return fields.title.slice(0, 3);
  362. })();
  363. const num = (() => {
  364. NUM_RE.lastIndex = 0;
  365. const e = NUM_RE.exec(fields.year);
  366. if (e) {
  367. const matched = e[0];
  368. return e[1] + matched.slice(matched.length - 2);
  369. }
  370. return fields.year.slice(fields.year.length - 2);
  371. })();
  372. return alpha + num;
  373. }
  374. const SCRIPT_RE = /^[A-Z]{4}$/;
  375. const REGION_RE = /^([A-Z]{2}|\d{3})$/;
  376. const VAR_RE = /^([A-Z\d]{5,8}|\d[A-Z\d]{3})$/;
  377. const PRIMARIES = new Map([
  378. ["ANG", "Old English"],
  379. ["CA", "Catalan"],
  380. ["CMN", "Mandarin"],
  381. ["DE", "High German"],
  382. ["EN", "English"],
  383. ["FR", "French"],
  384. ["GRC", "Ancient Greek"],
  385. ["JA", "Japanese"],
  386. ["SIT", "Proto-Sino-Tibetan"],
  387. ["SV", "Swedish"],
  388. ["UND", "[unspecified]"],
  389. ]);
  390. const SCRIPTS = new Map([
  391. ["BOPO", "Bopomofo (Zhùyīn)"],
  392. ["HANS", "simplified Sinographs"],
  393. ["HANT", "traditional Sinographs"],
  394. ["LATN", "the Latin alphabet"],
  395. ]);
  396. const REGIONS = new Map([]);
  397. const VARS = new Map([
  398. [
  399. "ALALC97",
  400. "American Library Association \u2013 Library of Congress Romanization",
  401. ],
  402. ["FONIPA", "International Phonetic Alphabet"],
  403. ["HEPBURN", "Hepburn Romanization"],
  404. ["PINYIN", "Hànyǔ Pīnyīn Romanization"],
  405. ]);
  406. function langToTitle(lang) {
  407. const tags = lang.toUpperCase().split("-");
  408. if (tags.length < 1) {
  409. return;
  410. }
  411. const primary = tags[0];
  412. if (primary === "EN" && tags.length === 1) {
  413. return;
  414. }
  415. const bail = () => {
  416. console.error("Bad `lang`:", lang);
  417. throw "";
  418. };
  419. let priv = false;
  420. let primaryText, script, region;
  421. const vars = [];
  422. for (const tag of tags.slice(1)) {
  423. if (priv) {
  424. switch (tag) {
  425. case "MSM": {
  426. if (primary !== "CMN") {
  427. bail();
  428. }
  429. primaryText = "Standard Mandarin";
  430. break;
  431. }
  432. default:
  433. bail();
  434. }
  435. continue;
  436. }
  437. if (SCRIPT_RE.test(tag)) {
  438. script = script ? bail() : tag;
  439. } else if (REGION_RE.test(tag)) {
  440. region = region ? bail() : tag;
  441. } else if (VAR_RE.test(tag)) {
  442. vars.push(tag);
  443. } else if (tag === "X") {
  444. priv = true;
  445. } else {
  446. bail();
  447. }
  448. }
  449. const defaultPrimaryText = PRIMARIES.get(primary) ?? bail();
  450. if (!primaryText) {
  451. primaryText = defaultPrimaryText;
  452. }
  453. const ipaIx = vars.indexOf("FONIPA");
  454. if (ipaIx !== -1) {
  455. vars.splice(ipaIx, 1);
  456. }
  457. let text =
  458. ipaIx === -1
  459. ? `${primaryText} text`
  460. : `phonetic transcription: ${primaryText} speech`;
  461. if (ipaIx !== -1 && primary === "UND") {
  462. return "phonetic transcription";
  463. }
  464. if (region) {
  465. const regionText = REGIONS.get(region) ?? bail();
  466. text += `, as spoken in ${regionText}`;
  467. }
  468. if (script) {
  469. const scriptText = SCRIPTS.get(script) ?? bail();
  470. text += `, written in ${scriptText}`;
  471. }
  472. for (const variant of vars) {
  473. const varText = VARS.get(variant) ?? bail();
  474. text += `, ${varText}`;
  475. }
  476. return text.replaceAll("-", "\u2010");
  477. }
  478. /** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
  479. export default function (eleventyConfig) {
  480. eleventyConfig.setUseGitIgnore(false);
  481. eleventyConfig.addFilter("posts", pages =>
  482. pages.filter(page => page.url.startsWith("/post/")),
  483. );
  484. eleventyConfig.addFilter("preview", post => {
  485. const $ = cheerio.load(
  486. post.content.split("<!-- __END_PREVIEW -->")[0],
  487. null,
  488. false,
  489. );
  490. const url = post.url + (post.url.endsWith("/") ? "" : "/");
  491. const anchors = $("a");
  492. for (const a of anchors) {
  493. const href = $(a).attr("href");
  494. if (href.startsWith("#")) {
  495. $(a).attr("href", url + href);
  496. }
  497. }
  498. return $.html();
  499. });
  500. eleventyConfig.addFilter("withtags", (pages, tagList) => {
  501. const tags = new Set(tagList);
  502. return pages.filter(page => page.data.tags.some(tag => tags.has(tag)));
  503. });
  504. eleventyConfig.addFilter("getNewestCollectionItemDate", collection =>
  505. collection.reduce(
  506. (newestDate, post) =>
  507. post.data.date > newestDate ? post.data.date : newestDate,
  508. "1970-01-01",
  509. ),
  510. );
  511. eleventyConfig.addFilter("htmlBaseUrl", (relativeUrl, baseUrl) => {
  512. if (!relativeUrl) {
  513. return baseUrl;
  514. }
  515. while (relativeUrl[0] === "/" || relativeUrl[0] === ".") {
  516. relativeUrl = relativeUrl.slice(1);
  517. if (!relativeUrl) {
  518. return baseUrl;
  519. }
  520. }
  521. while (baseUrl.at(-1) === "/") {
  522. baseUrl = baseUrl.slice(0, baseUrl.length - 1);
  523. }
  524. return `${baseUrl}/${relativeUrl}`;
  525. });
  526. eleventyConfig.addFilter(
  527. "atomRender",
  528. (content, baseUrl, absolutePostUrl) =>
  529. `<base href="${baseUrl}" />` +
  530. content.split("<!-- __END_PREVIEW -->")[0].trim() +
  531. `<footer>[<a href="${absolutePostUrl}"><i>&hellip;Read the full post here.</i></a>]</footer>`,
  532. );
  533. eleventyConfig.addPassthroughCopy("src/css");
  534. eleventyConfig.addPassthroughCopy("src/fonts");
  535. eleventyConfig.addPassthroughCopy("src/img");
  536. eleventyConfig.addPassthroughCopy("src/js/*.js");
  537. eleventyConfig.addPassthroughCopy("src/**/*.avif");
  538. eleventyConfig.addPassthroughCopy("src/**/*.webp");
  539. eleventyConfig.addPassthroughCopy("src/**/*.jpeg");
  540. eleventyConfig.addPassthroughCopy("src/**/*.svg");
  541. eleventyConfig.addPassthroughCopy("src/**/*.ly");
  542. eleventyConfig.addPassthroughCopy("src/**/*.mid");
  543. eleventyConfig.addPassthroughCopy("src/**/*.opus");
  544. eleventyConfig.addWatchTarget("src/fonts");
  545. eleventyConfig.addWatchTarget("src/fonts/**/*");
  546. eleventyConfig.addWatchTarget("src/js/*.ts");
  547. eleventyConfig.addWatchTarget("src/js/*.json");
  548. eleventyConfig.addWatchTarget("src/**/*.bib");
  549. eleventyConfig.addWatchTarget("src/**/*.avif");
  550. eleventyConfig.addWatchTarget("src/**/*.webp");
  551. eleventyConfig.addWatchTarget("src/**/*.jpeg");
  552. eleventyConfig.addWatchTarget("src/**/*.svg");
  553. eleventyConfig.addWatchTarget("src/**/*.ly");
  554. eleventyConfig.addWatchTarget("src/**/*.mid");
  555. eleventyConfig.addWatchTarget("src/**/*.opus");
  556. eleventyConfig.watchIgnores.add("src/tag/**/*");
  557. eleventyConfig.watchIgnores.add("src/post/**/*.ly.*");
  558. eleventyConfig.addPlugin(syntaxHighlight);
  559. eleventyConfig.on("eleventy.before", async ({ incremental, inputDir }) => {
  560. if (incremental) {
  561. return;
  562. }
  563. const jsInputDir = path.join(inputDir, "js/");
  564. const out = await exec(`npx tsc -p ${jsInputDir}`).catch(e =>
  565. console.error(e),
  566. );
  567. if (out?.stderr?.trim()) {
  568. console.error(out.stderr);
  569. }
  570. });
  571. eleventyConfig.on("eleventy.before", async ({ incremental, inputDir }) =>
  572. incremental ? undefined : await buildLy(inputDir),
  573. );
  574. const tagPaths = new Map();
  575. eleventyConfig.on("eleventy.before", async ({ inputDir }) => {
  576. const jsonText = await fs.readFile(path.join(inputDir, "tags.json"), {
  577. encoding: "utf8",
  578. });
  579. const tagData = JSON.parse(jsonText);
  580. const tagPath = path.join(inputDir, "tag/");
  581. await fs.rm(tagPath, { force: true, recursive: true });
  582. const writeTemplate = async (predecessors, prefix, vertex) => {
  583. const isRoot = vertex.tag === "post";
  584. const currPath = isRoot
  585. ? prefix
  586. : path.join(prefix, slugify(vertex.tag) + "/");
  587. tagPaths.set(vertex.tag, currPath.split("src", 2)[1]);
  588. const predecessorsIncl = isRoot
  589. ? []
  590. : predecessors.concat([vertex.tag]);
  591. const descendants = (
  592. await Promise.all(
  593. vertex.children.map(vert =>
  594. writeTemplate(predecessorsIncl, currPath, vert),
  595. ),
  596. )
  597. ).flat();
  598. descendants.unshift(vertex.tag);
  599. let list = `<ul><li><a ${
  600. isRoot ? 'aria-current="page"' : ""
  601. } href="/tag/" aria-label="root of the hierarchy">&radic;</a><ul>`;
  602. for (const predecessor of predecessors) {
  603. list += `<li><a href="${tagPaths.get(
  604. predecessor,
  605. )}" rel="tag">${predecessor}</a><ul>`;
  606. }
  607. if (!isRoot) {
  608. list += `<li><a aria-current="page" href="${tagPaths.get(
  609. vertex.tag,
  610. )}" rel="tag">${vertex.tag}</a><ul>`;
  611. }
  612. for (const child of vertex.children) {
  613. list += `<li><a href="${tagPaths.get(child.tag)}" rel="tag">${
  614. child.tag
  615. }</a></li>`;
  616. }
  617. list += "</ul>".repeat(predecessors.length + 3);
  618. const hierarchyH1 = isRoot
  619. ? '{% h1 "hierarchy-of-tags" %}Hierarchy of tags{% endh1 %}'
  620. : `{% h1 "hierarchy-of-tags" %}Hierarchy of tags directly related to <b class="tag-name">${vertex.tag}</b>{% endh1 %}`;
  621. const indexTemplate =
  622. `---
  623. {
  624. "layout": "base.njk",
  625. "title": "tag: \\u201c${vertex.tag}\\u201d",
  626. "curr": "tags",
  627. "eleventyExcludeFromCollections": true,
  628. "noendnotes": true
  629. }
  630. ---
  631. {% set anchorRe = r/<a(\\s|>)/i %}
  632. {% set tagarr = [${descendants.map(d => '"' + d + '"').join(", ")}] %}
  633. <section aria-labelledby="hierarchy-of-tags" class="tag-hierarchy">
  634. ${hierarchyH1}
  635. ${list}
  636. </section>
  637. ` +
  638. (isRoot
  639. ? ""
  640. : `
  641. <section role="feed"
  642. aria-labelledby="posts-classified-under-this-tag"
  643. aria-busy="false"
  644. class="post-list"
  645. itemscope
  646. itemtype="https://schema.org/DataFeed"
  647. >
  648. {% h1 "posts-classified-under-this-tag" %}Posts classified under <b class="tag-name">${vertex.tag}</b>{% endh1 %}
  649. {%- for peaust in collections.all | posts | withtags(tagarr) | datesort -%}
  650. {%- set post_slug = peaust.data.title | slugify -%}
  651. {%- set post_id = post_slug ~ "--HEAD" -%}
  652. {%- set post_info_id = post_slug ~ "--INFO" -%}
  653. <article aria-labelledby="{{ post_id }}"
  654. aria-describedby="{{ post_info_id }}"
  655. aria-posinset="{{ loop.index }}"
  656. aria-setsize="{{ loop.length }}"
  657. class="post-preview"
  658. itemprop="dataFeedElement"
  659. itemscope
  660. itemtype="https://schema.org/BlogPosting"
  661. >
  662. <header>
  663. {#--#}
  664. <h2 id="{{ post_id }}">
  665. {#--#}
  666. <a href="{{ peaust.url }}" itemprop="url"><span itemprop="name headline">
  667. {%- if peaust.data.titleHeading -%}
  668. {%- if anchorRe.test(peaust.data.titleHeading) -%}
  669. {{- peaust.data.title -}}
  670. {%- else -%}
  671. {{- peaust.data.titleHeading | safe -}}
  672. {%- endif -%}
  673. {%- else -%}
  674. {{- peaust.data.title -}}
  675. {%- endif -%}
  676. </span></a>
  677. {#--#}
  678. </h2>
  679. <div id="{{ post_info_id }}" class="post-info">
  680. {%- set d8 = peaust.data.date -%}
  681. <time itemprop="datePublished" datetime="{{ d8 }}">{{ d8 }}</time>
  682. {#--#}
  683. <span class="tags-listing">
  684. {#--#}
  685. <span class="tags-colon">Tags:&nbsp;</span>
  686. {#--#}
  687. <span class="tags-list">
  688. {%- for tag in peaust.data.tags -%}
  689. <a href="{{ tag | tagpath }}" rel="tag" itemprop="keywords">{{ tag }}</a>
  690. {%- if not loop.last -%}
  691. ,
  692. {% endif -%}
  693. {%- endfor -%}
  694. </span>
  695. {#--#}
  696. </span>
  697. {#--#}
  698. </div>
  699. {%- if peaust.data.series -%}
  700. <div class="assoc-series">
  701. {#--#}
  702. <span class="part-of-a-series">Part of a series:</span>
  703. <cite itemprop="isPartOf" itemscope itemtype="https://schema.org/CreativeWork">
  704. {%- series peaust.data.series, false, true -%}
  705. </cite>
  706. {#--#}
  707. </div>
  708. {%- endif -%}
  709. </header>
  710. {#--#}
  711. <div itemprop="abstract">
  712. {{- peaust.content.split("<!-- __END_PREVIEW -->")[0] | safe -}}
  713. </div>
  714. {#--#}
  715. <footer>
  716. <a href="{{ peaust.url }}" itemprop="url">&hellip;Read the full post&nbsp;&rarr;</a>
  717. {#--#}
  718. </footer>
  719. </article>
  720. {%- endfor -%}
  721. </section>
  722. `);
  723. await fs.mkdir(currPath, { recursive: true });
  724. await fs.writeFile(
  725. path.join(currPath, "index.njk"),
  726. indexTemplate,
  727. {
  728. encoding: "utf8",
  729. mode: 0o664,
  730. },
  731. );
  732. return descendants;
  733. };
  734. await writeTemplate([], tagPath, tagData);
  735. });
  736. eleventyConfig.addFilter("tagpath", tag => tagPaths.get(tag));
  737. eleventyConfig.addFilter("tagterm", tag => {
  738. const p = tagPaths.get(tag);
  739. if (!p) {
  740. console.error("Cannot find tag: " + tag);
  741. throw "";
  742. }
  743. return p
  744. .split("/")
  745. .filter(s => s)
  746. .slice(1)
  747. .join("/");
  748. });
  749. eleventyConfig.addFilter("datesort", (posts, rev) =>
  750. posts.sort(
  751. (p1, p2) =>
  752. (rev ? -1 : 1) * (p1.data.date > p2.data.date ? -1 : 1),
  753. ),
  754. );
  755. for (const level of [1, 2, 3, 4, 5, 6]) {
  756. eleventyConfig.addPairedShortcode(`h${level}`, (content, id) =>
  757. makeHeading(content, id, level),
  758. );
  759. }
  760. const HEADING_RE =
  761. /\{%[\s\r\n]*h[1-6][\s\r\n]*"([a-zA-Z0-9_\-]+)"[\s\r\n]*%\}[\s\r\n]*(.+?)[\s\r\n]*\{%[\s\r\n]*endh[1-6][\s\r\n]*%\}/gms;
  762. eleventyConfig.addShortcode("headingref", function (id, slug) {
  763. const rawInp = slug
  764. ? this.ctx.collections.all.find(p => p.page.fileSlug === slug).page
  765. .rawInput
  766. : this.page.rawInput;
  767. for (const [, matchedId, content] of rawInp.matchAll(HEADING_RE)) {
  768. if (id === matchedId) {
  769. return `&ldquo;<a href="${
  770. slug ? "/post/" + slug + "/" : ""
  771. }#${id}">${content}</a>&rdquo;`;
  772. }
  773. }
  774. console.error("Can't find heading with id", id, "and slug", slug);
  775. throw "";
  776. });
  777. eleventyConfig.addShortcode("snip", () => "<!-- __END_PREVIEW -->");
  778. eleventyConfig.addShortcode("post", function (slug) {
  779. const post = this.ctx.collections.all.find(
  780. p => p.page.fileSlug === slug,
  781. );
  782. if (!post) {
  783. console.error("No post with the slug ", slug);
  784. throw "";
  785. }
  786. const headline =
  787. post.data.titleHeading && !ANCHOR_RE.test(post.data.titleHeading)
  788. ? post.data.titleHeading
  789. : htmlEsc(post.data.title);
  790. return (
  791. `<cite itemscope itemtype="https://schema.org/BlogPosting">` +
  792. `<a itemprop="url" href="/post/${slug}">` +
  793. `<span itemprop="name headline">${headline}</span></a></cite>`
  794. );
  795. });
  796. eleventyConfig.addShortcode("tag", (tag, text) => {
  797. const p = tagPaths.get(tag);
  798. if (!p) {
  799. console.error("Unrecognized tag: " + tag);
  800. throw "";
  801. }
  802. return `<a href="${p}" rel="tag">${text ? text : htmlEsc(tag)}</a>`;
  803. });
  804. eleventyConfig.addShortcode(
  805. "series",
  806. (series, text, itemprop) =>
  807. `<a href="/series/#${slugify(series)}"${
  808. itemprop ? ' itemprop="url"' : ""
  809. }><span itemprop="name headline">${
  810. text ? text : htmlEsc(series)
  811. }</span></a>`,
  812. );
  813. eleventyConfig.addPairedShortcode("ipa", (transc, lang = "und", level) => {
  814. const [delimL, delimR] = (() => {
  815. if (!level) {
  816. return ["/", "/"];
  817. }
  818. switch (level) {
  819. case "none":
  820. return ["", ""];
  821. case "phone":
  822. return ["[", "]"];
  823. case "morph":
  824. return ["&parsl;", "&parsl;"];
  825. default: {
  826. console.error("Unrecognized transcription level:", level);
  827. throw "";
  828. }
  829. }
  830. })();
  831. const langSplit = lang.split("-x-", 2);
  832. const langAttr = `lang="${langSplit[0]}-fonipa${
  833. langSplit.length > 1 ? "-x-" + langSplit[1] : ""
  834. }"`;
  835. const transcSegs = transc
  836. .split(WS_RE)
  837. .map(s => {
  838. const trimmed = s.trim();
  839. if (!trimmed) {
  840. return "";
  841. }
  842. const $ = cheerio.load(
  843. `<span id="--DUMMY--ID--">${trimmed}</span>`,
  844. null,
  845. false,
  846. );
  847. replaceGs($("#--DUMMY--ID--")[0]);
  848. return $("#--DUMMY--ID--").html();
  849. })
  850. .filter(s => s);
  851. if (transcSegs.length < 1) {
  852. console.error("Empty transcription:", transc, lang, level);
  853. throw "";
  854. }
  855. return transcSegs.length === 1
  856. ? `<span ${langAttr} class="no-wrap">${delimL}${transcSegs[0]}${delimR}</span>`
  857. : `<span ${langAttr}><span class="no-wrap">${delimL}${transcSegs.join(
  858. '</span> <span class="no-wrap">',
  859. )}${delimR}</span></span>`;
  860. });
  861. const C_TIME_RE = /^c\/?$/;
  862. const STD_TIME_RE =
  863. /^(?<num>[1-9][0-9]*(\.[0-9]+)?)\/(?<denom>[1-9][0-9]*(\.[0-9]+)?)$/;
  864. const ADD_TIME_RE = /^(?<num>[1-9](\+[1-9])+)\/(?<denom>[1-9][0-9]*)$/;
  865. const FORM_RE = /^([A-Z]'*([1-9][0-9]*)?)+$/;
  866. const FORM_SECT_RE_G = /([A-Z])('*)([1-9][0-9]*)?/g;
  867. const PITCH_RE = /^([A-G])(b{1,2}|#|x|n)?(\-?[1-9][0-9]*)$/;
  868. const PC_RE = /^([A-G])(b{1,2}|#|x|n)?$/;
  869. const PC_RE_G = /([A-G])(b{1,2}|#|x|n)?/g;
  870. const PC_COLLECT_RE =
  871. /^([\{\[])([A-G](b{1,2}|#|x|n)?\s*,\s*)*[A-G](b{1,2}|#|x|n)?,?[\]\}]$/;
  872. const PC_PROG_RE =
  873. /^([A-G](b{1,2}|#|x|n)?\s*\-\-\s*)*[A-G](b{1,2}|#|x|n)?$/;
  874. const DEG_RE = /^(b{1,2}|#|x|n)?([1-9])\^$/;
  875. const INT_RE_G = /[0-9]{1,2}/g;
  876. const INT_COLLECT_RE = /^([\{\[])([0-9]{1,2}\s*,\s*)*[0-9]{1,2},?[\]\}]$/;
  877. const FORTE_RE = /^([1-9][0-9]?\-Z?[1-9][0-9]?)([AB])?$/;
  878. const NAMED_INTERVAL_RE = /^([PMmAdT])([1-9][0-9]*|T)$/;
  879. const CENTS_RE = /^([+\-]?[0-9]+(\.[0-9]+)?)c$/;
  880. const CPATH_RE = /^<([0-9]+(\s+[0-9]+)*)>$/;
  881. const CPATH_SEG_RE_G = /[0-9]+/g;
  882. const CHORD_FACT_RE = /^(b{1,2}|#|x|n)?(1?[0-9])$/;
  883. const CHORD_RE =
  884. /^(?<letter>[A-G])(?<acci>b{1,2}|#|x|n)?(?<qual>\+\-|\-|\+|\|\-\||\+maj|maj|sus2|sus4|o\/?|alt|5)?(?<ext>add2|add4|6|7|9|11|13)?(?<alts>((#|b)(5|9|11|13))*)(no(?<omitted>[135]))?(\/(?<slash>[A-G](b{1,2}|#|x|n)?)|inv(?<inv>[1-6]))?$/;
  885. const CHORD_PROG_INIT_RE =
  886. /^(?<letter>[A-G])(?<acci>b{1,2}|#|x|n)?(?<qual>\+\-|\-|\+|\|\-\||\+maj|maj|sus2|sus4|o\/?|alt|5)?(?<ext>add2|add4|6|7|9|11|13)?(?<alts>((#|b)(5|9|11|13))*)(no(?<omitted>[135]))?(\/(?<slash>[A-G](b{1,2}|#|x|n)?)|inv(?<inv>[1-6]))?\-\-.+$/;
  887. const RNA_RE =
  888. /^(?<acci>b|#)?(?<roman>i|I|ii|II|iii|III|iv|IV|v|V|vi|VI|vii|VII)(?<qual>\+\-|\+|\|\-\||\+maj|maj|sus2|sus4|o\/?|alt|5)?(?<ext>6|7|9|11|13)?(?<alts>((#|b)(5|9|11|13))*)(no(?<omitted>[135]))?(inv(?<inv>[1-6]))?(\/(?<slashAcci>b|#)?(?<slash>i|I|ii|II|iii|III|iv|IV|v|V|vi|VI|vii|VII|[1-7]))?$/;
  889. const RNA_PROG_INIT_RE =
  890. /^(?<acci>b|#)?(?<roman>i|I|ii|II|iii|III|iv|IV|v|V|vi|VI|vii|VII)(?<qual>\+\-|\+|\|\-\||\+maj|maj|sus2|sus4|o\/?|alt|5)?(?<ext>6|7|9|11|13)?(?<alts>((#|b)(5|9|11|13))*)(no(?<omitted>[135]))?(inv(?<inv>[1-6]))?(\/(?<slashAcci>b|#)?(?<slash>i|I|ii|II|iii|III|iv|IV|v|V|vi|VI|vii|VII|[1-7]))?\-\-.+$/;
  891. const ALT_RE_G = /(#|b)(5|9|11|13)/g;
  892. const DYN_RE =
  893. /^((?<mezzo>m[pf])|(?<piano>s?p+)|(?<forte>s?f+)|fp|(?<forz>s?fz)|(?<rein>rfz?))$/;
  894. const NOTES_RE =
  895. /^(\[)?([rn][1-9][0-9]{0,2}\.?>?\s+)*[rn][1-9][0-9]{0,2}\.?>?\]?$/;
  896. const NOTE_RE_G = /([rn])([1-9][0-9]{0,2})(\.)?(>)?/g;
  897. const MOTION_RE = /^([\[\{])([cosip][1-9]?)+[\]\}]$/;
  898. const MOTION_RE_G = /([cosip])([1-9])?/g;
  899. const ICV_RE =
  900. /^<([0-9te])([0-9te])([0-9te])([0-9te])([0-9te])([0-9te])>$/;
  901. const ARROW_RE =
  902. /^(?<deltas>((-[1-9][0-9]*|0|\+[1-9][0-9]*),)*(-[1-9][0-9]*|0|\+[1-9][0-9]*),?)?\|(?<motions>(?<curly>\{)?([cosip][1-9]?)+\}?)?$/;
  903. const DELTA_RE_G = /(-[1-9][0-9]*)|(0)|(\+[1-9][0-9]*)/g;
  904. const FORTE_LUT = new Map([
  905. ["3-1", [0, 1, 2]],
  906. ["3-2", [0, 1, 3]],
  907. ["3-2B", [0, 2, 3]],
  908. ["3-4", [0, 1, 5]],
  909. ["3-4B", [0, 4, 5]],
  910. ["3-5", [0, 1, 6]],
  911. ["3-5B", [0, 5, 6]],
  912. ["3-6", [0, 2, 4]],
  913. ["3-7", [0, 2, 5]],
  914. ["3-7B", [0, 3, 5]],
  915. ["3-8", [0, 2, 6]],
  916. ["3-8B", [0, 4, 6]],
  917. ["3-9", [0, 2, 7]],
  918. ["3-10", [0, 3, 6]],
  919. ["3-11", [0, 3, 7]],
  920. ["3-11B", [0, 4, 7]],
  921. ["3-12", [0, 4, 8]],
  922. ["4-10", [0, 2, 3, 5]],
  923. ["4-11", [0, 1, 3, 5]],
  924. ["4-11B", [0, 2, 4, 5]],
  925. ["4-16", [0, 1, 5, 7]],
  926. ["4-16B", [0, 2, 6, 7]],
  927. ["4-23", [0, 2, 5, 7]],
  928. ["4-Z29", [0, 1, 3, 7]],
  929. ["4-Z29B", [0, 4, 6, 7]],
  930. ]);
  931. const DYN_LUT = new Map([
  932. ["r", "&#x1d18c;"],
  933. ["s", "&#x1d18d;"],
  934. ["z", "&#x1d18e;"],
  935. ["p", "&#x1d18f;"],
  936. ["m", "&#x1d190;"],
  937. ["f", "&#x1d191;"],
  938. ]);
  939. function accidental(ascii) {
  940. if (!ascii) {
  941. return "";
  942. }
  943. switch (ascii) {
  944. case "b":
  945. return "&flat;";
  946. case "bb":
  947. return "&#x1d12b;";
  948. case "#":
  949. return "&sharp;";
  950. case "x":
  951. return "&#x1d12a;";
  952. case "n":
  953. return "&natural;";
  954. default: {
  955. console.error("Unrecognized accidental: " + ascii);
  956. throw "";
  957. }
  958. }
  959. }
  960. function chordQual(ascii) {
  961. if (!ascii) {
  962. return ["", undefined];
  963. }
  964. switch (ascii) {
  965. case "+-":
  966. return ["&pm;", "split&dash;third augmented"];
  967. case "-":
  968. return ["&minus;", "minor"];
  969. case "+":
  970. return ["+", "augmented"];
  971. case "|-|":
  972. return ["&#x229f;", "split&dash;third"];
  973. case "+maj":
  974. return ["+&#x2206;", "augmented major"];
  975. case "maj":
  976. return ["&#x2206;", "major"];
  977. case "sus2":
  978. return ["sus2", "suspended second"];
  979. case "sus4":
  980. return ["sus4", "suspended fourth"];
  981. case "o/":
  982. return ["&#x1d1a9;", "half&dash;diminished"];
  983. case "o":
  984. return ["&deg;", "diminished"];
  985. case "alt":
  986. return ["alt", "altered"];
  987. case "5":
  988. return ["5", "power"];
  989. default: {
  990. console.error("Unrecognized chord quality notation: " + ascii);
  991. throw "";
  992. }
  993. }
  994. }
  995. function rnaQual(ascii) {
  996. switch (ascii) {
  997. case "+maj":
  998. return ["+", "augmented major"];
  999. case "maj":
  1000. return ["", "major"];
  1001. case "5":
  1002. return ["", "power"];
  1003. default:
  1004. return chordQual(ascii);
  1005. }
  1006. }
  1007. function chordExt(ascii) {
  1008. if (!ascii) {
  1009. return ["", undefined];
  1010. }
  1011. switch (ascii) {
  1012. case "add2":
  1013. return ["add2", "added second"];
  1014. case "add4":
  1015. return ["add4", "added fourth"];
  1016. case "6":
  1017. return ["6", "sixth"];
  1018. case "7":
  1019. return ["7", "seventh"];
  1020. case "9":
  1021. return ["9", "ninth"];
  1022. case "11":
  1023. return ["11", "eleventh"];
  1024. case "13":
  1025. return ["13", "thirteenth"];
  1026. default: {
  1027. console.error(
  1028. "Unrecognized chord extension notation: " + ascii,
  1029. );
  1030. throw "";
  1031. }
  1032. }
  1033. }
  1034. function chordAlts(ascii) {
  1035. if (!ascii) {
  1036. return ["", undefined];
  1037. }
  1038. const ret = ["", "with "];
  1039. let first = true;
  1040. for (const [, acci, factor] of ascii.matchAll(ALT_RE_G)) {
  1041. const s = `${accidental(acci)}${factor}`;
  1042. ret[0] += `${first ? "" : "&ic;"}${s}`;
  1043. ret[1] += `${first ? "" : ", "}${s}`;
  1044. first = false;
  1045. }
  1046. return ret;
  1047. }
  1048. function chordOmitted(ascii) {
  1049. if (!ascii) {
  1050. return ["", undefined];
  1051. }
  1052. const factor = parseInt(ascii, 10);
  1053. if (!factor || !Number.isSafeInteger(factor)) {
  1054. console.error("Unexpected `omitted`: " + ascii);
  1055. throw "";
  1056. }
  1057. switch (factor) {
  1058. case 3:
  1059. return ["no3", "with omitted third"];
  1060. case 5:
  1061. return ["no5", "with omitted fifth"];
  1062. default: {
  1063. console.error("`chordOmitted` fallthrough: " + ascii);
  1064. throw "";
  1065. }
  1066. }
  1067. }
  1068. function chordInv(ascii, hasSeventh, isPower) {
  1069. if (!ascii) {
  1070. return ["", undefined];
  1071. }
  1072. const n = parseInt(ascii, 10);
  1073. switch (n) {
  1074. case 1: {
  1075. if (isPower) {
  1076. console.error("Power-chord in first inversion");
  1077. throw "";
  1078. }
  1079. return [
  1080. `<span class="inversion"><sup class="inversion-upper">6</sup>${
  1081. hasSeventh
  1082. ? '<sub class="inversion-lower">5</sub>'
  1083. : '<span class="inversion-lower"></span>'
  1084. }</span>`,
  1085. "in first inversion",
  1086. ];
  1087. }
  1088. case 2:
  1089. return [
  1090. isPower
  1091. ? '<span class="inversion"><sup class="inversion-upper">4</sup><span class="inversion-lower"></span></span>'
  1092. : `<span class="inversion"><sup class="inversion-upper">${
  1093. hasSeventh ? 4 : 6
  1094. }</sup><sub class="inversion-lower">${
  1095. hasSeventh ? 3 : 4
  1096. }</sub></span>`,
  1097. "in second inversion",
  1098. ];
  1099. case 3: {
  1100. if (!hasSeventh) {
  1101. console.error(
  1102. "Chord has no seventh, but is in third inversion",
  1103. );
  1104. throw "";
  1105. }
  1106. if (isPower) {
  1107. console.error("Power-chord in third inversion");
  1108. throw "";
  1109. }
  1110. return [
  1111. '<span class="inversion"><sup class="inversion-upper">4</sup><sub class="inversion-lower">2</sub></span>',
  1112. "in third inversion",
  1113. ];
  1114. }
  1115. default: {
  1116. console.error("Unrecognized inversion: " + ascii);
  1117. throw "";
  1118. }
  1119. }
  1120. }
  1121. function rnaInv(ext, inv, alts, hasMaj7, isPower) {
  1122. const intExt = parseInt(ext, 10);
  1123. const hasExt = Number.isSafeInteger(intExt);
  1124. const hasSeventh = hasExt && intExt >= 7;
  1125. const intInv = parseInt(inv, 10);
  1126. const hasInv = Number.isSafeInteger(intInv);
  1127. const parsedAlts = [];
  1128. for (const [, acci, factor] of alts.matchAll(ALT_RE_G)) {
  1129. parsedAlts.push([accidental(acci), parseInt(factor, 10)]);
  1130. }
  1131. let inline = "";
  1132. const upper = [];
  1133. let lower = 0;
  1134. if (hasExt) {
  1135. switch (intExt) {
  1136. case 6: {
  1137. inline += "6";
  1138. break;
  1139. }
  1140. case 7: {
  1141. if (!hasInv) {
  1142. upper.push([hasMaj7 ? "&#x2206;" : "", 7]);
  1143. } else {
  1144. inline += hasMaj7 ? "&#x2206;" : "";
  1145. }
  1146. break;
  1147. }
  1148. case 9: {
  1149. if (!hasInv) {
  1150. upper.push([hasMaj7 ? "&#x2206;" : "", 7]);
  1151. upper.push(["", 9]);
  1152. } else {
  1153. inline += hasMaj7 ? "&#x2206;9" : "9";
  1154. }
  1155. break;
  1156. }
  1157. case 11: {
  1158. if (!hasInv) {
  1159. upper.push([hasMaj7 ? "&#x2206;" : "", 7]);
  1160. upper.push(["", 11]);
  1161. } else {
  1162. inline += hasMaj7 ? "&#x2206;11" : "11";
  1163. }
  1164. break;
  1165. }
  1166. case 13: {
  1167. if (!hasInv) {
  1168. upper.push([hasMaj7 ? "&#x2206;" : "", 7]);
  1169. upper.push(["", 13]);
  1170. } else {
  1171. inline += hasMaj7 ? "&#x2206;13" : "13";
  1172. }
  1173. break;
  1174. }
  1175. default: {
  1176. console.error("Unrecognized RNA ext:", intExt);
  1177. throw "";
  1178. }
  1179. }
  1180. }
  1181. if (isPower) {
  1182. if (!hasInv) {
  1183. upper.push(["", 5]);
  1184. } else if (intInv === 2) {
  1185. upper.push(["", 4]);
  1186. } else {
  1187. console.error("Inverted power-chord not in 2nd inversion");
  1188. throw "";
  1189. }
  1190. } else if (hasInv) {
  1191. switch (intInv) {
  1192. case 1: {
  1193. upper.push(["", 6]);
  1194. if (hasSeventh) {
  1195. lower = 5;
  1196. }
  1197. break;
  1198. }
  1199. case 2: {
  1200. if (hasSeventh) {
  1201. upper.push(["", 4]);
  1202. lower = 3;
  1203. } else {
  1204. upper.push(["", 6]);
  1205. lower = 4;
  1206. }
  1207. break;
  1208. }
  1209. case 3: {
  1210. upper.push(["", 4]);
  1211. lower = 2;
  1212. break;
  1213. }
  1214. default: {
  1215. console.error("Bad inversion:", intInv);
  1216. throw "";
  1217. }
  1218. }
  1219. }
  1220. if (hasInv) {
  1221. inline += parsedAlts.map(([s, n]) => "" + s + n).join("&ic;");
  1222. } else {
  1223. upper.push(...parsedAlts);
  1224. }
  1225. upper.sort(([, n], [, m]) => n - m);
  1226. const invHtml = `<span class="inversion"><${
  1227. upper.length > 0 ? "sup" : "span"
  1228. } class="inversion-upper">${upper
  1229. .map(([s, n]) => "" + s + n)
  1230. .join(",")}</${upper.length > 0 ? "sup" : "span"}><${
  1231. lower ? "sub" : "span"
  1232. } class="inversion-lower">${lower ? lower : ""}</${
  1233. lower ? "sub" : "span"
  1234. }></span>`;
  1235. return `${inline}${upper.length > 0 || lower ? invHtml : ""}`;
  1236. }
  1237. function romanDesc(acci, rNum) {
  1238. const [deg, fun] = (() => {
  1239. switch (rNum.toUpperCase()) {
  1240. case "I":
  1241. return ["1st", "tonic"];
  1242. case "II":
  1243. return ["2nd", "supertonic"];
  1244. case "III":
  1245. return ["3rd", "mediant"];
  1246. case "IV":
  1247. return ["4th", "subdominant"];
  1248. case "V":
  1249. return ["5th", "dominant"];
  1250. case "VI":
  1251. return ["6th", "submediant"];
  1252. case "VII":
  1253. return ["7th", "leading&dash;tone or subtonic"];
  1254. }
  1255. })();
  1256. return `${
  1257. acci === "b" ? "flattened " : acci === "#" ? "sharpened " : ""
  1258. }${deg} degree (${fun})`;
  1259. }
  1260. function parseChord(chordMatch) {
  1261. const { letter, acci, qual, ext, alts, omitted, slash, inv } =
  1262. chordMatch.groups;
  1263. const root = `${letter}${accidental(acci)}`;
  1264. const [qualText, qualDesc] = chordQual(qual);
  1265. const [extText, extDesc] = chordExt(ext);
  1266. const [altsText, altsDesc] = chordAlts(alts);
  1267. const [omittedText, omittedDesc] = chordOmitted(omitted);
  1268. const [, slashLetter, slashAcci] = slash
  1269. ? PC_RE_G.exec(slash)
  1270. : [undefined, undefined];
  1271. const intExt = parseInt(ext, 10);
  1272. const hasSeventh = Number.isSafeInteger(intExt) && intExt >= 7;
  1273. const [invText, invDesc] = chordInv(inv, hasSeventh, qual === "5");
  1274. return [
  1275. `<span class="no-wrap">${root}${qualText}${extText}${altsText}${omittedText}${
  1276. slashLetter
  1277. ? '<span class="chord-slash">/</span><span class="chord-slash-bass">' +
  1278. slashLetter +
  1279. accidental(slashAcci) +
  1280. "</span>"
  1281. : ""
  1282. }${invText}</span>`,
  1283. `${root} ${
  1284. qualDesc ? qualDesc : hasSeventh ? "dominant" : "major"
  1285. }${extDesc ? " " + extDesc : ""} chord${
  1286. altsDesc ? " " + altsDesc : ""
  1287. }${omittedDesc ? " " + omittedDesc : ""}${
  1288. slashLetter
  1289. ? ", with " +
  1290. slashLetter +
  1291. accidental(slashAcci) +
  1292. " in the bass"
  1293. : invDesc
  1294. ? ", " + invDesc
  1295. : ""
  1296. }`,
  1297. ];
  1298. }
  1299. function parseRna(rnaMatch) {
  1300. const {
  1301. acci,
  1302. roman,
  1303. qual,
  1304. ext,
  1305. alts,
  1306. omitted,
  1307. inv,
  1308. slashAcci,
  1309. slash,
  1310. } = rnaMatch.groups;
  1311. const [qualText, qualDesc] = rnaQual(qual);
  1312. const isPower = qual === "5";
  1313. const invText = rnaInv(
  1314. ext,
  1315. inv,
  1316. alts,
  1317. qual && qual.indexOf("maj") !== -1,
  1318. isPower,
  1319. );
  1320. const intExt = parseInt(ext, 10);
  1321. const hasExt = Number.isSafeInteger(intExt);
  1322. const hasSeventh = hasExt && intExt >= 7;
  1323. const minor = roman === roman.toLowerCase();
  1324. const defaultQual = (() => {
  1325. if (minor) {
  1326. return "minor";
  1327. } else if (hasSeventh) {
  1328. return "dominant";
  1329. } else {
  1330. return "major";
  1331. }
  1332. })();
  1333. const [, extDesc] = chordExt(ext);
  1334. const [, altsDesc] = chordAlts(alts);
  1335. const [omittedText, omittedDesc] = chordOmitted(omitted);
  1336. const [, invDesc] = chordInv(inv, hasSeventh, isPower);
  1337. const [slashText, slashDesc] = (() => {
  1338. if (!slash) {
  1339. return ["", ""];
  1340. }
  1341. const accident = accidental(slashAcci);
  1342. const slashDeg = Number.parseInt(slash, 10);
  1343. if (Number.isSafeInteger(slashDeg)) {
  1344. const deg = `${accident}${slashDeg}&#x302;`;
  1345. return [
  1346. `<span class="rna-slash">/</span><span class="rna-slash-bass">${deg}</span>`,
  1347. `, with the ${deg} degree in the bass`,
  1348. ];
  1349. }
  1350. return [
  1351. `<span class="rna-slash">/</span><span class="rna-slash-target">${accident}${slash}</span>`,
  1352. `, as applied to the ${romanDesc(
  1353. slashAcci,
  1354. slash,
  1355. )} of the key`,
  1356. ];
  1357. })();
  1358. return [
  1359. `<span class="no-wrap">${accidental(
  1360. acci,
  1361. )}${roman}${qualText}${omittedText}${invText}${slashText}</span>`,
  1362. `Chord on the ${romanDesc(acci, roman)}: ${
  1363. qualDesc ? qualDesc : defaultQual
  1364. }${extDesc ? " " + extDesc : ""} chord${
  1365. altsDesc ? " " + altsDesc : ""
  1366. }${omittedDesc ? " " + omittedDesc : ""}${
  1367. invDesc ? ", " + invDesc : ""
  1368. }${slashDesc}`,
  1369. ];
  1370. }
  1371. function namedInterval(text, namedIntervalMatch) {
  1372. if (text.indexOf("T") !== -1) {
  1373. if (text !== "TT") {
  1374. return;
  1375. }
  1376. return ["TT", "An interval of a tritone"];
  1377. }
  1378. const [s, qual, size] = namedIntervalMatch;
  1379. const qualName = (() => {
  1380. switch (qual) {
  1381. case "P":
  1382. return "a perfect";
  1383. case "M":
  1384. return "a major";
  1385. case "m":
  1386. return "a minor";
  1387. case "A":
  1388. return "an augmented";
  1389. case "d":
  1390. return "a diminished";
  1391. }
  1392. })();
  1393. const sizeName = (() => {
  1394. const parsedSize = parseInt(size, 10);
  1395. switch (parsedSize) {
  1396. case 1:
  1397. return "unison";
  1398. case 2:
  1399. return "second";
  1400. case 3:
  1401. return "third";
  1402. case 4:
  1403. return "fourth";
  1404. case 5:
  1405. return "fifth";
  1406. case 6:
  1407. return "sixth";
  1408. case 7:
  1409. return "seventh";
  1410. case 8:
  1411. return "octave";
  1412. case 9:
  1413. return "ninth";
  1414. case 10:
  1415. return "tenth";
  1416. case 11:
  1417. return "eleventh";
  1418. case 12:
  1419. return "twelfth";
  1420. case 13:
  1421. return "thirteenth";
  1422. default: {
  1423. console.error(
  1424. "Unexpected named interval size:",
  1425. parsedSize,
  1426. );
  1427. throw "";
  1428. }
  1429. }
  1430. })();
  1431. return [s, `An interval of ${qualName} ${sizeName}`];
  1432. }
  1433. function subOctaveIntToStr(n) {
  1434. switch (n) {
  1435. case 0:
  1436. case 1:
  1437. case 2:
  1438. case 3:
  1439. case 4:
  1440. case 5:
  1441. case 6:
  1442. case 7:
  1443. case 8:
  1444. case 9:
  1445. return "" + n;
  1446. case 10:
  1447. return "&#x218a;";
  1448. case 11:
  1449. return "&#x218b;";
  1450. default:
  1451. return;
  1452. }
  1453. }
  1454. function prime(n) {
  1455. if (n % 1 !== 0 || n < 0) {
  1456. return;
  1457. }
  1458. switch (n) {
  1459. case 0:
  1460. return "";
  1461. case 1:
  1462. return "&prime;";
  1463. case 2:
  1464. return "&Prime;";
  1465. case 3:
  1466. return "&tprime;";
  1467. default:
  1468. return `&qprime;${prime(n - 4)}`;
  1469. }
  1470. }
  1471. function ordinal(n) {
  1472. const last = n % 10;
  1473. const penultimate = Math.trunc(n / 10) % 10;
  1474. if (penultimate !== 1) {
  1475. if (last === 1) {
  1476. return `${n}st`;
  1477. }
  1478. if (last === 2) {
  1479. return `${n}nd`;
  1480. }
  1481. if (last === 3) {
  1482. return `${n}rd`;
  1483. }
  1484. }
  1485. return `${n}th`;
  1486. }
  1487. function motionName(text) {
  1488. switch (text) {
  1489. case "c":
  1490. return "contrary";
  1491. case "o":
  1492. return "oblique";
  1493. case "s":
  1494. return "similar";
  1495. case "i":
  1496. return "imperfect parallel";
  1497. case "p":
  1498. return "perfect parallel";
  1499. default: {
  1500. console.error("Unrecognized motion type:", text);
  1501. throw "";
  1502. }
  1503. }
  1504. }
  1505. function musNotate(text, ty) {
  1506. C_TIME_RE.lastIndex = 0;
  1507. STD_TIME_RE.lastIndex = 0;
  1508. ADD_TIME_RE.lastIndex = 0;
  1509. FORM_RE.lastIndex = 0;
  1510. FORM_SECT_RE_G.lastIndex = 0;
  1511. PITCH_RE.lastIndex = 0;
  1512. PC_RE.lastIndex = 0;
  1513. PC_RE_G.lastIndex = 0;
  1514. PC_COLLECT_RE.lastIndex = 0;
  1515. PC_PROG_RE.lastIndex = 0;
  1516. DEG_RE.lastIndex = 0;
  1517. INT_RE_G.lastIndex = 0;
  1518. INT_COLLECT_RE.lastIndex = 0;
  1519. FORTE_RE.lastIndex = 0;
  1520. NAMED_INTERVAL_RE.lastIndex = 0;
  1521. CENTS_RE.lastIndex = 0;
  1522. CPATH_RE.lastIndex = 0;
  1523. CPATH_SEG_RE_G.lastIndex = 0;
  1524. CHORD_FACT_RE.lastIndex = 0;
  1525. CHORD_RE.lastIndex = 0;
  1526. CHORD_PROG_INIT_RE.lastIndex = 0;
  1527. RNA_RE.lastIndex = 0;
  1528. RNA_PROG_INIT_RE.lastIndex = 0;
  1529. ALT_RE_G.lastIndex = 0;
  1530. DYN_RE.lastIndex = 0;
  1531. NOTES_RE.lastIndex = 0;
  1532. NOTE_RE_G.lastIndex = 0;
  1533. MOTION_RE.lastIndex = 0;
  1534. MOTION_RE_G.lastIndex = 0;
  1535. ICV_RE.lastIndex = 0;
  1536. ARROW_RE.lastIndex = 0;
  1537. DELTA_RE_G.lastIndex = 0;
  1538. switch ("" + ty) {
  1539. case "undefined":
  1540. break;
  1541. case "0":
  1542. return [text, undefined];
  1543. case "mot":
  1544. return [text, `An instance of ${motionName(text)} motion`];
  1545. case "->": {
  1546. const arrowMatch = ARROW_RE.exec(text);
  1547. if (!arrowMatch) {
  1548. return;
  1549. }
  1550. const { deltas, motions, curly } = arrowMatch.groups;
  1551. if (!deltas && !motions) {
  1552. return;
  1553. }
  1554. const under = deltas && motions ? "under" : "";
  1555. const deltasStr = deltas
  1556. ? '<mrow><mspace depth="0" height="0" width="0.25em" />' +
  1557. Array.from(deltas.matchAll(DELTA_RE_G))
  1558. .map(m => {
  1559. const s = m[0].trim();
  1560. const parsed = parseInt(s, 10);
  1561. if (!Number.isSafeInteger(parsed)) {
  1562. console.error("Couldn't parse delta:", text);
  1563. throw "";
  1564. }
  1565. return `<mn>${s.replaceAll(
  1566. "-",
  1567. "&minus;",
  1568. )}</mn>`;
  1569. })
  1570. .join("<mo>,</mo>") +
  1571. '<mspace depth="0" height="0" width="0.25em" /></mrow>'
  1572. : "";
  1573. const motionsStr = motions
  1574. ? '<mrow><mspace depth="0" height="0" width="0.25em" />' +
  1575. (curly ? "<mo>&#x27c5;</mo><mrow>" : "") +
  1576. Array.from(motions.matchAll(MOTION_RE_G))
  1577. .map(([, ty, multi]) => {
  1578. const multiplicity = multi
  1579. ? parseInt(multi, 10)
  1580. : 1;
  1581. if (!Number.isSafeInteger(multiplicity)) {
  1582. console.error(
  1583. "Couldn't parse multiplicity:",
  1584. text,
  1585. );
  1586. throw "";
  1587. }
  1588. return multiplicity === 1
  1589. ? `<mi mathvariant="normal">${ty}</mi>`
  1590. : `<msup><mi mathvariant="normal">${ty}</mi><mn>${multiplicity}</mn></msup>`;
  1591. })
  1592. .join("<mo>&ic;</mo>") +
  1593. (curly ? "</mrow><mo>&#x27c6;</mo>" : "") +
  1594. '<mspace depth="0" height="0" width="0.25em" /></mrow>'
  1595. : "";
  1596. return [
  1597. `<math><m${under}over><mo stretchy="true" largeop="true" form="infix">&rarr;</mo>${motionsStr}${deltasStr}</m${under}over></math>`,
  1598. undefined,
  1599. ];
  1600. }
  1601. case "icv": {
  1602. const icvMatch = ICV_RE.exec(text);
  1603. if (!icvMatch) {
  1604. return;
  1605. }
  1606. const [, one, two, three, four, five, six] = icvMatch;
  1607. const pitman = c => {
  1608. switch (c) {
  1609. case "t":
  1610. return "&#x218a;";
  1611. case "e":
  1612. return "&#x218b;";
  1613. default:
  1614. return c;
  1615. }
  1616. };
  1617. const joined = `&lang;${pitman(one)}&ic;${pitman(
  1618. two,
  1619. )}&ic;${pitman(three)}&ic;${pitman(four)}&ic;${pitman(
  1620. five,
  1621. )}&ic;${pitman(six)}&rang;`;
  1622. return [joined, `The interval&dash;class vector ${joined}`];
  1623. }
  1624. case "inter": {
  1625. const namedIntervalMatch = NAMED_INTERVAL_RE.exec(text);
  1626. if (!namedIntervalMatch) {
  1627. return;
  1628. }
  1629. return namedInterval(text, namedIntervalMatch);
  1630. }
  1631. case "fact": {
  1632. const chordFactMatch = CHORD_FACT_RE.exec(text);
  1633. if (!chordFactMatch) {
  1634. return;
  1635. }
  1636. const [, acci, nStr] = chordFactMatch;
  1637. const n = parseInt(nStr, 10);
  1638. const ord = ordinal(n);
  1639. const alt = (() => {
  1640. switch (acci) {
  1641. case "b":
  1642. return "flattened";
  1643. case "bb":
  1644. return "doubly&dash;flattened";
  1645. case "#":
  1646. return "sharpened";
  1647. case "x":
  1648. return "doubly&dash;sharpened";
  1649. case "n":
  1650. return "unaltered";
  1651. }
  1652. })();
  1653. return [
  1654. `${accidental(acci)}${n}`,
  1655. `The ${alt ? alt + " " : ""}${ord} factor of a chord`,
  1656. ];
  1657. }
  1658. case "form": {
  1659. const formMatch = FORM_RE.exec(text);
  1660. if (!formMatch) {
  1661. return;
  1662. }
  1663. const matches = Array.from(text.matchAll(FORM_SECT_RE_G));
  1664. return [
  1665. "&Lang;" +
  1666. matches
  1667. .map(
  1668. ([, letter, p, n]) =>
  1669. `${letter}${prime(p.length)}${
  1670. n ? "<sub>" + n + "</sub>" : ""
  1671. }`,
  1672. )
  1673. .join("&ic;") +
  1674. "&Rang;",
  1675. matches.length > 1
  1676. ? `A musical form with ${matches.length} sections`
  1677. : `The ${matches[0][1]}${prime(matches[0][2].length)}${
  1678. matches[0][3] ? matches[0][3] : ""
  1679. } section of a musical form`,
  1680. ];
  1681. }
  1682. case "chord": {
  1683. const chordMatch = CHORD_RE.exec(text);
  1684. if (!chordMatch) {
  1685. console.error("Couldn't parse chord: " + text);
  1686. throw "";
  1687. }
  1688. return parseChord(chordMatch);
  1689. }
  1690. default:
  1691. return;
  1692. }
  1693. const cTimeMatch = C_TIME_RE.exec(text);
  1694. if (cTimeMatch) {
  1695. switch (cTimeMatch[0]) {
  1696. case "c":
  1697. return [
  1698. '<span class="c-time">&#x1d134;</span>',
  1699. "Common time (4&frasl;4)",
  1700. ];
  1701. case "c/":
  1702. return [
  1703. '<span class="c-time">&#x1d135;</span>',
  1704. "Alla breve (2&frasl;2; cut time)",
  1705. ];
  1706. default:
  1707. return;
  1708. }
  1709. }
  1710. const parenthesizeIfFractional = (s, noMarkup) =>
  1711. s.includes(".")
  1712. ? `${noMarkup ? "" : '<span class="time-sig-paren">'}(${
  1713. noMarkup ? "" : "</span>"
  1714. }${s}${noMarkup ? "" : '<span class="time-sig-paren">'})${
  1715. noMarkup ? "" : "</span>"
  1716. }`
  1717. : s;
  1718. const stdTimeMatch = STD_TIME_RE.exec(text);
  1719. if (stdTimeMatch) {
  1720. const { num, denom } = stdTimeMatch.groups;
  1721. return [
  1722. `<span class="time-sig"><span class="time-sig-num">${parenthesizeIfFractional(
  1723. num,
  1724. )}</span><span class="time-sig-slash">&frasl;</span><span class="time-sig-denom">${parenthesizeIfFractional(
  1725. denom,
  1726. )}</span></span>`,
  1727. `${parenthesizeIfFractional(
  1728. num,
  1729. true,
  1730. )}&frasl;${parenthesizeIfFractional(denom, true)} time`,
  1731. ];
  1732. }
  1733. const addTimeMatch = ADD_TIME_RE.exec(text);
  1734. if (addTimeMatch) {
  1735. const { num, denom } = addTimeMatch.groups;
  1736. return [
  1737. `<span class="time-sig"><span class="time-sig-num"><span class="time-sig-paren">(</span>${num}<span class="time-sig-paren">)</span></span><span class="time-sig-slash">&frasl;</span><span class="time-sig-denom">${denom}</span></span>`,
  1738. `(${num})&frasl;${denom} time`,
  1739. ];
  1740. }
  1741. const pitchMatch = PITCH_RE.exec(text);
  1742. if (pitchMatch) {
  1743. const [, name, acci, oct] = pitchMatch;
  1744. const s = name + accidental(acci);
  1745. const n = oct.replace("-", "&minus;");
  1746. return [
  1747. `${s}<sub>${n}</sub>`,
  1748. `The pitch, in scientific pitch notation: ${s}${n}`,
  1749. ];
  1750. }
  1751. const pcMatch = PC_RE.exec(text);
  1752. if (pcMatch) {
  1753. const [, name, acci] = pcMatch;
  1754. const s = name + accidental(acci);
  1755. return [s, `The pitchclass ${s}`];
  1756. }
  1757. const pcCollectMatch = PC_COLLECT_RE.exec(text);
  1758. if (pcCollectMatch) {
  1759. const [lBrack, rBrack, collectTy] =
  1760. pcCollectMatch[1] === "{"
  1761. ? ["{", "}", "set"]
  1762. : ["[", "]", "row"];
  1763. const joined = Array.from(text.matchAll(PC_RE_G))
  1764. .map(match => match[1] + accidental(match[2]))
  1765. .join(", ");
  1766. return [
  1767. `${lBrack}${joined}${rBrack}`,
  1768. `The ${collectTy} of pitchclasses ${lBrack}${joined}${rBrack}`,
  1769. ];
  1770. }
  1771. const pcProgMatch = PC_PROG_RE.exec(text);
  1772. if (pcProgMatch) {
  1773. const joined = Array.from(text.matchAll(PC_RE_G))
  1774. .map(match => match[1] + accidental(match[2]))
  1775. .join("&ndash;");
  1776. return [joined, `The progression of pitchclasses ${joined}`];
  1777. }
  1778. const degMatch = DEG_RE.exec(text);
  1779. if (degMatch) {
  1780. const [, acci, nStr] = degMatch;
  1781. const n = parseInt(nStr, 10);
  1782. return [
  1783. `${accidental(acci)}${nStr}&#x302;`,
  1784. `The ${ordinal(n)} degree of a key or mode`,
  1785. ];
  1786. }
  1787. const intCollectMatch = INT_COLLECT_RE.exec(text);
  1788. if (intCollectMatch) {
  1789. const [lBrack, rBrack, collectTy] =
  1790. intCollectMatch[1] === "{"
  1791. ? ["{", "}", "set"]
  1792. : ["[", "]", "row"];
  1793. const ints = text.match(INT_RE_G).map(m => +m);
  1794. const intStrs = ints.map(subOctaveIntToStr);
  1795. const joined = intStrs.every(s => s)
  1796. ? intStrs.join("&ic;")
  1797. : ints.join(", ");
  1798. return [
  1799. `${lBrack}${joined}${rBrack}`,
  1800. `The ${collectTy} of pitchclasses, in integer notation: ${lBrack}${joined}${rBrack}`,
  1801. ];
  1802. }
  1803. const forteMatch = FORTE_RE.exec(text);
  1804. if (forteMatch) {
  1805. const [m, forte, inv] = forteMatch;
  1806. const pcSetClass =
  1807. inv === "A" || inv === undefined
  1808. ? FORTE_LUT.get(forte)
  1809. : FORTE_LUT.get(m);
  1810. if (!pcSetClass) {
  1811. console.error("Unknown pc-set class:", m);
  1812. throw "";
  1813. }
  1814. const joined = pcSetClass.map(subOctaveIntToStr).join("&ic;");
  1815. const mDashed = m.replaceAll("-", "&dash;");
  1816. const [lBrack, rBrack, lBrackSpan, rBrackSpan] = inv
  1817. ? [
  1818. "&#x2045;",
  1819. "&#x2046;",
  1820. '<span class="piggpar">&#x2045;</span>',
  1821. '<span class="piggpar">&#x2046;</span>',
  1822. ]
  1823. : ["&lobrk;", "&robrk;", "&lobrk;", "&robrk;"];
  1824. return [
  1825. `<span class="no-wrap">${lBrackSpan}${joined}${rBrackSpan}<sub>${mDashed}</sub></span>`,
  1826. `The pitchclass&dash;set equivalence&dash;class, in integer notation: ${lBrack}${joined}${rBrack} (Forte number ${mDashed})`,
  1827. ];
  1828. }
  1829. const namedIntervalMatch = NAMED_INTERVAL_RE.exec(text);
  1830. if (namedIntervalMatch) {
  1831. return namedInterval(text, namedIntervalMatch);
  1832. }
  1833. const centsMatch = CENTS_RE.exec(text);
  1834. if (centsMatch) {
  1835. const [, n] = centsMatch;
  1836. const nm = n.replaceAll("-", "&minus;");
  1837. return [nm + "&it;&cent;", `An interval of ${nm}&it;&nbsp;cents`];
  1838. }
  1839. const cpathMatch = CPATH_RE.exec(text);
  1840. if (cpathMatch) {
  1841. const [, inner] = cpathMatch;
  1842. const [joined, numPoints] = inner
  1843. .match(CPATH_SEG_RE_G)
  1844. .map(seg => {
  1845. const segSplit = seg.split("");
  1846. return [segSplit.join("&ic;"), segSplit.length];
  1847. })
  1848. .reduce(([j, n], [s, p]) => [`${j}&ic; ${s}`, n + p]);
  1849. const [lBrack, rBrack] =
  1850. numPoints > 1 ? ["&lang;", "&rang;"] : ["", ""];
  1851. return [
  1852. `${lBrack}${joined}${rBrack}<sub style="font-weight: 700;">c</sub>`,
  1853. numPoints > 1
  1854. ? `A segment in contour&dash;space (c&dash;seg), consisting of ${numPoints} (not necessarily distinct) c&dash;pitches`
  1855. : "A contour&dash;pitch (c&dash;pitch)",
  1856. ];
  1857. }
  1858. const chordMatch = CHORD_RE.exec(text);
  1859. if (chordMatch) {
  1860. return parseChord(chordMatch);
  1861. }
  1862. if (CHORD_PROG_INIT_RE.exec(text)) {
  1863. let ret = "";
  1864. let first = true;
  1865. for (const frag of text.split("--")) {
  1866. CHORD_RE.lastIndex = 0;
  1867. const chordM = CHORD_RE.exec(frag);
  1868. if (!chordM) {
  1869. console.error("Couldn't parse chord prog:", text);
  1870. throw "";
  1871. }
  1872. const [s, title] = parseChord(chordM);
  1873. if (!first) {
  1874. ret += "&ndash;";
  1875. }
  1876. ret += `<span title="${title}">${s}</span>`;
  1877. first = false;
  1878. }
  1879. return [ret, undefined];
  1880. }
  1881. const rnaMatch = RNA_RE.exec(text);
  1882. if (rnaMatch) {
  1883. return parseRna(rnaMatch);
  1884. }
  1885. if (RNA_PROG_INIT_RE.exec(text)) {
  1886. let ret = "";
  1887. let first = true;
  1888. for (const frag of text.split("--")) {
  1889. RNA_RE.lastIndex = 0;
  1890. const rnaM = RNA_RE.exec(frag);
  1891. if (!rnaM) {
  1892. console.error("Couldn't parse RNA prog:", text);
  1893. throw "";
  1894. }
  1895. const [s, title] = parseRna(rnaM);
  1896. if (!first) {
  1897. ret += "&ndash;";
  1898. }
  1899. ret += `<span title="${title}">${s}</span>`;
  1900. first = false;
  1901. }
  1902. return [ret, undefined];
  1903. }
  1904. const dynMatch = DYN_RE.exec(text);
  1905. if (dynMatch) {
  1906. const s = text
  1907. .split("")
  1908. .map(c => DYN_LUT.get(c))
  1909. .join("");
  1910. const { mezzo, piano, forte, forz, rein } = dynMatch.groups;
  1911. if (mezzo) {
  1912. return [
  1913. s,
  1914. `mezzo&dash;${text.endsWith("p") ? "piano" : "forte"}`,
  1915. ];
  1916. }
  1917. const suffix = (n, v) => (n < 2 ? v : "iss".repeat(n - 1) + "imo");
  1918. if (piano) {
  1919. return text.startsWith("s")
  1920. ? [s, `subito pian${suffix(text.length - 1, "o")}`]
  1921. : [s, `pian${suffix(text.length, "o")}`];
  1922. }
  1923. if (forte) {
  1924. return text.startsWith("s")
  1925. ? [s, `subito fort${suffix(text.length - 1, "e")}`]
  1926. : [s, `fort${suffix(text.length, "e")}`];
  1927. }
  1928. if (text === "fp") {
  1929. return [s, "fortepiano"];
  1930. }
  1931. if (forz) {
  1932. return [s, text.startsWith("s") ? "sforzando" : "forzando"];
  1933. }
  1934. if (rein) {
  1935. return [s, "rinforzando"];
  1936. }
  1937. }
  1938. const notesMatch = NOTES_RE.exec(text);
  1939. if (notesMatch) {
  1940. const [, brack] = notesMatch;
  1941. const joined = Array.from(text.matchAll(NOTE_RE_G))
  1942. .map(([, rn, denomStr, dot, accent]) => {
  1943. const rest = rn === "r";
  1944. const denom = parseInt(denomStr, 10);
  1945. const base = (() => {
  1946. switch (denom) {
  1947. case 1:
  1948. return rest ? "&#x1d13b;" : "&#x1d15d;";
  1949. case 2:
  1950. return rest ? "&#x1d13c;" : "&#x1d15e;";
  1951. case 4:
  1952. return rest ? "&#x1d13d;" : "&#x1d15f;";
  1953. case 8:
  1954. return rest ? "&#x1d13e;" : "&#x1d160;";
  1955. case 16:
  1956. return rest ? "&#x1d13f;" : "&#x1d161;";
  1957. case 32:
  1958. return rest ? "&#x1d140;" : "&#x1d162;";
  1959. case 64:
  1960. return rest ? "&#x1d141;" : "&#x1d163;";
  1961. case 128:
  1962. return rest ? "&#x1d142;" : "&#x1d164;";
  1963. default: {
  1964. console.error(
  1965. "Bad note duration denominator:",
  1966. denom,
  1967. );
  1968. throw "";
  1969. }
  1970. }
  1971. })();
  1972. return `${base}${dot ? "&#x1d16d;" : ""}${
  1973. accent ? "&#x1d17b;" : ""
  1974. }`;
  1975. })
  1976. .join("");
  1977. return [
  1978. `${
  1979. brack ? "&lang;&NoBreak;" : ""
  1980. }<span class="mus-notes">${joined}</span>${
  1981. brack ? "&NoBreak;&rang;" : ""
  1982. }`,
  1983. undefined,
  1984. ];
  1985. }
  1986. const motionMatch = MOTION_RE.exec(text);
  1987. if (motionMatch) {
  1988. const [, bracket] = motionMatch;
  1989. const [lBrack, rBrack, sq] =
  1990. bracket === "["
  1991. ? ["[", "]", true]
  1992. : ["&#x27c5;", "&#x27c6;", false];
  1993. const a = Array.from(text.matchAll(MOTION_RE_G));
  1994. const joined = a
  1995. .map(([, t, m]) => `${t}${m ? "<sup>" + m + "</sup>" : ""}`)
  1996. .join("&ic;");
  1997. const full = a
  1998. .map(([, t, m]) =>
  1999. Array(m ? parseInt(m, 10) : 1).fill(motionName(t)),
  2000. )
  2001. .flat();
  2002. const fullJoined = full.join(", ");
  2003. return [
  2004. `${lBrack}${joined}${rBrack}`,
  2005. `An instance of polyphonic motion: ${fullJoined}${
  2006. full.length > 1
  2007. ? sq
  2008. ? ""
  2009. : " (in no particular order)"
  2010. : " motion"
  2011. }`,
  2012. ];
  2013. }
  2014. }
  2015. eleventyConfig.addShortcode("m", (text, ty) => {
  2016. const n = musNotate(text, ty);
  2017. if (!n) {
  2018. console.error(`Couldn't parse {% m "${text}" %}`);
  2019. throw "";
  2020. }
  2021. return `<span class="music"${n[1] ? ' title="' + n[1] + '"' : ""}>${
  2022. n[0]
  2023. }</span>`;
  2024. });
  2025. function rankToWikidata(rank) {
  2026. switch (rank) {
  2027. case "genus":
  2028. return "34740";
  2029. case "suborder":
  2030. return "5867959";
  2031. case "superfamily":
  2032. return "2136103";
  2033. default: {
  2034. console.error("Unrecognized rank:", rank);
  2035. throw "";
  2036. }
  2037. }
  2038. }
  2039. eleventyConfig.addShortcode("species", (genus, species, url, abbr) => {
  2040. const genusText =
  2041. '<span itemprop="parentTaxon" itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q34740" /><' +
  2042. (abbr
  2043. ? `meta itemprop="name" content="${genus}" /><abbr title="${genus}">${genus[0]}.</abbr`
  2044. : `span itemprop="name">${genus}</span`) +
  2045. "></span>";
  2046. return `<span itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q7432" />${
  2047. url ? '<a itemprop="url" href="' + url + '">' : ""
  2048. }<i itemprop="name">${genusText} ${species}</i>${
  2049. url ? "</a>" : ""
  2050. }</span>`;
  2051. });
  2052. eleventyConfig.addShortcode("taxon", (rank, name, parent, url) => {
  2053. const nameTag = rank === "genus" ? "i" : "span";
  2054. return `<span itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q${rankToWikidata(
  2055. rank,
  2056. )}" />${
  2057. parent
  2058. ? '<meta itemprop="parentTaxon" content="' + parent + '" />'
  2059. : ""
  2060. }${
  2061. url ? '<a itemprop="url" href="' + url + '">' : ""
  2062. }<${nameTag} itemprop="name">${name}</${nameTag}>${
  2063. url ? "</a>" : ""
  2064. }</span>`;
  2065. });
  2066. eleventyConfig.addShortcode("personage", personage);
  2067. eleventyConfig.addShortcode("band", (name, href) =>
  2068. personage(name, undefined, href, "MusicGroup"),
  2069. );
  2070. eleventyConfig.addShortcode(
  2071. "album",
  2072. (name, artist, date, noArtist, noDate) => {
  2073. const artistStr = artist
  2074. ? noArtist
  2075. ? `<meta itemprop="byArtist creator author" content="${artist}" />`
  2076. : `<span itemprop="byArtist creator author">${personage(
  2077. artist,
  2078. undefined,
  2079. undefined,
  2080. "MusicGroup",
  2081. )}</span>&rsquo;s `
  2082. : "";
  2083. const dateStr = date
  2084. ? noDate
  2085. ? `<meta itemprop="datePublished" content="${date}" />`
  2086. : ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
  2087. : "";
  2088. return `<span class="work-mention" itemscope itemtype="https://schema.org/MusicAlbum">${artistStr}<cite itemprop="name">${name}</cite>${dateStr}</span>`;
  2089. },
  2090. );
  2091. eleventyConfig.addShortcode(
  2092. "track",
  2093. (name, album, artist, date, showDate) => {
  2094. const albumStr = album
  2095. ? `<meta itemprop="inAlbum" content="${album}" />`
  2096. : "";
  2097. const artistStr = artist
  2098. ? `<meta itemprop="byArtist" content="${artist}" />`
  2099. : "";
  2100. const dateStr = date
  2101. ? showDate
  2102. ? ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
  2103. : `<meta itemprop="datePublished" content="${date}" />`
  2104. : "";
  2105. return (
  2106. '<span class="track work-mention" itemscope itemtype="https://schema.org/MusicRecording">' +
  2107. `&ldquo;<span itemprop="name">${name}</span>&rdquo;${albumStr}${artistStr}${dateStr}</span>`
  2108. );
  2109. },
  2110. );
  2111. eleventyConfig.addShortcode(
  2112. "litmention",
  2113. (name, ty, author, date, noAuthor, noDate, titleLang, url) => {
  2114. const authorStr = author
  2115. ? noAuthor
  2116. ? `<meta itemprop="author creator" content="${author}" />`
  2117. : `<span itemprop="author creator">${personage(
  2118. author,
  2119. undefined,
  2120. undefined,
  2121. "Person",
  2122. )}</span>&rsquo;s `
  2123. : "";
  2124. const dateStr = date
  2125. ? noDate
  2126. ? `<meta itemprop="datePublished" content="${date}" />`
  2127. : ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
  2128. : "";
  2129. const [aOpen, aClose] = url
  2130. ? ['<a itemprop="url" href="' + url + '">', "</a>"]
  2131. : ["", ""];
  2132. const cite =
  2133. ty === "Poem"
  2134. ? `&ldquo;${aOpen}<cite${
  2135. titleLang ? ' lang="' + titleLang + '"' : ""
  2136. } class="quote-cite" itemprop="name">${name}</cite>${aClose}&rdquo;`
  2137. : `${aOpen}<cite${
  2138. titleLang ? ' lang="' + titleLang + '"' : ""
  2139. } itemprop="name">${name}</cite>${aClose}`;
  2140. return `<span class="work-mention" itemscope itemtype="https://schema.org/${
  2141. ty && ty !== "Poem" ? ty : "CreativeWork"
  2142. }">${authorStr}${cite}${dateStr}</span>`;
  2143. },
  2144. );
  2145. eleventyConfig.addPairedShortcode(
  2146. "note",
  2147. content =>
  2148. '<div role="note">' +
  2149. '<img class="note-i has-transparency" alt="&#x2139;&#xfe0f;" decoding="async" src="/img/i.svg">' +
  2150. content +
  2151. "</div>",
  2152. );
  2153. eleventyConfig.addPairedShortcode(
  2154. "eg",
  2155. content =>
  2156. '<div class="eg" role="note">' +
  2157. '<img class="eg-eg has-transparency" alt="For example: " decoding="async" src="/img/eg.svg">' +
  2158. content +
  2159. "</div>",
  2160. );
  2161. eleventyConfig.addPairedShortcode(
  2162. "aside",
  2163. content =>
  2164. '<aside class="excursion">' +
  2165. '<img class="thought-bubble has-transparency" alt="&#x1f4ad;" decoding="async" src="/img/thought.svg">' +
  2166. content +
  2167. "</aside>",
  2168. );
  2169. eleventyConfig.addPairedShortcode("bq", async function (content, cite) {
  2170. const [href, sauce, clazz] =
  2171. cite.startsWith("http") || cite.startsWith("//")
  2172. ? [cite, "source", ""]
  2173. : [
  2174. `#BIB--${cite}`,
  2175. await getCiteIdByKey(this.page.inputPath, cite),
  2176. " cite",
  2177. ];
  2178. return (
  2179. `<blockquote cite="${href}" class="has-inner-cite">` +
  2180. content +
  2181. `<footer><small><cite class="blockquote-src${clazz}">` +
  2182. `<a href="${href}"><span class="src-brace">[</span>${sauce}<span class="src-brace">]</span></a>` +
  2183. "</cite></small></footer>" +
  2184. "</blockquote>"
  2185. );
  2186. });
  2187. eleventyConfig.addShortcode("img", async function (src, alt) {
  2188. return makeImg(this, src, alt);
  2189. });
  2190. eleventyConfig.addPairedShortcode(
  2191. "imgdet",
  2192. async function (content, src, alt) {
  2193. return imgDet(this, content, src, alt);
  2194. },
  2195. );
  2196. eleventyConfig.addShortcode("ly", async function (srcBase, alt) {
  2197. return makeLy(this, srcBase, alt);
  2198. });
  2199. eleventyConfig.addShortcode("refs", async function () {
  2200. const lib = await parseLibrary(this.page.inputPath);
  2201. lib.entries.sort((e0, e1) => {
  2202. const lastName0 = e0.fields.author[0].lastName;
  2203. const lastName1 = e1.fields.author[0].lastName;
  2204. if (lastName0 > lastName1) {
  2205. return 1;
  2206. } else if (lastName0 < lastName1) {
  2207. return -1;
  2208. }
  2209. const firstName0 = e0.fields.author[0].firstName;
  2210. const firstName1 = e1.fields.author[1].firstName;
  2211. if (firstName0 > firstName1) {
  2212. return 1;
  2213. } else if (firstName0 < firstName1) {
  2214. return -1;
  2215. }
  2216. const year0 = e0.fields.year;
  2217. const year1 = e1.fields.year;
  2218. if (year0 > year1) {
  2219. return 1;
  2220. } else if (year0 < year1) {
  2221. return -1;
  2222. }
  2223. return 0;
  2224. });
  2225. let refs = `<section role="doc-endnotes">${makeHeading(
  2226. "References",
  2227. "refs",
  2228. 2,
  2229. )}<table class="refs-table"><tbody>`;
  2230. for (const entry of lib.entries) {
  2231. refs +=
  2232. `<tr id="BIB--${entry.key}">` +
  2233. `<th scope="row"><a href="#BIB--${entry.key}">` +
  2234. `<span class="src-brace">[</span>${getCiteId(
  2235. entry.fields,
  2236. )}<span class="src-brace">]</span>` +
  2237. '</a></th><td itemprop="citation" ' +
  2238. `itemscope itemtype="${entryTypeToItemType(entry.type)}">`;
  2239. const addPersons = (arr, ed) => {
  2240. for (const [i, person] of arr.entries()) {
  2241. const nameArr = [];
  2242. if (person.prefix) {
  2243. nameArr.push(
  2244. `<span itemprop="honorificPrefix">${person.prefix}</span>`,
  2245. );
  2246. }
  2247. if (person.name) {
  2248. nameArr.push(
  2249. `<span itemprop="name">${person.name}</span>`,
  2250. );
  2251. } else {
  2252. if (person.firstName) {
  2253. let first = true;
  2254. for (const s of person.firstName.split(WS_RE)) {
  2255. nameArr.push(
  2256. `<span itemprop="${
  2257. first ? "givenName" : "additionalName"
  2258. }">${s}</span>`,
  2259. );
  2260. first = false;
  2261. }
  2262. }
  2263. if (person.lastName) {
  2264. nameArr.push(
  2265. `<span itemprop="familyName">${person.lastName}</span>`,
  2266. );
  2267. }
  2268. }
  2269. if (person.suffix) {
  2270. nameArr.push(
  2271. `<span itemprop="honorificSuffix">${person.suffix}</span>`,
  2272. );
  2273. }
  2274. const last = i === arr.length - 1;
  2275. const name = nameArr.join(" ");
  2276. const trailing = (() => {
  2277. if (last) {
  2278. if (ed) {
  2279. if (arr.length > 1) {
  2280. return ' (<abbr title="editors">eds.</abbr>). ';
  2281. }
  2282. return ' (<abbr title="editor">ed.</abbr>). ';
  2283. }
  2284. return ". ";
  2285. }
  2286. if (arr.length === 2) {
  2287. return " ";
  2288. }
  2289. return ", ";
  2290. })();
  2291. refs +=
  2292. `${
  2293. last && arr.length > 1 ? "&amp; " : ""
  2294. }<b itemprop="${
  2295. ed ? "editor" : "author"
  2296. }" itemscope itemtype="https://schema.org/Person" ` +
  2297. `class="ref-author">${name}</b>${trailing}`;
  2298. }
  2299. };
  2300. if (entry.fields.author) {
  2301. addPersons(entry.fields.author);
  2302. }
  2303. if (entry.fields.booktitle || entry.fields.journal) {
  2304. refs += `\u{201c}<span itemprop="name headline">${entry.fields.title
  2305. .replaceAll("\u{201c}", "\u{2018}")
  2306. .replaceAll(
  2307. "\u{201d}",
  2308. "\u{2019}",
  2309. )}</span>\u{201d}, in <span itemprop="isPartOf" itemscope itemtype="https://schema.org/${
  2310. entry.fields.volume || entry.fields.number
  2311. ? "PublicationIssue"
  2312. : "Periodical"
  2313. }"><cite itemprop="name headline">${
  2314. entry.fields.booktitle
  2315. ? entry.fields.booktitle
  2316. : entry.fields.journal
  2317. }</cite>`;
  2318. if (entry.fields.volume) {
  2319. refs += ` vol. <span itemprop="issueNumber">${entry.fields.volume}`;
  2320. if (entry.fields.number) {
  2321. refs += ` (${entry.fields.number})</span>`;
  2322. } else {
  2323. refs += "</span>";
  2324. }
  2325. } else if (entry.fields.number) {
  2326. refs += ` (<span itemprop="issueNumber">${entry.fields.number}</span>)`;
  2327. }
  2328. refs += "</span>";
  2329. } else {
  2330. refs += `<cite itemprop="name headline">${entry.fields.title}</cite>`;
  2331. }
  2332. if (entry.fields.series) {
  2333. refs += ` (<span itemprop="isPartOf">${entry.fields.series}</span>)`;
  2334. }
  2335. if (entry.fields.edition) {
  2336. refs += `, <span itemprop="version">${entry.fields.edition} edition</span>`;
  2337. }
  2338. if (entry.fields.chapter) {
  2339. refs += `, ch. ${entry.fields.chapter}`;
  2340. }
  2341. if (entry.fields.pages) {
  2342. refs += `, ${
  2343. entry.fields.pages.includes("\u{2013}") ? "p" : ""
  2344. }p. <span itemprop="pagination">${entry.fields.pages}</span>`;
  2345. }
  2346. refs += ". ";
  2347. if (entry.fields.editor) {
  2348. addPersons(entry.fields.editor, true);
  2349. }
  2350. const datetime = `${entry.fields.year}${
  2351. entry.fields.month
  2352. ? "-" + entry.fields.month.padStart(2, "0")
  2353. : ""
  2354. }`;
  2355. const date = `${
  2356. entry.fields.month ? MONTHS[+entry.fields.month - 1] + " " : ""
  2357. }${entry.fields.year}`;
  2358. refs +=
  2359. '<time itemprop="datePublished" class="ref-time" ' +
  2360. `datetime="${datetime}">${date}</time>. `;
  2361. if (entry.fields.publisher) {
  2362. for (const [
  2363. i,
  2364. publisher,
  2365. ] of entry.fields.publisher.entries()) {
  2366. refs += `<span itemprop="publisher">${publisher}</span>`;
  2367. if (i !== entry.fields.publisher.length - 1) {
  2368. refs += ", ";
  2369. }
  2370. }
  2371. if (entry.fields.address) {
  2372. refs += `; ${entry.fields.address}`;
  2373. }
  2374. refs += ".";
  2375. } else if (entry.fields.address) {
  2376. refs += entry.fields.address + ".";
  2377. }
  2378. if (entry.fields.howpublished) {
  2379. refs += ` Published at: ${entry.fields.howpublished
  2380. .replaceAll("<a", '<code><a itemprop="url"')
  2381. .replaceAll("</a>", "</a></code>")}.`;
  2382. }
  2383. if (entry.fields.doi) {
  2384. refs += ` <code><a itemprop="sameAs" href="https://doi.org/${entry.fields.doi}">doi:${entry.fields.doi}</a></code>`;
  2385. }
  2386. refs = refs.trimEnd();
  2387. refs += "</td></tr>";
  2388. }
  2389. refs = refs.replaceAll("'", "\u{2019}");
  2390. refs += "</tbody></table></section>";
  2391. return refs;
  2392. });
  2393. eleventyConfig.addShortcode("cite", async function (key, inline) {
  2394. const citeId = await getCiteIdByKey(this.page.inputPath, key);
  2395. const tagName = inline ? "span" : "sup";
  2396. return (
  2397. `<${tagName} class="cite"><a href="#BIB--${key}">` +
  2398. `<span class="src-brace">[</span>${citeId}<span class="src-brace">]</span>` +
  2399. `</a></${tagName}>`
  2400. );
  2401. });
  2402. eleventyConfig.addPairedShortcode("toc", content => {
  2403. let toc = '<nav class="toc" aria-label="table of contents">';
  2404. const $ = cheerio.load(content, null, false);
  2405. const headings = $("h1, h2, h3, h4, h5, h6");
  2406. const levelStack = [0];
  2407. for (const heading of headings) {
  2408. $(heading)
  2409. .find("a")
  2410. .each((_, a) => {
  2411. a.tagName = "SPAN";
  2412. $(a)
  2413. .removeAttr("href")
  2414. .removeAttr("referrerpolicy")
  2415. .removeAttr("rel")
  2416. .removeAttr("target")
  2417. .removeAttr("download")
  2418. .removeAttr("itemprop");
  2419. });
  2420. const level = parseInt(heading.tagName[1], 10);
  2421. while (level < levelStack[levelStack.length - 1]) {
  2422. levelStack.pop();
  2423. toc += "</li></ol>";
  2424. }
  2425. if (level > levelStack[levelStack.length - 1]) {
  2426. levelStack.push(level);
  2427. toc += "<ol><li>";
  2428. } else {
  2429. toc += "</li><li>";
  2430. }
  2431. toc += `<a href="#${$(heading).attr("id")}">`;
  2432. toc += $(heading).html();
  2433. toc += "</a>";
  2434. }
  2435. toc += "</li></ol>".repeat(levelStack.length - 1);
  2436. return toc + "</nav>" + content;
  2437. });
  2438. eleventyConfig.addFilter("serieses", function () {
  2439. const serieses = [];
  2440. for (const post of this.ctx.collections.all.filter(
  2441. p => p.data.series,
  2442. )) {
  2443. const series = serieses.find(s => s.name === post.data.series);
  2444. if (series) {
  2445. series.posts.push(post);
  2446. } else {
  2447. serieses.push({ name: post.data.series, posts: [post] });
  2448. }
  2449. }
  2450. return serieses;
  2451. });
  2452. eleventyConfig.addFilter("commontags", posts => {
  2453. const tagCounts = new Map();
  2454. posts.forEach(post =>
  2455. post.data.tags.forEach(tag =>
  2456. tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1),
  2457. ),
  2458. );
  2459. return Array.from(tagCounts.entries())
  2460. .sort(([, ct0], [, ct1]) => ct1 - ct0)
  2461. .reduce((accu, [tag, ct]) => {
  2462. if (ct > 1) {
  2463. accu.push(tag);
  2464. }
  2465. return accu;
  2466. }, []);
  2467. });
  2468. eleventyConfig.addShortcode("articleHeader", function () {
  2469. const date = this.ctx.date;
  2470. const tagsList = this.ctx.tags
  2471. .map(
  2472. tag =>
  2473. `<a href="${tagPaths.get(
  2474. tag,
  2475. )}" rel="tag" itemprop="keywords">${htmlEsc(tag)}</a>`,
  2476. )
  2477. .join(", ");
  2478. const series = this.ctx.series;
  2479. return `<header class="article-header"><span id="start"></span>\
  2480. <div class="post-info">\
  2481. <time itemprop="datePublished" datetime="${date}">${date}</time>\
  2482. <span class="tags-listing"><span class="tags-colon">Tags:&nbsp;</span>\
  2483. <span class="tags-list">${tagsList}</span></span>\
  2484. </div></header>${
  2485. series
  2486. ? '<div class="assoc-series"><span class="part-of-a-series">' +
  2487. 'Part of a series:</span> <cite itemprop="isPartOf" ' +
  2488. 'itemscope itemtype="https://schema.org/Collection">' +
  2489. '<a href="/series/#' +
  2490. slugify(series) +
  2491. '" itemprop="url"><span itemprop="name headline">' +
  2492. htmlEsc(series) +
  2493. "</span></a></cite></div>"
  2494. : ""
  2495. }`;
  2496. });
  2497. const TEXT = 0;
  2498. const ANNOT = 1;
  2499. const ANNOT_EXIT = 2;
  2500. eleventyConfig.addShortcode(
  2501. "ruby",
  2502. (text, lang, annotLang, annotLang1) => {
  2503. let state = TEXT;
  2504. const components = [[""]];
  2505. for (const c of text) {
  2506. switch (c) {
  2507. case "{": {
  2508. switch (state) {
  2509. case TEXT:
  2510. case ANNOT_EXIT: {
  2511. state = ANNOT;
  2512. components[components.length - 1].push("");
  2513. break;
  2514. }
  2515. case ANNOT: {
  2516. console.error(
  2517. "Stray `{` inside of ruby annotation: " +
  2518. text,
  2519. );
  2520. throw "";
  2521. }
  2522. }
  2523. break;
  2524. }
  2525. case "}": {
  2526. switch (state) {
  2527. case TEXT:
  2528. case ANNOT_EXIT: {
  2529. console.error(
  2530. "Stray `}` with no preceding `{`: " + text,
  2531. );
  2532. throw "";
  2533. }
  2534. case ANNOT: {
  2535. state = ANNOT_EXIT;
  2536. break;
  2537. }
  2538. }
  2539. break;
  2540. }
  2541. default: {
  2542. switch (state) {
  2543. case TEXT:
  2544. case ANNOT: {
  2545. const comp = components[components.length - 1];
  2546. comp[comp.length - 1] += c;
  2547. break;
  2548. }
  2549. case ANNOT_EXIT: {
  2550. state = TEXT;
  2551. components.push([c]);
  2552. break;
  2553. }
  2554. }
  2555. break;
  2556. }
  2557. }
  2558. }
  2559. let ret = lang ? `<ruby lang="${lang}">` : "<ruby>";
  2560. const annotLangAttr = annotLang ? ` lang="${annotLang}"` : "";
  2561. const annotLang1Attr = annotLang1 ? ` lang="${annotLang1}"` : "";
  2562. for (const comp of components) {
  2563. if (comp.length > 2) {
  2564. ret += "<ruby>".repeat(comp.length - 2);
  2565. }
  2566. for (const [i, subcomp] of comp.entries()) {
  2567. switch (i) {
  2568. case 0: {
  2569. ret += subcomp;
  2570. break;
  2571. }
  2572. case 1: {
  2573. ret += `<rp>(</rp><rt${annotLangAttr}>${subcomp}</rt><rp>)</rp>`;
  2574. break;
  2575. }
  2576. default: {
  2577. ret += `</ruby><rp>(</rp><rt${annotLang1Attr}>${subcomp}</rt><rp>)</rp>`;
  2578. break;
  2579. }
  2580. }
  2581. }
  2582. }
  2583. ret += "</ruby>";
  2584. return ret;
  2585. },
  2586. );
  2587. function figTypeToText(type, abbr) {
  2588. switch (type) {
  2589. case "fig":
  2590. return abbr
  2591. ? '<abbr class="fig-type" title="Figure">Fig.</abbr>'
  2592. : "Figure";
  2593. case "table":
  2594. return "Table";
  2595. case "list":
  2596. return "List";
  2597. default: {
  2598. console.error(`Unknown figure type: "${type}"`);
  2599. throw "";
  2600. }
  2601. }
  2602. }
  2603. function figTypeInitial(type) {
  2604. switch (type) {
  2605. case "list":
  2606. return '<abbr class="fig-type" title="List">L</abbr>';
  2607. default: {
  2608. console.error(`Cannot make initial of figure type "${type}"`);
  2609. throw "";
  2610. }
  2611. }
  2612. }
  2613. eleventyConfig.addShortcode("figref", (id, type = "fig", li, noInit) =>
  2614. li
  2615. ? `<a href="#${id}-${li}">(${
  2616. noInit ? "" : figTypeInitial(type) + "{{{#" + id + "--N}}}"
  2617. }${li}.)</a>`
  2618. : `<a href="#${id}">${figTypeToText(
  2619. type,
  2620. true,
  2621. )} {{{#${id}--N}}}</a>`,
  2622. );
  2623. eleventyConfig.addPairedShortcode("fig", (content, id, type = "fig") => {
  2624. const $ = cheerio.load(
  2625. `<figure id="${id}" data-fig-type="${type}">${content}</figure>`,
  2626. null,
  2627. false,
  2628. );
  2629. const firstP = $(`#${id} > figcaption > p`)[0];
  2630. $(firstP).prepend(" ");
  2631. $(firstP).prepend(
  2632. `<b><a href="#${id}" class="figcaption-label">${figTypeToText(
  2633. type,
  2634. )} {{{#${id}--N}}}</a>:</b>`,
  2635. );
  2636. return $.html();
  2637. });
  2638. eleventyConfig.addPairedShortcode("figdomain", content => {
  2639. const $ = cheerio.load(content, null, false);
  2640. const figs = $("figure[id][data-fig-type]");
  2641. const ordering = new Map();
  2642. for (const fig of figs) {
  2643. const id = $(fig).attr("id");
  2644. const figType = $(fig).attr("data-fig-type");
  2645. if (ordering.has(figType)) {
  2646. ordering.get(figType).push(id);
  2647. } else {
  2648. ordering.set(figType, [id]);
  2649. }
  2650. }
  2651. ordering.forEach(ids =>
  2652. ids.forEach(
  2653. (id, i) =>
  2654. (content = content.replaceAll(
  2655. `{{{#${id}--N}}}`,
  2656. `${1 + i}`,
  2657. )),
  2658. ),
  2659. );
  2660. return content;
  2661. });
  2662. eleventyConfig.addPairedShortcode("glosslist", content => {
  2663. const $ = cheerio.load(
  2664. `<dl class="gloss-list">${content}</dl>`,
  2665. null,
  2666. false,
  2667. );
  2668. for (const dt of $("dl:first > dt")) {
  2669. const text = $(dt).text().trim();
  2670. const innerHtml = $(dt).html();
  2671. const id = "gloss-" + slugify(text);
  2672. $(dt).attr("id", id);
  2673. $(dt).attr("itemprop", "hasDefinedTerm");
  2674. $(dt).html(`<a href="#${id}">${innerHtml}</a>`);
  2675. }
  2676. return $.html();
  2677. });
  2678. eleventyConfig.addShortcode(
  2679. "gloss",
  2680. (term, text) =>
  2681. `<a class="gloss-ref" href="/glossary/#gloss-${slugify(term)}">${
  2682. text ? text : term
  2683. }</a>`,
  2684. );
  2685. const CODEPOINT_CAPITAL_A = 0x0041;
  2686. const CODEPOINT_SMALL_A = 0x0061;
  2687. const ROMAN = [
  2688. ["M", 1_000],
  2689. ["CM", 900],
  2690. ["D", 500],
  2691. ["CD", 400],
  2692. ["C", 100],
  2693. ["XC", 90],
  2694. ["L", 50],
  2695. ["XL", 40],
  2696. ["X", 10],
  2697. ["IX", 9],
  2698. ["V", 5],
  2699. ["IV", 4],
  2700. ["I", 1],
  2701. ];
  2702. function liMarker(n, type = "1") {
  2703. if (type !== "1" && n < 1) {
  2704. console.error(`n < 1 for marker type "${type}"`);
  2705. throw "";
  2706. }
  2707. const letterMarker = baseCodepoint => {
  2708. const n0 = n - 1;
  2709. if (n0 < 26) {
  2710. return String.fromCodePoint(baseCodepoint + n0);
  2711. }
  2712. const residue = n0 % 26;
  2713. return (
  2714. liMarker((n0 - residue) / 26, type) +
  2715. String.fromCodePoint(baseCodepoint + residue)
  2716. );
  2717. };
  2718. const romanMarker = lower =>
  2719. ROMAN.reduce(
  2720. ([m, s], [r, rVal]) => {
  2721. const q = Math.trunc(m / rVal);
  2722. return [
  2723. m - q * rVal,
  2724. s + (lower ? r.toLowerCase() : r).repeat(q),
  2725. ];
  2726. },
  2727. [n, ""],
  2728. )[1];
  2729. switch (type) {
  2730. case "1":
  2731. return "" + n;
  2732. case "A":
  2733. return letterMarker(CODEPOINT_CAPITAL_A);
  2734. case "a":
  2735. return letterMarker(CODEPOINT_SMALL_A);
  2736. case "I":
  2737. return romanMarker();
  2738. case "i":
  2739. return romanMarker(true);
  2740. default: {
  2741. console.error(`Unrecognized <li> marker type: "${type}"`);
  2742. throw "";
  2743. }
  2744. }
  2745. }
  2746. eleventyConfig.addPairedShortcode("ol", (content, baseId, type = "1") => {
  2747. const $ = cheerio.load(
  2748. `<ol type="${type}">${content}</ol>`,
  2749. null,
  2750. false,
  2751. );
  2752. $("ol:first > li").each((i, elem) =>
  2753. $(elem).attr("id", `${baseId}-${liMarker(i + 1, type)}`),
  2754. );
  2755. return $.html();
  2756. });
  2757. eleventyConfig.addPairedShortcode(
  2758. "enmark",
  2759. (content, id) =>
  2760. `<span class="enmark-span">${content}<sup class="enmark"><a id="${id}--MARK" href="#${id}">[?]</a></sup></span>`,
  2761. );
  2762. eleventyConfig.addPairedShortcode(
  2763. "en",
  2764. (content, id) =>
  2765. `<li class="en" id="${id}"><a class="en-arrow" href="#${id}--MARK" aria-label="back to the main text">&larrhk;&#xfe0e;</a> ${content}</li>`,
  2766. );
  2767. eleventyConfig.addPairedShortcode("endomain", (content, lvl) => {
  2768. const $ = cheerio.load(content, null, false);
  2769. const firstHeading = $("h1, h2, h3, h4, h5, h6")[0];
  2770. const level = lvl
  2771. ? lvl
  2772. : Math.min(parseInt(firstHeading.tagName[1], 10) + 1, 6);
  2773. const ens = $("li.en");
  2774. const enIds = [];
  2775. for (const en of ens) {
  2776. enIds.push($(en).attr("id"));
  2777. }
  2778. const enMarks = $("sup.enmark > a");
  2779. const enMarkIds = new Set();
  2780. for (const enMark of enMarks) {
  2781. const id = $(enMark).attr("id");
  2782. if (enMarkIds.has(id)) {
  2783. $(enMark).removeAttr("id");
  2784. } else {
  2785. enMarkIds.add(id);
  2786. }
  2787. const enId = $(enMark).attr("href").slice(1);
  2788. const ix = enIds.indexOf(enId);
  2789. $(enMark).text(`[${1 + ix}]`);
  2790. }
  2791. const firstHeadingId = $(firstHeading).attr("id");
  2792. const olId = `${firstHeadingId}--ENS`;
  2793. $(
  2794. `<section role="doc-endnotes">${makeHeading(
  2795. "Endnotes",
  2796. `${firstHeadingId}-endnotes`,
  2797. level,
  2798. )}<ol type="1" id="${olId}"></ol></section>`,
  2799. ).insertBefore($("li.en").get(0));
  2800. ens.remove();
  2801. ens.appendTo(`#${olId}`);
  2802. return $.html();
  2803. });
  2804. eleventyConfig.addTransform("hyphens", function (content) {
  2805. if (this.page.url === "/atom.xml") {
  2806. return content;
  2807. }
  2808. const $ = cheerio.load(content);
  2809. replaceHyphenMinuses($("body")[0]);
  2810. return $.html();
  2811. });
  2812. const externalUriRe = /^([a-zA-Z][a-zA-Z0-9\+\.\-]*:.|\/\/)/;
  2813. eleventyConfig.addTransform("addrels", function (content) {
  2814. if (this.page.url === "/atom.xml") {
  2815. return content;
  2816. }
  2817. const $ = cheerio.load(content);
  2818. $("a").each((_, a) => {
  2819. const href = $(a).attr("href");
  2820. if (externalUriRe.test(href)) {
  2821. let rel = ($(a).attr("rel") ?? "").trim();
  2822. if (rel.length > 0) {
  2823. rel += " ";
  2824. }
  2825. rel += "external noopener noreferrer";
  2826. $(a).attr("rel", rel);
  2827. }
  2828. });
  2829. return $.html();
  2830. });
  2831. const hasLetterRe = /[a-zA-Z]/;
  2832. eleventyConfig.addTransform("datetimes", function (content) {
  2833. if (this.page.url === "/atom.xml") {
  2834. return content;
  2835. }
  2836. const $ = cheerio.load(content);
  2837. $("time[datetime]").each((_, time) => {
  2838. const datetime = $(time).attr("datetime");
  2839. if (!$(time).attr("title") && hasLetterRe.test(datetime)) {
  2840. $(time).attr("title", datetime);
  2841. }
  2842. });
  2843. return $.html();
  2844. });
  2845. eleventyConfig.addTransform("titlelangs", function (content) {
  2846. if (this.page.url === "/atom.xml") {
  2847. return content;
  2848. }
  2849. const $ = cheerio.load(content);
  2850. $("[lang]").each((_, elem) => {
  2851. const lang = $(elem).attr("lang");
  2852. const title = $(elem).attr("title");
  2853. if (!lang || !!title) {
  2854. return;
  2855. }
  2856. const newTitle = langToTitle(lang);
  2857. $(elem).attr("title", newTitle);
  2858. });
  2859. return $.html();
  2860. });
  2861. eleventyConfig.setFrontMatterParsingOptions({
  2862. language: "json",
  2863. });
  2864. return {
  2865. dir: {
  2866. input: "src",
  2867. output: "dist",
  2868. },
  2869. htmlTemplateEngine: "njk",
  2870. };
  2871. }