eleventy.config.js 111 KB

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