1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166 |
- import * as cheerio from "cheerio";
- import * as fs from "node:fs/promises";
- import * as nodeChildProcess from "node:child_process";
- import * as path from "node:path";
- import * as util from "node:util";
- import bib from "@retorquere/bibtex-parser";
- import { optimize } from "svgo";
- import sharp from "sharp";
- import slugify from "@sindresorhus/slugify";
- import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
- const exec = util.promisify(nodeChildProcess.exec);
- const NUM_RE = /^(-?)[0-9]+/;
- const WS_RE = /[\s\r\n]+/u;
- const ANCHOR_RE = /<a(\s|>)/i;
- const HTML_ESC_LUT = new Map([
- ["&", "&"],
- ['"', """],
- ["'", "'"],
- ["<", "<"],
- [">", ">"],
- ]);
- const PERSONAGE_TYPES = new Set(["Person", "MusicGroup"]);
- const ENTRY_TO_ITEM = new Map([
- ["article", "Article"],
- ["book", "Book"],
- ["inbook", "Chapter"],
- ["incollection", "Article"],
- ["misc", "WebPage"],
- ]);
- const MONTHS = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ];
- async function walk(dirPath) {
- return Promise.all(
- await fs.readdir(dirPath, { withFileTypes: true }).then(entries =>
- entries.map(entry => {
- const childPath = path.join(dirPath, entry.name);
- return entry.isDirectory() ? walk(childPath) : childPath;
- }),
- ),
- );
- }
- function personage(displayName, longName, href, ty) {
- if (ty && !PERSONAGE_TYPES.has(ty)) {
- console.error("Bad personage type: " + ty);
- throw "";
- }
- const b = `<b class="personage"${
- ty ? ' itemscope itemtype="https://schema.org/' + ty + '"' : ""
- }>`;
- const a = href ? `<a${ty ? ' itemprop="url"' : ""} href="${href}">` : "";
- const inner = (() => {
- if (ty) {
- if (longName) {
- return (
- `<abbr title="${longName}" itemprop="name">${displayName}</abbr>` +
- `<meta itemprop="alternateName" content="${longName}" />`
- );
- }
- return `<span itemprop="name">${displayName}</span>`;
- }
- if (longName) {
- return `<abbr title="${longName}">${displayName}</abbr>`;
- }
- return displayName;
- })();
- return `${b}${a}${inner}${a ? "</a>" : ""}</b>`;
- }
- function entryTypeToItemType(entryTy) {
- const itemTy =
- ENTRY_TO_ITEM.get(entryTy) ??
- (() => {
- console.error("Bad bib entry type: " + entryTy);
- throw "";
- })();
- return `https://schema.org/${itemTy}`;
- }
- function htmlEsc(s) {
- return s.replace(/[&"'<>]/g, c => HTML_ESC_LUT.get(c));
- }
- function makeHeading(content, id, level) {
- if (!id) {
- const $ = cheerio.load(content, null, false);
- id = slugify($.text());
- }
- const labelText = "#".repeat(level);
- return (
- '<div class="heading-wrapper">' +
- `<h${level} aria-labelledby="${id}--LABEL" id="${id}">` +
- content +
- `</h${level}>` +
- `<a id="${id}--LABEL" class="h${level}-label heading-label" href="#${id}">` +
- labelText +
- "</a>" +
- "</div>"
- );
- }
- function replaceHyphenMinuses(elem) {
- if (elem.type === "text") {
- elem.data = elem.data.replaceAll("-", "\u2010");
- }
- const title = elem.attribs?.title;
- if (title) {
- elem.attribs.title = title.replaceAll("-", "\u2010");
- }
- if (elem.children) {
- for (const child of elem.children) {
- replaceHyphenMinuses(child);
- }
- }
- }
- function replaceGs(elem) {
- if (elem.type === "text") {
- elem.data = elem.data.replaceAll("g", "\u0261");
- }
- if (elem.children) {
- for (const child of elem.children) {
- replaceGs(child);
- }
- }
- }
- function srcToReqPath(thiz, src) {
- return path.resolve(thiz.page.url, src);
- }
- function srcToFsPath(thiz, src) {
- return path.join(thiz.eleventy.env.root, "src", srcToReqPath(thiz, src));
- }
- async function makeImg(thiz, src, alt, detId) {
- const fsPath = srcToFsPath(thiz, src);
- const reqPath = srcToReqPath(thiz, src);
- const { width, height, hasAlpha } = await sharp(fsPath).metadata();
- if (!width || !height) {
- console.error(
- `Bad dimensions for ${fsPath}: ${width}\u{00d7}${height}`,
- );
- throw "";
- }
- const ly = src.endsWith(".ly.svg");
- const imgClass = hasAlpha
- ? `class="has-transparency${ly ? " ly-img" : ""}"`
- : "";
- const aClasses = (ly ? " ly-img-a" : "") + (detId ? " has-det" : "");
- const ariaDetails = detId ? `aria-details="${detId}"` : "";
- const wh = ly ? "" : `width="${width}" height="${height}"`;
- return (
- `<a href="${reqPath}" class="img-a${aClasses}">` +
- `<img ${ariaDetails} alt="${alt}" loading="lazy" decoding="async" ${wh} ${imgClass} src="${reqPath}">` +
- "</a>"
- );
- }
- async function imgDet(
- thiz,
- content,
- src,
- alt,
- summary = "Transcription of the above image",
- ) {
- const id = slugify(src) + "--DET";
- const img = await makeImg(thiz, src, alt, id);
- return (
- `${img}<details id="${id}" class="det">` +
- `<summary>${summary}</summary>${content}` +
- "</details>"
- );
- }
- async function makeLy(thiz, srcBase, alt) {
- const lyReqPath = srcToReqPath(thiz, `${srcBase}.ly`);
- const midReqPath = srcToReqPath(thiz, `${srcBase}.ly.mid`);
- const opusReqPath = srcToReqPath(thiz, `${srcBase}.ly.mid.opus`);
- const detContent =
- `<div class="ly-det-content"><audio preload="metadata" controls src="${opusReqPath}"></audio>` +
- `<div class="ly-det-a-container"><a class="ly-src" href="${lyReqPath}">LilyPond source</a>` +
- `<a class="midi-src" type="audio/midi" href="${midReqPath}">MIDI file (<abbr title="standard MIDI file">SMF</abbr>)</a></div></div>`;
- return await imgDet(
- thiz,
- detContent,
- `${srcBase}.ly.svg`,
- alt,
- 'Playable <abbr title="Musical Instrument Digital Interface">MIDI</abbr> rendering and LilyPond source',
- );
- }
- async function buildLy(inputDir) {
- const postDir = path.join(inputDir, "post/");
- const paths = (await walk(postDir)).flat(Number.POSITIVE_INFINITY);
- await Promise.all(
- paths
- .filter(p => p.endsWith(".ly"))
- .map(lyPath =>
- exec(
- `lilypond --loglevel=WARNING -dpoint-and-click=#f -ddelete-intermediate-files -dmidi-extension=mid --svg -o${lyPath} ${lyPath}`,
- ).then(out =>
- Promise.all([
- (async () =>
- out?.stderr?.trim()
- ? console.warn(out.stderr)
- : undefined)(),
- (async () => {
- const svgPath = `${lyPath}.svg`;
- const s = await fs.readFile(svgPath, {
- encoding: "utf8",
- });
- await fs.writeFile(
- svgPath,
- s.replaceAll(
- 'font-family="serif"',
- "font-family=\"'C059',serif\"",
- ),
- {
- mode: 0o664,
- },
- );
- await exec(
- `inkscape --export-type=svg --vacuum-defs -l -D -T --export-area-snap --export-overwrite ${svgPath}`,
- );
- const svgStr = await fs.readFile(svgPath, {
- encoding: "utf8",
- });
- const $ = cheerio.load(svgStr, {
- xml: true,
- });
- $("svg").attr("color", "#000000");
- $("svg").removeAttr("width");
- $("svg").removeAttr("height");
- const svgStrColored = $.xml();
- const res = optimize(svgStrColored, {
- multipass: true,
- path: svgPath,
- });
- await fs.writeFile(
- svgPath,
- res.data.replaceAll(
- 'font-family="serif"',
- "font-family=\"'Gentium Plus',serif\"",
- ),
- {
- mode: 0o664,
- },
- );
- })(),
- exec(
- `fluidsynth -i -q -C0 -R0 -K16 -Twav -Os16 -F ${lyPath}.mid.wav ${path.join(
- inputDir,
- "..",
- "scratch",
- "GeneralUser_GS_v2.0.0.sf2",
- )} ${lyPath}.mid`,
- )
- .then(() =>
- exec(
- `opusenc --quiet --bitrate 128 --vbr --comp 10 --discard-comments --discard-pictures --padding 0 ${lyPath}.mid.wav ${lyPath}.mid.opus`,
- ),
- )
- .then(() => fs.rm(`${lyPath}.mid.wav`)),
- ]),
- ),
- ),
- );
- }
- async function parseLibrary(inputPath) {
- const bibText = await fs.readFile(`${path.dirname(inputPath)}/refs.bib`, {
- encoding: "utf-8",
- });
- return bib.parseAsync(bibText, { english: false });
- }
- async function getCiteIdByKey(inputPath, key) {
- const lib = await parseLibrary(inputPath);
- const entry = lib.entries.find(entry => key === entry.key);
- if (!entry) {
- console.error(`Bad cite key: "${key}". Input path: ${inputPath}\n`);
- throw "";
- }
- return getCiteId(entry.fields);
- }
- function joinNames(xs, surnames) {
- if (!xs) {
- return;
- }
- const filtered = surnames
- ? xs.flatMap(a => (a.lastName ? [a.lastName] : []))
- : xs.filter(s => s);
- if (filtered.length === 1) {
- return filtered[0].slice(0, 3);
- }
- if (filtered.length >= 2 && filtered.length <= 4) {
- return filtered.reduce((accu, name) => accu + name.slice(0, 1), "");
- }
- if (filtered.length > 0) {
- return (
- filtered
- .slice(0, 3)
- .reduce((accu, name) => accu + name.slice(0, 1), "") + "+"
- );
- }
- return;
- }
- function getCiteId(fields) {
- const alpha = (() => {
- const authorJoined = joinNames(fields.author, true);
- if (authorJoined) {
- return authorJoined;
- }
- const bookauthorJoined = joinNames(fields.bookauthor, true);
- if (bookauthorJoined) {
- return bookauthorJoined;
- }
- const editorJoined = joinNames(fields.editor, true);
- if (editorJoined) {
- return editorJoined;
- }
- const editorsJoined = joinNames(fields.editors, true);
- if (editorsJoined) {
- return editorsJoined;
- }
- const editoraJoined = joinNames(fields.editora, true);
- if (editoraJoined) {
- return editoraJoined;
- }
- const editorbJoined = joinNames(fields.editorb, true);
- if (editorbJoined) {
- return editorbJoined;
- }
- const scriptwriterJoined = joinNames(fields.scriptwriter, true);
- if (scriptwriterJoined) {
- return scriptwriterJoined;
- }
- const directorJoined = joinNames(fields.director, true);
- if (directorJoined) {
- return directorJoined;
- }
- const commentatorJoined = joinNames(fields.commentator, true);
- if (commentatorJoined) {
- return commentatorJoined;
- }
- const collaboratorJoined = joinNames(fields.collaborator, true);
- if (collaboratorJoined) {
- return collaboratorJoined;
- }
- const organizationJoined = joinNames(fields.organization);
- if (organizationJoined) {
- return organizationJoined;
- }
- const institutionJoined = joinNames(fields.institution);
- if (institutionJoined) {
- return institutionJoined;
- }
- const translatorJoined = joinNames(fields.translator, true);
- if (translatorJoined) {
- return translatorJoined;
- }
- return fields.title.slice(0, 3);
- })();
- const num = (() => {
- NUM_RE.lastIndex = 0;
- const e = NUM_RE.exec(fields.year);
- if (e) {
- const matched = e[0];
- return e[1] + matched.slice(matched.length - 2);
- }
- return fields.year.slice(fields.year.length - 2);
- })();
- return alpha + num;
- }
- const SCRIPT_RE = /^[A-Z]{4}$/;
- const REGION_RE = /^([A-Z]{2}|\d{3})$/;
- const VAR_RE = /^([A-Z\d]{5,8}|\d[A-Z\d]{3})$/;
- const PRIMARIES = new Map([
- ["ANG", "Old English"],
- ["CA", "Catalan"],
- ["CMN", "Mandarin"],
- ["DE", "High German"],
- ["EN", "English"],
- ["FR", "French"],
- ["GRC", "Ancient Greek"],
- ["JA", "Japanese"],
- ["SIT", "Proto-Sino-Tibetan"],
- ["SV", "Swedish"],
- ["UND", "[unspecified]"],
- ]);
- const SCRIPTS = new Map([
- ["BOPO", "Bopomofo (Zhùyīn)"],
- ["HANS", "simplified Sinographs"],
- ["HANT", "traditional Sinographs"],
- ["LATN", "the Latin alphabet"],
- ]);
- const REGIONS = new Map([]);
- const VARS = new Map([
- [
- "ALALC97",
- "American Library Association \u2013 Library of Congress Romanization",
- ],
- ["FONIPA", "International Phonetic Alphabet"],
- ["HEPBURN", "Hepburn Romanization"],
- ["PINYIN", "Hànyǔ Pīnyīn Romanization"],
- ]);
- function langToTitle(lang) {
- const tags = lang.toUpperCase().split("-");
- if (tags.length < 1) {
- return;
- }
- const primary = tags[0];
- if (primary === "EN" && tags.length === 1) {
- return;
- }
- const bail = () => {
- console.error("Bad `lang`:", lang);
- throw "";
- };
- let priv = false;
- let primaryText, script, region;
- const vars = [];
- for (const tag of tags.slice(1)) {
- if (priv) {
- switch (tag) {
- case "MSM": {
- if (primary !== "CMN") {
- bail();
- }
- primaryText = "Standard Mandarin";
- break;
- }
- default:
- bail();
- }
- continue;
- }
- if (SCRIPT_RE.test(tag)) {
- script = script ? bail() : tag;
- } else if (REGION_RE.test(tag)) {
- region = region ? bail() : tag;
- } else if (VAR_RE.test(tag)) {
- vars.push(tag);
- } else if (tag === "X") {
- priv = true;
- } else {
- bail();
- }
- }
- const defaultPrimaryText = PRIMARIES.get(primary) ?? bail();
- if (!primaryText) {
- primaryText = defaultPrimaryText;
- }
- const ipaIx = vars.indexOf("FONIPA");
- if (ipaIx !== -1) {
- vars.splice(ipaIx, 1);
- }
- let text =
- ipaIx === -1
- ? `${primaryText} text`
- : `phonetic transcription: ${primaryText} speech`;
- if (ipaIx !== -1 && primary === "UND") {
- return "phonetic transcription";
- }
- if (region) {
- const regionText = REGIONS.get(region) ?? bail();
- text += `, as spoken in ${regionText}`;
- }
- if (script) {
- const scriptText = SCRIPTS.get(script) ?? bail();
- text += `, written in ${scriptText}`;
- }
- for (const variant of vars) {
- const varText = VARS.get(variant) ?? bail();
- text += `, ${varText}`;
- }
- return text.replaceAll("-", "\u2010");
- }
- /** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
- export default function (eleventyConfig) {
- eleventyConfig.setUseGitIgnore(false);
- eleventyConfig.addFilter("posts", pages =>
- pages.filter(page => page.url.startsWith("/post/")),
- );
- eleventyConfig.addFilter("preview", post => {
- const $ = cheerio.load(
- post.content.split("<!-- __END_PREVIEW -->")[0],
- null,
- false,
- );
- const url = post.url + (post.url.endsWith("/") ? "" : "/");
- const anchors = $("a");
- for (const a of anchors) {
- const href = $(a).attr("href");
- if (href.startsWith("#")) {
- $(a).attr("href", url + href);
- }
- }
- return $.html();
- });
- eleventyConfig.addFilter("withtags", (pages, tagList) => {
- const tags = new Set(tagList);
- return pages.filter(page => page.data.tags.some(tag => tags.has(tag)));
- });
- eleventyConfig.addFilter("getNewestCollectionItemDate", collection =>
- collection.reduce(
- (newestDate, post) =>
- post.data.date > newestDate ? post.data.date : newestDate,
- "1970-01-01",
- ),
- );
- eleventyConfig.addFilter("htmlBaseUrl", (relativeUrl, baseUrl) => {
- if (!relativeUrl) {
- return baseUrl;
- }
- while (relativeUrl[0] === "/" || relativeUrl[0] === ".") {
- relativeUrl = relativeUrl.slice(1);
- if (!relativeUrl) {
- return baseUrl;
- }
- }
- while (baseUrl.at(-1) === "/") {
- baseUrl = baseUrl.slice(0, baseUrl.length - 1);
- }
- return `${baseUrl}/${relativeUrl}`;
- });
- eleventyConfig.addFilter(
- "atomRender",
- (content, baseUrl, absolutePostUrl) =>
- `<base href="${baseUrl}" />` +
- content.split("<!-- __END_PREVIEW -->")[0].trim() +
- `<footer>[<a href="${absolutePostUrl}"><i>…Read the full post here.</i></a>]</footer>`,
- );
- eleventyConfig.addPassthroughCopy("src/css");
- eleventyConfig.addPassthroughCopy("src/fonts");
- eleventyConfig.addPassthroughCopy("src/img");
- eleventyConfig.addPassthroughCopy("src/js/*.js");
- eleventyConfig.addPassthroughCopy("src/**/*.avif");
- eleventyConfig.addPassthroughCopy("src/**/*.webp");
- eleventyConfig.addPassthroughCopy("src/**/*.jpeg");
- eleventyConfig.addPassthroughCopy("src/**/*.svg");
- eleventyConfig.addPassthroughCopy("src/**/*.ly");
- eleventyConfig.addPassthroughCopy("src/**/*.mid");
- eleventyConfig.addPassthroughCopy("src/**/*.opus");
- eleventyConfig.addWatchTarget("src/fonts");
- eleventyConfig.addWatchTarget("src/fonts/**/*");
- eleventyConfig.addWatchTarget("src/js/*.ts");
- eleventyConfig.addWatchTarget("src/js/*.json");
- eleventyConfig.addWatchTarget("src/**/*.bib");
- eleventyConfig.addWatchTarget("src/**/*.avif");
- eleventyConfig.addWatchTarget("src/**/*.webp");
- eleventyConfig.addWatchTarget("src/**/*.jpeg");
- eleventyConfig.addWatchTarget("src/**/*.svg");
- eleventyConfig.addWatchTarget("src/**/*.ly");
- eleventyConfig.addWatchTarget("src/**/*.mid");
- eleventyConfig.addWatchTarget("src/**/*.opus");
- eleventyConfig.watchIgnores.add("src/tag/**/*");
- eleventyConfig.watchIgnores.add("src/post/**/*.ly.*");
- eleventyConfig.addPlugin(syntaxHighlight);
- eleventyConfig.on("eleventy.before", async ({ incremental, inputDir }) => {
- if (incremental) {
- return;
- }
- const jsInputDir = path.join(inputDir, "js/");
- const out = await exec(`npx tsc -p ${jsInputDir}`).catch(e =>
- console.error(e),
- );
- if (out?.stderr?.trim()) {
- console.error(out.stderr);
- }
- });
- eleventyConfig.on("eleventy.before", async ({ incremental, inputDir }) =>
- incremental ? undefined : await buildLy(inputDir),
- );
- const tagPaths = new Map();
- eleventyConfig.on("eleventy.before", async ({ inputDir }) => {
- const jsonText = await fs.readFile(path.join(inputDir, "tags.json"), {
- encoding: "utf8",
- });
- const tagData = JSON.parse(jsonText);
- const tagPath = path.join(inputDir, "tag/");
- await fs.rm(tagPath, { force: true, recursive: true });
- const writeTemplate = async (predecessors, prefix, vertex) => {
- const isRoot = vertex.tag === "post";
- const currPath = isRoot
- ? prefix
- : path.join(prefix, slugify(vertex.tag) + "/");
- tagPaths.set(vertex.tag, currPath.split("src", 2)[1]);
- const predecessorsIncl = isRoot
- ? []
- : predecessors.concat([vertex.tag]);
- const descendants = (
- await Promise.all(
- vertex.children.map(vert =>
- writeTemplate(predecessorsIncl, currPath, vert),
- ),
- )
- ).flat();
- descendants.unshift(vertex.tag);
- let list = `<ul><li><a ${
- isRoot ? 'aria-current="page"' : ""
- } href="/tag/" aria-label="root of the hierarchy">√</a><ul>`;
- for (const predecessor of predecessors) {
- list += `<li><a href="${tagPaths.get(
- predecessor,
- )}" rel="tag">${predecessor}</a><ul>`;
- }
- if (!isRoot) {
- list += `<li><a aria-current="page" href="${tagPaths.get(
- vertex.tag,
- )}" rel="tag">${vertex.tag}</a><ul>`;
- }
- for (const child of vertex.children) {
- list += `<li><a href="${tagPaths.get(child.tag)}" rel="tag">${
- child.tag
- }</a></li>`;
- }
- list += "</ul>".repeat(predecessors.length + 3);
- const hierarchyH1 = isRoot
- ? '{% h1 "hierarchy-of-tags" %}Hierarchy of tags{% endh1 %}'
- : `{% h1 "hierarchy-of-tags" %}Hierarchy of tags directly related to <b class="tag-name">${vertex.tag}</b>{% endh1 %}`;
- const indexTemplate =
- `---
- {
- "layout": "base.njk",
- "title": "tag: \\u201c${vertex.tag}\\u201d",
- "curr": "tags",
- "eleventyExcludeFromCollections": true,
- "noendnotes": true
- }
- ---
- {% set anchorRe = r/<a(\\s|>)/i %}
- {% set tagarr = [${descendants.map(d => '"' + d + '"').join(", ")}] %}
- <section aria-labelledby="hierarchy-of-tags" class="tag-hierarchy">
- ${hierarchyH1}
- ${list}
- </section>
- ` +
- (isRoot
- ? ""
- : `
- <section role="feed"
- aria-labelledby="posts-classified-under-this-tag"
- aria-busy="false"
- class="post-list"
- itemscope
- itemtype="https://schema.org/DataFeed"
- >
- {% h1 "posts-classified-under-this-tag" %}Posts classified under <b class="tag-name">${vertex.tag}</b>{% endh1 %}
- {%- for peaust in collections.all | posts | withtags(tagarr) | datesort -%}
- {%- set post_slug = peaust.data.title | slugify -%}
- {%- set post_id = post_slug ~ "--HEAD" -%}
- {%- set post_info_id = post_slug ~ "--INFO" -%}
- <article aria-labelledby="{{ post_id }}"
- aria-describedby="{{ post_info_id }}"
- aria-posinset="{{ loop.index }}"
- aria-setsize="{{ loop.length }}"
- class="post-preview"
- itemprop="dataFeedElement"
- itemscope
- itemtype="https://schema.org/BlogPosting"
- >
- <header>
- {#--#}
- <h2 id="{{ post_id }}">
- {#--#}
- <a href="{{ peaust.url }}" itemprop="url"><span itemprop="name headline">
- {%- if peaust.data.titleHeading -%}
- {%- if anchorRe.test(peaust.data.titleHeading) -%}
- {{- peaust.data.title -}}
- {%- else -%}
- {{- peaust.data.titleHeading | safe -}}
- {%- endif -%}
- {%- else -%}
- {{- peaust.data.title -}}
- {%- endif -%}
- </span></a>
- {#--#}
- </h2>
- <div id="{{ post_info_id }}" class="post-info">
- {%- set d8 = peaust.data.date -%}
- <time itemprop="datePublished" datetime="{{ d8 }}">{{ d8 }}</time>
- {#--#}
- <span class="tags-listing">
- {#--#}
- <span class="tags-colon">Tags: </span>
- {#--#}
- <span class="tags-list">
- {%- for tag in peaust.data.tags -%}
- <a href="{{ tag | tagpath }}" rel="tag" itemprop="keywords">{{ tag }}</a>
- {%- if not loop.last -%}
- ,
- {% endif -%}
- {%- endfor -%}
- </span>
- {#--#}
- </span>
- {#--#}
- </div>
- {%- if peaust.data.series -%}
- <div class="assoc-series">
- {#--#}
- <span class="part-of-a-series">Part of a series:</span>
- <cite itemprop="isPartOf" itemscope itemtype="https://schema.org/CreativeWork">
- {%- series peaust.data.series, false, true -%}
- </cite>
- {#--#}
- </div>
- {%- endif -%}
- </header>
- {#--#}
- <div itemprop="abstract">
- {{- peaust.content.split("<!-- __END_PREVIEW -->")[0] | safe -}}
- </div>
- {#--#}
- <footer>
- <a href="{{ peaust.url }}" itemprop="url">…Read the full post →</a>
- {#--#}
- </footer>
- </article>
- {%- endfor -%}
- </section>
- `);
- await fs.mkdir(currPath, { recursive: true });
- await fs.writeFile(
- path.join(currPath, "index.njk"),
- indexTemplate,
- {
- encoding: "utf8",
- mode: 0o664,
- },
- );
- return descendants;
- };
- await writeTemplate([], tagPath, tagData);
- });
- eleventyConfig.addFilter("tagpath", tag => tagPaths.get(tag));
- eleventyConfig.addFilter("tagterm", tag => {
- const p = tagPaths.get(tag);
- if (!p) {
- console.error("Cannot find tag: " + tag);
- throw "";
- }
- return p
- .split("/")
- .filter(s => s)
- .slice(1)
- .join("/");
- });
- eleventyConfig.addFilter("datesort", (posts, rev) =>
- posts.sort(
- (p1, p2) =>
- (rev ? -1 : 1) * (p1.data.date > p2.data.date ? -1 : 1),
- ),
- );
- for (const level of [1, 2, 3, 4, 5, 6]) {
- eleventyConfig.addPairedShortcode(`h${level}`, (content, id) =>
- makeHeading(content, id, level),
- );
- }
- const HEADING_RE =
- /\{%[\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;
- eleventyConfig.addShortcode("headingref", function (id, slug) {
- const rawInp = slug
- ? this.ctx.collections.all.find(p => p.page.fileSlug === slug).page
- .rawInput
- : this.page.rawInput;
- for (const [, matchedId, content] of rawInp.matchAll(HEADING_RE)) {
- if (id === matchedId) {
- return `“<a href="${
- slug ? "/post/" + slug + "/" : ""
- }#${id}">${content}</a>”`;
- }
- }
- console.error("Can't find heading with id", id, "and slug", slug);
- throw "";
- });
- eleventyConfig.addShortcode("snip", () => "<!-- __END_PREVIEW -->");
- eleventyConfig.addShortcode("post", function (slug) {
- const post = this.ctx.collections.all.find(
- p => p.page.fileSlug === slug,
- );
- if (!post) {
- console.error("No post with the slug ", slug);
- throw "";
- }
- const headline =
- post.data.titleHeading && !ANCHOR_RE.test(post.data.titleHeading)
- ? post.data.titleHeading
- : htmlEsc(post.data.title);
- return (
- `<cite itemscope itemtype="https://schema.org/BlogPosting">` +
- `<a itemprop="url" href="/post/${slug}">` +
- `<span itemprop="name headline">${headline}</span></a></cite>`
- );
- });
- eleventyConfig.addShortcode("tag", (tag, text) => {
- const p = tagPaths.get(tag);
- if (!p) {
- console.error("Unrecognized tag: " + tag);
- throw "";
- }
- return `<a href="${p}" rel="tag">${text ? text : htmlEsc(tag)}</a>`;
- });
- eleventyConfig.addShortcode(
- "series",
- (series, text, itemprop) =>
- `<a href="/series/#${slugify(series)}"${
- itemprop ? ' itemprop="url"' : ""
- }><span itemprop="name headline">${
- text ? text : htmlEsc(series)
- }</span></a>`,
- );
- eleventyConfig.addPairedShortcode("ipa", (transc, lang = "und", level) => {
- const [delimL, delimR] = (() => {
- if (!level) {
- return ["/", "/"];
- }
- switch (level) {
- case "none":
- return ["", ""];
- case "phone":
- return ["[", "]"];
- case "morph":
- return ["⫽", "⫽"];
- default: {
- console.error("Unrecognized transcription level:", level);
- throw "";
- }
- }
- })();
- const langSplit = lang.split("-x-", 2);
- const langAttr = `lang="${langSplit[0]}-fonipa${
- langSplit.length > 1 ? "-x-" + langSplit[1] : ""
- }"`;
- const transcSegs = transc
- .split(WS_RE)
- .map(s => {
- const trimmed = s.trim();
- if (!trimmed) {
- return "";
- }
- const $ = cheerio.load(
- `<span id="--DUMMY--ID--">${trimmed}</span>`,
- null,
- false,
- );
- replaceGs($("#--DUMMY--ID--")[0]);
- return $("#--DUMMY--ID--").html();
- })
- .filter(s => s);
- if (transcSegs.length < 1) {
- console.error("Empty transcription:", transc, lang, level);
- throw "";
- }
- return transcSegs.length === 1
- ? `<span ${langAttr} class="no-wrap">${delimL}${transcSegs[0]}${delimR}</span>`
- : `<span ${langAttr}><span class="no-wrap">${delimL}${transcSegs.join(
- '</span> <span class="no-wrap">',
- )}${delimR}</span></span>`;
- });
- const C_TIME_RE = /^c\/?$/;
- const STD_TIME_RE =
- /^(?<num>[1-9][0-9]*(\.[0-9]+)?)\/(?<denom>[1-9][0-9]*(\.[0-9]+)?)$/;
- const ADD_TIME_RE = /^(?<num>[1-9](\+[1-9])+)\/(?<denom>[1-9][0-9]*)$/;
- const FORM_RE = /^([A-Z]'*([1-9][0-9]*)?)+$/;
- const FORM_SECT_RE_G = /([A-Z])('*)([1-9][0-9]*)?/g;
- const PITCH_RE = /^([A-G])(b{1,2}|#|x|n)?(\-?[1-9][0-9]*)$/;
- const PC_RE = /^([A-G])(b{1,2}|#|x|n)?$/;
- const PC_RE_G = /([A-G])(b{1,2}|#|x|n)?/g;
- const PC_COLLECT_RE =
- /^([\{\[])([A-G](b{1,2}|#|x|n)?\s*,\s*)*[A-G](b{1,2}|#|x|n)?,?[\]\}]$/;
- const PC_PROG_RE =
- /^([A-G](b{1,2}|#|x|n)?\s*\-\-\s*)*[A-G](b{1,2}|#|x|n)?$/;
- const DEG_RE = /^(b{1,2}|#|x|n)?([1-9])\^$/;
- const INT_RE_G = /[0-9]{1,2}/g;
- const INT_COLLECT_RE = /^([\{\[])([0-9]{1,2}\s*,\s*)*[0-9]{1,2},?[\]\}]$/;
- const FORTE_RE = /^([1-9][0-9]?\-Z?[1-9][0-9]?)([AB])?$/;
- const NAMED_INTERVAL_RE = /^([PMmAdT])([1-9][0-9]*|T)$/;
- const CENTS_RE = /^([+\-]?[0-9]+(\.[0-9]+)?)c$/;
- const CPATH_RE = /^<([0-9]+(\s+[0-9]+)*)>$/;
- const CPATH_SEG_RE_G = /[0-9]+/g;
- const CHORD_FACT_RE = /^(b{1,2}|#|x|n)?(1?[0-9])$/;
- const CHORD_RE =
- /^(?<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]))?$/;
- const CHORD_PROG_INIT_RE =
- /^(?<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]))?\-\-.+$/;
- const RNA_RE =
- /^(?<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]))?$/;
- const RNA_PROG_INIT_RE =
- /^(?<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]))?\-\-.+$/;
- const ALT_RE_G = /(#|b)(5|9|11|13)/g;
- const DYN_RE =
- /^((?<mezzo>m[pf])|(?<piano>s?p+)|(?<forte>s?f+)|fp|(?<forz>s?fz)|(?<rein>rfz?))$/;
- const NOTES_RE =
- /^(\[)?([rn][1-9][0-9]{0,2}\.?>?\s+)*[rn][1-9][0-9]{0,2}\.?>?\]?$/;
- const NOTE_RE_G = /([rn])([1-9][0-9]{0,2})(\.)?(>)?/g;
- const MOTION_RE = /^([\[\{])([cosip][1-9]?)+[\]\}]$/;
- const MOTION_RE_G = /([cosip])([1-9])?/g;
- const ICV_RE =
- /^<([0-9te])([0-9te])([0-9te])([0-9te])([0-9te])([0-9te])>$/;
- const ARROW_RE =
- /^(?<deltas>((-[1-9][0-9]*|0|\+[1-9][0-9]*),)*(-[1-9][0-9]*|0|\+[1-9][0-9]*),?)?\|(?<motions>(?<curly>\{)?([cosip][1-9]?)+\}?)?$/;
- const DELTA_RE_G = /(-[1-9][0-9]*)|(0)|(\+[1-9][0-9]*)/g;
- const FORTE_LUT = new Map([
- ["3-1", [0, 1, 2]],
- ["3-2", [0, 1, 3]],
- ["3-2B", [0, 2, 3]],
- ["3-4", [0, 1, 5]],
- ["3-4B", [0, 4, 5]],
- ["3-5", [0, 1, 6]],
- ["3-5B", [0, 5, 6]],
- ["3-6", [0, 2, 4]],
- ["3-7", [0, 2, 5]],
- ["3-7B", [0, 3, 5]],
- ["3-8", [0, 2, 6]],
- ["3-8B", [0, 4, 6]],
- ["3-9", [0, 2, 7]],
- ["3-10", [0, 3, 6]],
- ["3-11", [0, 3, 7]],
- ["3-11B", [0, 4, 7]],
- ["3-12", [0, 4, 8]],
- ["4-10", [0, 2, 3, 5]],
- ["4-11", [0, 1, 3, 5]],
- ["4-11B", [0, 2, 4, 5]],
- ["4-16", [0, 1, 5, 7]],
- ["4-16B", [0, 2, 6, 7]],
- ["4-23", [0, 2, 5, 7]],
- ["4-Z29", [0, 1, 3, 7]],
- ["4-Z29B", [0, 4, 6, 7]],
- ]);
- const DYN_LUT = new Map([
- ["r", "𝆌"],
- ["s", "𝆍"],
- ["z", "𝆎"],
- ["p", "𝆏"],
- ["m", "𝆐"],
- ["f", "𝆑"],
- ]);
- function accidental(ascii) {
- if (!ascii) {
- return "";
- }
- switch (ascii) {
- case "b":
- return "♭";
- case "bb":
- return "𝄫";
- case "#":
- return "♯";
- case "x":
- return "𝄪";
- case "n":
- return "♮";
- default: {
- console.error("Unrecognized accidental: " + ascii);
- throw "";
- }
- }
- }
- function chordQual(ascii) {
- if (!ascii) {
- return ["", undefined];
- }
- switch (ascii) {
- case "+-":
- return ["±", "split‐third augmented"];
- case "-":
- return ["−", "minor"];
- case "+":
- return ["+", "augmented"];
- case "|-|":
- return ["⊟", "split‐third"];
- case "+maj":
- return ["+∆", "augmented major"];
- case "maj":
- return ["∆", "major"];
- case "sus2":
- return ["sus2", "suspended second"];
- case "sus4":
- return ["sus4", "suspended fourth"];
- case "o/":
- return ["𝆩", "half‐diminished"];
- case "o":
- return ["°", "diminished"];
- case "alt":
- return ["alt", "altered"];
- case "5":
- return ["5", "power"];
- default: {
- console.error("Unrecognized chord quality notation: " + ascii);
- throw "";
- }
- }
- }
- function rnaQual(ascii) {
- switch (ascii) {
- case "+maj":
- return ["+", "augmented major"];
- case "maj":
- return ["", "major"];
- case "5":
- return ["", "power"];
- default:
- return chordQual(ascii);
- }
- }
- function chordExt(ascii) {
- if (!ascii) {
- return ["", undefined];
- }
- switch (ascii) {
- case "add2":
- return ["add2", "added second"];
- case "add4":
- return ["add4", "added fourth"];
- case "6":
- return ["6", "sixth"];
- case "7":
- return ["7", "seventh"];
- case "9":
- return ["9", "ninth"];
- case "11":
- return ["11", "eleventh"];
- case "13":
- return ["13", "thirteenth"];
- default: {
- console.error(
- "Unrecognized chord extension notation: " + ascii,
- );
- throw "";
- }
- }
- }
- function chordAlts(ascii) {
- if (!ascii) {
- return ["", undefined];
- }
- const ret = ["", "with "];
- let first = true;
- for (const [, acci, factor] of ascii.matchAll(ALT_RE_G)) {
- const s = `${accidental(acci)}${factor}`;
- ret[0] += `${first ? "" : "⁣"}${s}`;
- ret[1] += `${first ? "" : ", "}${s}`;
- first = false;
- }
- return ret;
- }
- function chordOmitted(ascii) {
- if (!ascii) {
- return ["", undefined];
- }
- const factor = parseInt(ascii, 10);
- if (!factor || !Number.isSafeInteger(factor)) {
- console.error("Unexpected `omitted`: " + ascii);
- throw "";
- }
- switch (factor) {
- case 3:
- return ["no3", "with omitted third"];
- case 5:
- return ["no5", "with omitted fifth"];
- default: {
- console.error("`chordOmitted` fallthrough: " + ascii);
- throw "";
- }
- }
- }
- function chordInv(ascii, hasSeventh, isPower) {
- if (!ascii) {
- return ["", undefined];
- }
- const n = parseInt(ascii, 10);
- switch (n) {
- case 1: {
- if (isPower) {
- console.error("Power-chord in first inversion");
- throw "";
- }
- return [
- `<span class="inversion"><sup class="inversion-upper">6</sup>${
- hasSeventh
- ? '<sub class="inversion-lower">5</sub>'
- : '<span class="inversion-lower"></span>'
- }</span>`,
- "in first inversion",
- ];
- }
- case 2:
- return [
- isPower
- ? '<span class="inversion"><sup class="inversion-upper">4</sup><span class="inversion-lower"></span></span>'
- : `<span class="inversion"><sup class="inversion-upper">${
- hasSeventh ? 4 : 6
- }</sup><sub class="inversion-lower">${
- hasSeventh ? 3 : 4
- }</sub></span>`,
- "in second inversion",
- ];
- case 3: {
- if (!hasSeventh) {
- console.error(
- "Chord has no seventh, but is in third inversion",
- );
- throw "";
- }
- if (isPower) {
- console.error("Power-chord in third inversion");
- throw "";
- }
- return [
- '<span class="inversion"><sup class="inversion-upper">4</sup><sub class="inversion-lower">2</sub></span>',
- "in third inversion",
- ];
- }
- default: {
- console.error("Unrecognized inversion: " + ascii);
- throw "";
- }
- }
- }
- function rnaInv(ext, inv, alts, hasMaj7, isPower) {
- const intExt = parseInt(ext, 10);
- const hasExt = Number.isSafeInteger(intExt);
- const hasSeventh = hasExt && intExt >= 7;
- const intInv = parseInt(inv, 10);
- const hasInv = Number.isSafeInteger(intInv);
- const parsedAlts = [];
- for (const [, acci, factor] of alts.matchAll(ALT_RE_G)) {
- parsedAlts.push([accidental(acci), parseInt(factor, 10)]);
- }
- let inline = "";
- const upper = [];
- let lower = 0;
- if (hasExt) {
- switch (intExt) {
- case 6: {
- inline += "6";
- break;
- }
- case 7: {
- if (!hasInv) {
- upper.push([hasMaj7 ? "∆" : "", 7]);
- } else {
- inline += hasMaj7 ? "∆" : "";
- }
- break;
- }
- case 9: {
- if (!hasInv) {
- upper.push([hasMaj7 ? "∆" : "", 7]);
- upper.push(["", 9]);
- } else {
- inline += hasMaj7 ? "∆9" : "9";
- }
- break;
- }
- case 11: {
- if (!hasInv) {
- upper.push([hasMaj7 ? "∆" : "", 7]);
- upper.push(["", 11]);
- } else {
- inline += hasMaj7 ? "∆11" : "11";
- }
- break;
- }
- case 13: {
- if (!hasInv) {
- upper.push([hasMaj7 ? "∆" : "", 7]);
- upper.push(["", 13]);
- } else {
- inline += hasMaj7 ? "∆13" : "13";
- }
- break;
- }
- default: {
- console.error("Unrecognized RNA ext:", intExt);
- throw "";
- }
- }
- }
- if (isPower) {
- if (!hasInv) {
- upper.push(["", 5]);
- } else if (intInv === 2) {
- upper.push(["", 4]);
- } else {
- console.error("Inverted power-chord not in 2nd inversion");
- throw "";
- }
- } else if (hasInv) {
- switch (intInv) {
- case 1: {
- upper.push(["", 6]);
- if (hasSeventh) {
- lower = 5;
- }
- break;
- }
- case 2: {
- if (hasSeventh) {
- upper.push(["", 4]);
- lower = 3;
- } else {
- upper.push(["", 6]);
- lower = 4;
- }
- break;
- }
- case 3: {
- upper.push(["", 4]);
- lower = 2;
- break;
- }
- default: {
- console.error("Bad inversion:", intInv);
- throw "";
- }
- }
- }
- if (hasInv) {
- inline += parsedAlts.map(([s, n]) => "" + s + n).join("⁣");
- } else {
- upper.push(...parsedAlts);
- }
- upper.sort(([, n], [, m]) => n - m);
- const invHtml = `<span class="inversion"><${
- upper.length > 0 ? "sup" : "span"
- } class="inversion-upper">${upper
- .map(([s, n]) => "" + s + n)
- .join(",")}</${upper.length > 0 ? "sup" : "span"}><${
- lower ? "sub" : "span"
- } class="inversion-lower">${lower ? lower : ""}</${
- lower ? "sub" : "span"
- }></span>`;
- return `${inline}${upper.length > 0 || lower ? invHtml : ""}`;
- }
- function romanDesc(acci, rNum) {
- const [deg, fun] = (() => {
- switch (rNum.toUpperCase()) {
- case "I":
- return ["1st", "tonic"];
- case "II":
- return ["2nd", "supertonic"];
- case "III":
- return ["3rd", "mediant"];
- case "IV":
- return ["4th", "subdominant"];
- case "V":
- return ["5th", "dominant"];
- case "VI":
- return ["6th", "submediant"];
- case "VII":
- return ["7th", "leading‐tone or subtonic"];
- }
- })();
- return `${
- acci === "b" ? "flattened " : acci === "#" ? "sharpened " : ""
- }${deg} degree (${fun})`;
- }
- function parseChord(chordMatch) {
- const { letter, acci, qual, ext, alts, omitted, slash, inv } =
- chordMatch.groups;
- const root = `${letter}${accidental(acci)}`;
- const [qualText, qualDesc] = chordQual(qual);
- const [extText, extDesc] = chordExt(ext);
- const [altsText, altsDesc] = chordAlts(alts);
- const [omittedText, omittedDesc] = chordOmitted(omitted);
- const [, slashLetter, slashAcci] = slash
- ? PC_RE_G.exec(slash)
- : [undefined, undefined];
- const intExt = parseInt(ext, 10);
- const hasSeventh = Number.isSafeInteger(intExt) && intExt >= 7;
- const [invText, invDesc] = chordInv(inv, hasSeventh, qual === "5");
- return [
- `<span class="no-wrap">${root}${qualText}${extText}${altsText}${omittedText}${
- slashLetter
- ? '<span class="chord-slash">/</span><span class="chord-slash-bass">' +
- slashLetter +
- accidental(slashAcci) +
- "</span>"
- : ""
- }${invText}</span>`,
- `${root} ${
- qualDesc ? qualDesc : hasSeventh ? "dominant" : "major"
- }${extDesc ? " " + extDesc : ""} chord${
- altsDesc ? " " + altsDesc : ""
- }${omittedDesc ? " " + omittedDesc : ""}${
- slashLetter
- ? ", with " +
- slashLetter +
- accidental(slashAcci) +
- " in the bass"
- : invDesc
- ? ", " + invDesc
- : ""
- }`,
- ];
- }
- function parseRna(rnaMatch) {
- const {
- acci,
- roman,
- qual,
- ext,
- alts,
- omitted,
- inv,
- slashAcci,
- slash,
- } = rnaMatch.groups;
- const [qualText, qualDesc] = rnaQual(qual);
- const isPower = qual === "5";
- const invText = rnaInv(
- ext,
- inv,
- alts,
- qual && qual.indexOf("maj") !== -1,
- isPower,
- );
- const intExt = parseInt(ext, 10);
- const hasExt = Number.isSafeInteger(intExt);
- const hasSeventh = hasExt && intExt >= 7;
- const minor = roman === roman.toLowerCase();
- const defaultQual = (() => {
- if (minor) {
- return "minor";
- } else if (hasSeventh) {
- return "dominant";
- } else {
- return "major";
- }
- })();
- const [, extDesc] = chordExt(ext);
- const [, altsDesc] = chordAlts(alts);
- const [omittedText, omittedDesc] = chordOmitted(omitted);
- const [, invDesc] = chordInv(inv, hasSeventh, isPower);
- const [slashText, slashDesc] = (() => {
- if (!slash) {
- return ["", ""];
- }
- const accident = accidental(slashAcci);
- const slashDeg = Number.parseInt(slash, 10);
- if (Number.isSafeInteger(slashDeg)) {
- const deg = `${accident}${slashDeg}̂`;
- return [
- `<span class="rna-slash">/</span><span class="rna-slash-bass">${deg}</span>`,
- `, with the ${deg} degree in the bass`,
- ];
- }
- return [
- `<span class="rna-slash">/</span><span class="rna-slash-target">${accident}${slash}</span>`,
- `, as applied to the ${romanDesc(
- slashAcci,
- slash,
- )} of the key`,
- ];
- })();
- return [
- `<span class="no-wrap">${accidental(
- acci,
- )}${roman}${qualText}${omittedText}${invText}${slashText}</span>`,
- `Chord on the ${romanDesc(acci, roman)}: ${
- qualDesc ? qualDesc : defaultQual
- }${extDesc ? " " + extDesc : ""} chord${
- altsDesc ? " " + altsDesc : ""
- }${omittedDesc ? " " + omittedDesc : ""}${
- invDesc ? ", " + invDesc : ""
- }${slashDesc}`,
- ];
- }
- function namedInterval(text, namedIntervalMatch) {
- if (text.indexOf("T") !== -1) {
- if (text !== "TT") {
- return;
- }
- return ["TT", "An interval of a tritone"];
- }
- const [s, qual, size] = namedIntervalMatch;
- const qualName = (() => {
- switch (qual) {
- case "P":
- return "a perfect";
- case "M":
- return "a major";
- case "m":
- return "a minor";
- case "A":
- return "an augmented";
- case "d":
- return "a diminished";
- }
- })();
- const sizeName = (() => {
- const parsedSize = parseInt(size, 10);
- switch (parsedSize) {
- case 1:
- return "unison";
- case 2:
- return "second";
- case 3:
- return "third";
- case 4:
- return "fourth";
- case 5:
- return "fifth";
- case 6:
- return "sixth";
- case 7:
- return "seventh";
- case 8:
- return "octave";
- case 9:
- return "ninth";
- case 10:
- return "tenth";
- case 11:
- return "eleventh";
- case 12:
- return "twelfth";
- case 13:
- return "thirteenth";
- default: {
- console.error(
- "Unexpected named interval size:",
- parsedSize,
- );
- throw "";
- }
- }
- })();
- return [s, `An interval of ${qualName} ${sizeName}`];
- }
- function subOctaveIntToStr(n) {
- switch (n) {
- case 0:
- case 1:
- case 2:
- case 3:
- case 4:
- case 5:
- case 6:
- case 7:
- case 8:
- case 9:
- return "" + n;
- case 10:
- return "↊";
- case 11:
- return "↋";
- default:
- return;
- }
- }
- function prime(n) {
- if (n % 1 !== 0 || n < 0) {
- return;
- }
- switch (n) {
- case 0:
- return "";
- case 1:
- return "′";
- case 2:
- return "″";
- case 3:
- return "‴";
- default:
- return `⁗${prime(n - 4)}`;
- }
- }
- function ordinal(n) {
- const last = n % 10;
- const penultimate = Math.trunc(n / 10) % 10;
- if (penultimate !== 1) {
- if (last === 1) {
- return `${n}st`;
- }
- if (last === 2) {
- return `${n}nd`;
- }
- if (last === 3) {
- return `${n}rd`;
- }
- }
- return `${n}th`;
- }
- function motionName(text) {
- switch (text) {
- case "c":
- return "contrary";
- case "o":
- return "oblique";
- case "s":
- return "similar";
- case "i":
- return "imperfect parallel";
- case "p":
- return "perfect parallel";
- default: {
- console.error("Unrecognized motion type:", text);
- throw "";
- }
- }
- }
- function musNotate(text, ty) {
- C_TIME_RE.lastIndex = 0;
- STD_TIME_RE.lastIndex = 0;
- ADD_TIME_RE.lastIndex = 0;
- FORM_RE.lastIndex = 0;
- FORM_SECT_RE_G.lastIndex = 0;
- PITCH_RE.lastIndex = 0;
- PC_RE.lastIndex = 0;
- PC_RE_G.lastIndex = 0;
- PC_COLLECT_RE.lastIndex = 0;
- PC_PROG_RE.lastIndex = 0;
- DEG_RE.lastIndex = 0;
- INT_RE_G.lastIndex = 0;
- INT_COLLECT_RE.lastIndex = 0;
- FORTE_RE.lastIndex = 0;
- NAMED_INTERVAL_RE.lastIndex = 0;
- CENTS_RE.lastIndex = 0;
- CPATH_RE.lastIndex = 0;
- CPATH_SEG_RE_G.lastIndex = 0;
- CHORD_FACT_RE.lastIndex = 0;
- CHORD_RE.lastIndex = 0;
- CHORD_PROG_INIT_RE.lastIndex = 0;
- RNA_RE.lastIndex = 0;
- RNA_PROG_INIT_RE.lastIndex = 0;
- ALT_RE_G.lastIndex = 0;
- DYN_RE.lastIndex = 0;
- NOTES_RE.lastIndex = 0;
- NOTE_RE_G.lastIndex = 0;
- MOTION_RE.lastIndex = 0;
- MOTION_RE_G.lastIndex = 0;
- ICV_RE.lastIndex = 0;
- ARROW_RE.lastIndex = 0;
- DELTA_RE_G.lastIndex = 0;
- switch ("" + ty) {
- case "undefined":
- break;
- case "0":
- return [text, undefined];
- case "mot":
- return [text, `An instance of ${motionName(text)} motion`];
- case "->": {
- const arrowMatch = ARROW_RE.exec(text);
- if (!arrowMatch) {
- return;
- }
- const { deltas, motions, curly } = arrowMatch.groups;
- if (!deltas && !motions) {
- return;
- }
- const under = deltas && motions ? "under" : "";
- const deltasStr = deltas
- ? '<mrow><mspace depth="0" height="0" width="0.25em" />' +
- Array.from(deltas.matchAll(DELTA_RE_G))
- .map(m => {
- const s = m[0].trim();
- const parsed = parseInt(s, 10);
- if (!Number.isSafeInteger(parsed)) {
- console.error("Couldn't parse delta:", text);
- throw "";
- }
- return `<mn>${s.replaceAll(
- "-",
- "−",
- )}</mn>`;
- })
- .join("<mo>,</mo>") +
- '<mspace depth="0" height="0" width="0.25em" /></mrow>'
- : "";
- const motionsStr = motions
- ? '<mrow><mspace depth="0" height="0" width="0.25em" />' +
- (curly ? "<mo>⟅</mo><mrow>" : "") +
- Array.from(motions.matchAll(MOTION_RE_G))
- .map(([, ty, multi]) => {
- const multiplicity = multi
- ? parseInt(multi, 10)
- : 1;
- if (!Number.isSafeInteger(multiplicity)) {
- console.error(
- "Couldn't parse multiplicity:",
- text,
- );
- throw "";
- }
- return multiplicity === 1
- ? `<mi mathvariant="normal">${ty}</mi>`
- : `<msup><mi mathvariant="normal">${ty}</mi><mn>${multiplicity}</mn></msup>`;
- })
- .join("<mo>⁣</mo>") +
- (curly ? "</mrow><mo>⟆</mo>" : "") +
- '<mspace depth="0" height="0" width="0.25em" /></mrow>'
- : "";
- return [
- `<math><m${under}over><mo stretchy="true" largeop="true" form="infix">→</mo>${motionsStr}${deltasStr}</m${under}over></math>`,
- undefined,
- ];
- }
- case "icv": {
- const icvMatch = ICV_RE.exec(text);
- if (!icvMatch) {
- return;
- }
- const [, one, two, three, four, five, six] = icvMatch;
- const pitman = c => {
- switch (c) {
- case "t":
- return "↊";
- case "e":
- return "↋";
- default:
- return c;
- }
- };
- const joined = `⟨${pitman(one)}⁣${pitman(
- two,
- )}⁣${pitman(three)}⁣${pitman(four)}⁣${pitman(
- five,
- )}⁣${pitman(six)}⟩`;
- return [joined, `The interval‐class vector ${joined}`];
- }
- case "inter": {
- const namedIntervalMatch = NAMED_INTERVAL_RE.exec(text);
- if (!namedIntervalMatch) {
- return;
- }
- return namedInterval(text, namedIntervalMatch);
- }
- case "fact": {
- const chordFactMatch = CHORD_FACT_RE.exec(text);
- if (!chordFactMatch) {
- return;
- }
- const [, acci, nStr] = chordFactMatch;
- const n = parseInt(nStr, 10);
- const ord = ordinal(n);
- const alt = (() => {
- switch (acci) {
- case "b":
- return "flattened";
- case "bb":
- return "doubly‐flattened";
- case "#":
- return "sharpened";
- case "x":
- return "doubly‐sharpened";
- case "n":
- return "unaltered";
- }
- })();
- return [
- `${accidental(acci)}${n}`,
- `The ${alt ? alt + " " : ""}${ord} factor of a chord`,
- ];
- }
- case "form": {
- const formMatch = FORM_RE.exec(text);
- if (!formMatch) {
- return;
- }
- const matches = Array.from(text.matchAll(FORM_SECT_RE_G));
- return [
- "⟪" +
- matches
- .map(
- ([, letter, p, n]) =>
- `${letter}${prime(p.length)}${
- n ? "<sub>" + n + "</sub>" : ""
- }`,
- )
- .join("⁣") +
- "⟫",
- matches.length > 1
- ? `A musical form with ${matches.length} sections`
- : `The ${matches[0][1]}${prime(matches[0][2].length)}${
- matches[0][3] ? matches[0][3] : ""
- } section of a musical form`,
- ];
- }
- case "chord": {
- const chordMatch = CHORD_RE.exec(text);
- if (!chordMatch) {
- console.error("Couldn't parse chord: " + text);
- throw "";
- }
- return parseChord(chordMatch);
- }
- default:
- return;
- }
- const cTimeMatch = C_TIME_RE.exec(text);
- if (cTimeMatch) {
- switch (cTimeMatch[0]) {
- case "c":
- return [
- '<span class="c-time">𝄴</span>',
- "Common time (4⁄4)",
- ];
- case "c/":
- return [
- '<span class="c-time">𝄵</span>',
- "Alla breve (2⁄2; cut time)",
- ];
- default:
- return;
- }
- }
- const parenthesizeIfFractional = (s, noMarkup) =>
- s.includes(".")
- ? `${noMarkup ? "" : '<span class="time-sig-paren">'}(${
- noMarkup ? "" : "</span>"
- }${s}${noMarkup ? "" : '<span class="time-sig-paren">'})${
- noMarkup ? "" : "</span>"
- }`
- : s;
- const stdTimeMatch = STD_TIME_RE.exec(text);
- if (stdTimeMatch) {
- const { num, denom } = stdTimeMatch.groups;
- return [
- `<span class="time-sig"><span class="time-sig-num">${parenthesizeIfFractional(
- num,
- )}</span><span class="time-sig-slash">⁄</span><span class="time-sig-denom">${parenthesizeIfFractional(
- denom,
- )}</span></span>`,
- `${parenthesizeIfFractional(
- num,
- true,
- )}⁄${parenthesizeIfFractional(denom, true)} time`,
- ];
- }
- const addTimeMatch = ADD_TIME_RE.exec(text);
- if (addTimeMatch) {
- const { num, denom } = addTimeMatch.groups;
- return [
- `<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">⁄</span><span class="time-sig-denom">${denom}</span></span>`,
- `(${num})⁄${denom} time`,
- ];
- }
- const pitchMatch = PITCH_RE.exec(text);
- if (pitchMatch) {
- const [, name, acci, oct] = pitchMatch;
- const s = name + accidental(acci);
- const n = oct.replace("-", "−");
- return [
- `${s}<sub>${n}</sub>`,
- `The pitch, in scientific pitch notation: ${s}${n}`,
- ];
- }
- const pcMatch = PC_RE.exec(text);
- if (pcMatch) {
- const [, name, acci] = pcMatch;
- const s = name + accidental(acci);
- return [s, `The pitchclass ${s}`];
- }
- const pcCollectMatch = PC_COLLECT_RE.exec(text);
- if (pcCollectMatch) {
- const [lBrack, rBrack, collectTy] =
- pcCollectMatch[1] === "{"
- ? ["{", "}", "set"]
- : ["[", "]", "row"];
- const joined = Array.from(text.matchAll(PC_RE_G))
- .map(match => match[1] + accidental(match[2]))
- .join(", ");
- return [
- `${lBrack}${joined}${rBrack}`,
- `The ${collectTy} of pitchclasses ${lBrack}${joined}${rBrack}`,
- ];
- }
- const pcProgMatch = PC_PROG_RE.exec(text);
- if (pcProgMatch) {
- const joined = Array.from(text.matchAll(PC_RE_G))
- .map(match => match[1] + accidental(match[2]))
- .join("–");
- return [joined, `The progression of pitchclasses ${joined}`];
- }
- const degMatch = DEG_RE.exec(text);
- if (degMatch) {
- const [, acci, nStr] = degMatch;
- const n = parseInt(nStr, 10);
- return [
- `${accidental(acci)}${nStr}̂`,
- `The ${ordinal(n)} degree of a key or mode`,
- ];
- }
- const intCollectMatch = INT_COLLECT_RE.exec(text);
- if (intCollectMatch) {
- const [lBrack, rBrack, collectTy] =
- intCollectMatch[1] === "{"
- ? ["{", "}", "set"]
- : ["[", "]", "row"];
- const ints = text.match(INT_RE_G).map(m => +m);
- const intStrs = ints.map(subOctaveIntToStr);
- const joined = intStrs.every(s => s)
- ? intStrs.join("⁣")
- : ints.join(", ");
- return [
- `${lBrack}${joined}${rBrack}`,
- `The ${collectTy} of pitchclasses, in integer notation: ${lBrack}${joined}${rBrack}`,
- ];
- }
- const forteMatch = FORTE_RE.exec(text);
- if (forteMatch) {
- const [m, forte, inv] = forteMatch;
- const pcSetClass =
- inv === "A" || inv === undefined
- ? FORTE_LUT.get(forte)
- : FORTE_LUT.get(m);
- if (!pcSetClass) {
- console.error("Unknown pc-set class:", m);
- throw "";
- }
- const joined = pcSetClass.map(subOctaveIntToStr).join("⁣");
- const mDashed = m.replaceAll("-", "‐");
- const [lBrack, rBrack, lBrackSpan, rBrackSpan] = inv
- ? [
- "⁅",
- "⁆",
- '<span class="piggpar">⁅</span>',
- '<span class="piggpar">⁆</span>',
- ]
- : ["⟦", "⟧", "⟦", "⟧"];
- return [
- `<span class="no-wrap">${lBrackSpan}${joined}${rBrackSpan}<sub>${mDashed}</sub></span>`,
- `The pitchclass‐set equivalence‐class, in integer notation: ${lBrack}${joined}${rBrack} (Forte number ${mDashed})`,
- ];
- }
- const namedIntervalMatch = NAMED_INTERVAL_RE.exec(text);
- if (namedIntervalMatch) {
- return namedInterval(text, namedIntervalMatch);
- }
- const centsMatch = CENTS_RE.exec(text);
- if (centsMatch) {
- const [, n] = centsMatch;
- const nm = n.replaceAll("-", "−");
- return [nm + "⁢¢", `An interval of ${nm}⁢ cents`];
- }
- const cpathMatch = CPATH_RE.exec(text);
- if (cpathMatch) {
- const [, inner] = cpathMatch;
- const [joined, numPoints] = inner
- .match(CPATH_SEG_RE_G)
- .map(seg => {
- const segSplit = seg.split("");
- return [segSplit.join("⁣"), segSplit.length];
- })
- .reduce(([j, n], [s, p]) => [`${j}⁣ ${s}`, n + p]);
- const [lBrack, rBrack] =
- numPoints > 1 ? ["⟨", "⟩"] : ["", ""];
- return [
- `${lBrack}${joined}${rBrack}<sub style="font-weight: 700;">c</sub>`,
- numPoints > 1
- ? `A segment in contour‐space (c‐seg), consisting of ${numPoints} (not necessarily distinct) c‐pitches`
- : "A contour‐pitch (c‐pitch)",
- ];
- }
- const chordMatch = CHORD_RE.exec(text);
- if (chordMatch) {
- return parseChord(chordMatch);
- }
- if (CHORD_PROG_INIT_RE.exec(text)) {
- let ret = "";
- let first = true;
- for (const frag of text.split("--")) {
- CHORD_RE.lastIndex = 0;
- const chordM = CHORD_RE.exec(frag);
- if (!chordM) {
- console.error("Couldn't parse chord prog:", text);
- throw "";
- }
- const [s, title] = parseChord(chordM);
- if (!first) {
- ret += "–";
- }
- ret += `<span title="${title}">${s}</span>`;
- first = false;
- }
- return [ret, undefined];
- }
- const rnaMatch = RNA_RE.exec(text);
- if (rnaMatch) {
- return parseRna(rnaMatch);
- }
- if (RNA_PROG_INIT_RE.exec(text)) {
- let ret = "";
- let first = true;
- for (const frag of text.split("--")) {
- RNA_RE.lastIndex = 0;
- const rnaM = RNA_RE.exec(frag);
- if (!rnaM) {
- console.error("Couldn't parse RNA prog:", text);
- throw "";
- }
- const [s, title] = parseRna(rnaM);
- if (!first) {
- ret += "–";
- }
- ret += `<span title="${title}">${s}</span>`;
- first = false;
- }
- return [ret, undefined];
- }
- const dynMatch = DYN_RE.exec(text);
- if (dynMatch) {
- const s = text
- .split("")
- .map(c => DYN_LUT.get(c))
- .join("");
- const { mezzo, piano, forte, forz, rein } = dynMatch.groups;
- if (mezzo) {
- return [
- s,
- `mezzo‐${text.endsWith("p") ? "piano" : "forte"}`,
- ];
- }
- const suffix = (n, v) => (n < 2 ? v : "iss".repeat(n - 1) + "imo");
- if (piano) {
- return text.startsWith("s")
- ? [s, `subito pian${suffix(text.length - 1, "o")}`]
- : [s, `pian${suffix(text.length, "o")}`];
- }
- if (forte) {
- return text.startsWith("s")
- ? [s, `subito fort${suffix(text.length - 1, "e")}`]
- : [s, `fort${suffix(text.length, "e")}`];
- }
- if (text === "fp") {
- return [s, "fortepiano"];
- }
- if (forz) {
- return [s, text.startsWith("s") ? "sforzando" : "forzando"];
- }
- if (rein) {
- return [s, "rinforzando"];
- }
- }
- const notesMatch = NOTES_RE.exec(text);
- if (notesMatch) {
- const [, brack] = notesMatch;
- const joined = Array.from(text.matchAll(NOTE_RE_G))
- .map(([, rn, denomStr, dot, accent]) => {
- const rest = rn === "r";
- const denom = parseInt(denomStr, 10);
- const base = (() => {
- switch (denom) {
- case 1:
- return rest ? "𝄻" : "𝅝";
- case 2:
- return rest ? "𝄼" : "𝅗𝅥";
- case 4:
- return rest ? "𝄽" : "𝅘𝅥";
- case 8:
- return rest ? "𝄾" : "𝅘𝅥𝅮";
- case 16:
- return rest ? "𝄿" : "𝅘𝅥𝅯";
- case 32:
- return rest ? "𝅀" : "𝅘𝅥𝅰";
- case 64:
- return rest ? "𝅁" : "𝅘𝅥𝅱";
- case 128:
- return rest ? "𝅂" : "𝅘𝅥𝅲";
- default: {
- console.error(
- "Bad note duration denominator:",
- denom,
- );
- throw "";
- }
- }
- })();
- return `${base}${dot ? "𝅭" : ""}${
- accent ? "𝅻" : ""
- }`;
- })
- .join("");
- return [
- `${
- brack ? "⟨⁠" : ""
- }<span class="mus-notes">${joined}</span>${
- brack ? "⁠⟩" : ""
- }`,
- undefined,
- ];
- }
- const motionMatch = MOTION_RE.exec(text);
- if (motionMatch) {
- const [, bracket] = motionMatch;
- const [lBrack, rBrack, sq] =
- bracket === "["
- ? ["[", "]", true]
- : ["⟅", "⟆", false];
- const a = Array.from(text.matchAll(MOTION_RE_G));
- const joined = a
- .map(([, t, m]) => `${t}${m ? "<sup>" + m + "</sup>" : ""}`)
- .join("⁣");
- const full = a
- .map(([, t, m]) =>
- Array(m ? parseInt(m, 10) : 1).fill(motionName(t)),
- )
- .flat();
- const fullJoined = full.join(", ");
- return [
- `${lBrack}${joined}${rBrack}`,
- `An instance of polyphonic motion: ${fullJoined}${
- full.length > 1
- ? sq
- ? ""
- : " (in no particular order)"
- : " motion"
- }`,
- ];
- }
- }
- eleventyConfig.addShortcode("m", (text, ty) => {
- const n = musNotate(text, ty);
- if (!n) {
- console.error(`Couldn't parse {% m "${text}" %}`);
- throw "";
- }
- return `<span class="music"${n[1] ? ' title="' + n[1] + '"' : ""}>${
- n[0]
- }</span>`;
- });
- function rankToWikidata(rank) {
- switch (rank) {
- case "genus":
- return "34740";
- case "suborder":
- return "5867959";
- case "superfamily":
- return "2136103";
- default: {
- console.error("Unrecognized rank:", rank);
- throw "";
- }
- }
- }
- eleventyConfig.addShortcode("species", (genus, species, url, abbr) => {
- const genusText =
- '<span itemprop="parentTaxon" itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q34740" /><' +
- (abbr
- ? `meta itemprop="name" content="${genus}" /><abbr title="${genus}">${genus[0]}.</abbr`
- : `span itemprop="name">${genus}</span`) +
- "></span>";
- return `<span itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q7432" />${
- url ? '<a itemprop="url" href="' + url + '">' : ""
- }<i itemprop="name">${genusText} ${species}</i>${
- url ? "</a>" : ""
- }</span>`;
- });
- eleventyConfig.addShortcode("taxon", (rank, name, parent, url) => {
- const nameTag = rank === "genus" ? "i" : "span";
- return `<span itemscope itemtype="https://schema.org/Taxon"><meta itemprop="taxonRank" content="http://www.wikidata.org/entity/Q${rankToWikidata(
- rank,
- )}" />${
- parent
- ? '<meta itemprop="parentTaxon" content="' + parent + '" />'
- : ""
- }${
- url ? '<a itemprop="url" href="' + url + '">' : ""
- }<${nameTag} itemprop="name">${name}</${nameTag}>${
- url ? "</a>" : ""
- }</span>`;
- });
- eleventyConfig.addShortcode("personage", personage);
- eleventyConfig.addShortcode("band", (name, href) =>
- personage(name, undefined, href, "MusicGroup"),
- );
- eleventyConfig.addShortcode(
- "album",
- (name, artist, date, noArtist, noDate) => {
- const artistStr = artist
- ? noArtist
- ? `<meta itemprop="byArtist creator author" content="${artist}" />`
- : `<span itemprop="byArtist creator author">${personage(
- artist,
- undefined,
- undefined,
- "MusicGroup",
- )}</span>’s `
- : "";
- const dateStr = date
- ? noDate
- ? `<meta itemprop="datePublished" content="${date}" />`
- : ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
- : "";
- return `<span class="work-mention" itemscope itemtype="https://schema.org/MusicAlbum">${artistStr}<cite itemprop="name">${name}</cite>${dateStr}</span>`;
- },
- );
- eleventyConfig.addShortcode(
- "track",
- (name, album, artist, date, showDate) => {
- const albumStr = album
- ? `<meta itemprop="inAlbum" content="${album}" />`
- : "";
- const artistStr = artist
- ? `<meta itemprop="byArtist" content="${artist}" />`
- : "";
- const dateStr = date
- ? showDate
- ? ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
- : `<meta itemprop="datePublished" content="${date}" />`
- : "";
- return (
- '<span class="track work-mention" itemscope itemtype="https://schema.org/MusicRecording">' +
- `“<span itemprop="name">${name}</span>”${albumStr}${artistStr}${dateStr}</span>`
- );
- },
- );
- eleventyConfig.addShortcode(
- "litmention",
- (name, ty, author, date, noAuthor, noDate, titleLang, url) => {
- const authorStr = author
- ? noAuthor
- ? `<meta itemprop="author creator" content="${author}" />`
- : `<span itemprop="author creator">${personage(
- author,
- undefined,
- undefined,
- "Person",
- )}</span>’s `
- : "";
- const dateStr = date
- ? noDate
- ? `<meta itemprop="datePublished" content="${date}" />`
- : ` (<time itemprop="datePublished" datetime="${date}">${date}</time>)`
- : "";
- const [aOpen, aClose] = url
- ? ['<a itemprop="url" href="' + url + '">', "</a>"]
- : ["", ""];
- const cite =
- ty === "Poem"
- ? `“${aOpen}<cite${
- titleLang ? ' lang="' + titleLang + '"' : ""
- } class="quote-cite" itemprop="name">${name}</cite>${aClose}”`
- : `${aOpen}<cite${
- titleLang ? ' lang="' + titleLang + '"' : ""
- } itemprop="name">${name}</cite>${aClose}`;
- return `<span class="work-mention" itemscope itemtype="https://schema.org/${
- ty && ty !== "Poem" ? ty : "CreativeWork"
- }">${authorStr}${cite}${dateStr}</span>`;
- },
- );
- eleventyConfig.addPairedShortcode(
- "note",
- content =>
- '<div role="note">' +
- '<img class="note-i has-transparency" alt="ℹ️" decoding="async" src="/img/i.svg">' +
- content +
- "</div>",
- );
- eleventyConfig.addPairedShortcode(
- "eg",
- content =>
- '<div class="eg" role="note">' +
- '<img class="eg-eg has-transparency" alt="For example: " decoding="async" src="/img/eg.svg">' +
- content +
- "</div>",
- );
- eleventyConfig.addPairedShortcode(
- "aside",
- content =>
- '<aside class="excursion">' +
- '<img class="thought-bubble has-transparency" alt="💭" decoding="async" src="/img/thought.svg">' +
- content +
- "</aside>",
- );
- eleventyConfig.addPairedShortcode("bq", async function (content, cite) {
- const [href, sauce, clazz] =
- cite.startsWith("http") || cite.startsWith("//")
- ? [cite, "source", ""]
- : [
- `#BIB--${cite}`,
- await getCiteIdByKey(this.page.inputPath, cite),
- " cite",
- ];
- return (
- `<blockquote cite="${href}" class="has-inner-cite">` +
- content +
- `<footer><small><cite class="blockquote-src${clazz}">` +
- `<a href="${href}"><span class="src-brace">[</span>${sauce}<span class="src-brace">]</span></a>` +
- "</cite></small></footer>" +
- "</blockquote>"
- );
- });
- eleventyConfig.addShortcode("img", async function (src, alt) {
- return makeImg(this, src, alt);
- });
- eleventyConfig.addPairedShortcode(
- "imgdet",
- async function (content, src, alt) {
- return imgDet(this, content, src, alt);
- },
- );
- eleventyConfig.addShortcode("ly", async function (srcBase, alt) {
- return makeLy(this, srcBase, alt);
- });
- eleventyConfig.addShortcode("refs", async function () {
- const lib = await parseLibrary(this.page.inputPath);
- lib.entries.sort((e0, e1) => {
- const lastName0 = e0.fields.author[0].lastName;
- const lastName1 = e1.fields.author[0].lastName;
- if (lastName0 > lastName1) {
- return 1;
- } else if (lastName0 < lastName1) {
- return -1;
- }
- const firstName0 = e0.fields.author[0].firstName;
- const firstName1 = e1.fields.author[1].firstName;
- if (firstName0 > firstName1) {
- return 1;
- } else if (firstName0 < firstName1) {
- return -1;
- }
- const year0 = e0.fields.year;
- const year1 = e1.fields.year;
- if (year0 > year1) {
- return 1;
- } else if (year0 < year1) {
- return -1;
- }
- return 0;
- });
- let refs = `<section role="doc-endnotes">${makeHeading(
- "References",
- "refs",
- 2,
- )}<table class="refs-table"><tbody>`;
- for (const entry of lib.entries) {
- refs +=
- `<tr id="BIB--${entry.key}">` +
- `<th scope="row"><a href="#BIB--${entry.key}">` +
- `<span class="src-brace">[</span>${getCiteId(
- entry.fields,
- )}<span class="src-brace">]</span>` +
- '</a></th><td itemprop="citation" ' +
- `itemscope itemtype="${entryTypeToItemType(entry.type)}">`;
- const addPersons = (arr, ed) => {
- for (const [i, person] of arr.entries()) {
- const nameArr = [];
- if (person.prefix) {
- nameArr.push(
- `<span itemprop="honorificPrefix">${person.prefix}</span>`,
- );
- }
- if (person.name) {
- nameArr.push(
- `<span itemprop="name">${person.name}</span>`,
- );
- } else {
- if (person.firstName) {
- let first = true;
- for (const s of person.firstName.split(WS_RE)) {
- nameArr.push(
- `<span itemprop="${
- first ? "givenName" : "additionalName"
- }">${s}</span>`,
- );
- first = false;
- }
- }
- if (person.lastName) {
- nameArr.push(
- `<span itemprop="familyName">${person.lastName}</span>`,
- );
- }
- }
- if (person.suffix) {
- nameArr.push(
- `<span itemprop="honorificSuffix">${person.suffix}</span>`,
- );
- }
- const last = i === arr.length - 1;
- const name = nameArr.join(" ");
- const trailing = (() => {
- if (last) {
- if (ed) {
- if (arr.length > 1) {
- return ' (<abbr title="editors">eds.</abbr>). ';
- }
- return ' (<abbr title="editor">ed.</abbr>). ';
- }
- return ". ";
- }
- if (arr.length === 2) {
- return " ";
- }
- return ", ";
- })();
- refs +=
- `${
- last && arr.length > 1 ? "& " : ""
- }<b itemprop="${
- ed ? "editor" : "author"
- }" itemscope itemtype="https://schema.org/Person" ` +
- `class="ref-author">${name}</b>${trailing}`;
- }
- };
- if (entry.fields.author) {
- addPersons(entry.fields.author);
- }
- if (entry.fields.booktitle || entry.fields.journal) {
- refs += `\u{201c}<span itemprop="name headline">${entry.fields.title
- .replaceAll("\u{201c}", "\u{2018}")
- .replaceAll(
- "\u{201d}",
- "\u{2019}",
- )}</span>\u{201d}, in <span itemprop="isPartOf" itemscope itemtype="https://schema.org/${
- entry.fields.volume || entry.fields.number
- ? "PublicationIssue"
- : "Periodical"
- }"><cite itemprop="name headline">${
- entry.fields.booktitle
- ? entry.fields.booktitle
- : entry.fields.journal
- }</cite>`;
- if (entry.fields.volume) {
- refs += ` vol. <span itemprop="issueNumber">${entry.fields.volume}`;
- if (entry.fields.number) {
- refs += ` (${entry.fields.number})</span>`;
- } else {
- refs += "</span>";
- }
- } else if (entry.fields.number) {
- refs += ` (<span itemprop="issueNumber">${entry.fields.number}</span>)`;
- }
- refs += "</span>";
- } else {
- refs += `<cite itemprop="name headline">${entry.fields.title}</cite>`;
- }
- if (entry.fields.series) {
- refs += ` (<span itemprop="isPartOf">${entry.fields.series}</span>)`;
- }
- if (entry.fields.edition) {
- refs += `, <span itemprop="version">${entry.fields.edition} edition</span>`;
- }
- if (entry.fields.chapter) {
- refs += `, ch. ${entry.fields.chapter}`;
- }
- if (entry.fields.pages) {
- refs += `, ${
- entry.fields.pages.includes("\u{2013}") ? "p" : ""
- }p. <span itemprop="pagination">${entry.fields.pages}</span>`;
- }
- refs += ". ";
- if (entry.fields.editor) {
- addPersons(entry.fields.editor, true);
- }
- const datetime = `${entry.fields.year}${
- entry.fields.month
- ? "-" + entry.fields.month.padStart(2, "0")
- : ""
- }`;
- const date = `${
- entry.fields.month ? MONTHS[+entry.fields.month - 1] + " " : ""
- }${entry.fields.year}`;
- refs +=
- '<time itemprop="datePublished" class="ref-time" ' +
- `datetime="${datetime}">${date}</time>. `;
- if (entry.fields.publisher) {
- for (const [
- i,
- publisher,
- ] of entry.fields.publisher.entries()) {
- refs += `<span itemprop="publisher">${publisher}</span>`;
- if (i !== entry.fields.publisher.length - 1) {
- refs += ", ";
- }
- }
- if (entry.fields.address) {
- refs += `; ${entry.fields.address}`;
- }
- refs += ".";
- } else if (entry.fields.address) {
- refs += entry.fields.address + ".";
- }
- if (entry.fields.howpublished) {
- refs += ` Published at: ${entry.fields.howpublished
- .replaceAll("<a", '<code><a itemprop="url"')
- .replaceAll("</a>", "</a></code>")}.`;
- }
- if (entry.fields.doi) {
- refs += ` <code><a itemprop="sameAs" href="https://doi.org/${entry.fields.doi}">doi:${entry.fields.doi}</a></code>`;
- }
- refs = refs.trimEnd();
- refs += "</td></tr>";
- }
- refs = refs.replaceAll("'", "\u{2019}");
- refs += "</tbody></table></section>";
- return refs;
- });
- eleventyConfig.addShortcode("cite", async function (key, inline) {
- const citeId = await getCiteIdByKey(this.page.inputPath, key);
- const tagName = inline ? "span" : "sup";
- return (
- `<${tagName} class="cite"><a href="#BIB--${key}">` +
- `<span class="src-brace">[</span>${citeId}<span class="src-brace">]</span>` +
- `</a></${tagName}>`
- );
- });
- eleventyConfig.addPairedShortcode("toc", content => {
- let toc = '<nav class="toc" aria-label="table of contents">';
- const $ = cheerio.load(content, null, false);
- const headings = $("h1, h2, h3, h4, h5, h6");
- const levelStack = [0];
- for (const heading of headings) {
- $(heading)
- .find("a")
- .each((_, a) => {
- a.tagName = "SPAN";
- $(a)
- .removeAttr("href")
- .removeAttr("referrerpolicy")
- .removeAttr("rel")
- .removeAttr("target")
- .removeAttr("download")
- .removeAttr("itemprop");
- });
- const level = parseInt(heading.tagName[1], 10);
- while (level < levelStack[levelStack.length - 1]) {
- levelStack.pop();
- toc += "</li></ol>";
- }
- if (level > levelStack[levelStack.length - 1]) {
- levelStack.push(level);
- toc += "<ol><li>";
- } else {
- toc += "</li><li>";
- }
- toc += `<a href="#${$(heading).attr("id")}">`;
- toc += $(heading).html();
- toc += "</a>";
- }
- toc += "</li></ol>".repeat(levelStack.length - 1);
- return toc + "</nav>" + content;
- });
- eleventyConfig.addFilter("serieses", function () {
- const serieses = [];
- for (const post of this.ctx.collections.all.filter(
- p => p.data.series,
- )) {
- const series = serieses.find(s => s.name === post.data.series);
- if (series) {
- series.posts.push(post);
- } else {
- serieses.push({ name: post.data.series, posts: [post] });
- }
- }
- return serieses;
- });
- eleventyConfig.addFilter("commontags", posts => {
- const tagCounts = new Map();
- posts.forEach(post =>
- post.data.tags.forEach(tag =>
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1),
- ),
- );
- return Array.from(tagCounts.entries())
- .sort(([, ct0], [, ct1]) => ct1 - ct0)
- .reduce((accu, [tag, ct]) => {
- if (ct > 1) {
- accu.push(tag);
- }
- return accu;
- }, []);
- });
- eleventyConfig.addShortcode("articleHeader", function () {
- const date = this.ctx.date;
- const tagsList = this.ctx.tags
- .map(
- tag =>
- `<a href="${tagPaths.get(
- tag,
- )}" rel="tag" itemprop="keywords">${htmlEsc(tag)}</a>`,
- )
- .join(", ");
- const series = this.ctx.series;
- return `<header class="article-header"><span id="start"></span>\
- <div class="post-info">\
- <time itemprop="datePublished" datetime="${date}">${date}</time>\
- <span class="tags-listing"><span class="tags-colon">Tags: </span>\
- <span class="tags-list">${tagsList}</span></span>\
- </div></header>${
- series
- ? '<div class="assoc-series"><span class="part-of-a-series">' +
- 'Part of a series:</span> <cite itemprop="isPartOf" ' +
- 'itemscope itemtype="https://schema.org/Collection">' +
- '<a href="/series/#' +
- slugify(series) +
- '" itemprop="url"><span itemprop="name headline">' +
- htmlEsc(series) +
- "</span></a></cite></div>"
- : ""
- }`;
- });
- const TEXT = 0;
- const ANNOT = 1;
- const ANNOT_EXIT = 2;
- eleventyConfig.addShortcode(
- "ruby",
- (text, lang, annotLang, annotLang1) => {
- let state = TEXT;
- const components = [[""]];
- for (const c of text) {
- switch (c) {
- case "{": {
- switch (state) {
- case TEXT:
- case ANNOT_EXIT: {
- state = ANNOT;
- components[components.length - 1].push("");
- break;
- }
- case ANNOT: {
- console.error(
- "Stray `{` inside of ruby annotation: " +
- text,
- );
- throw "";
- }
- }
- break;
- }
- case "}": {
- switch (state) {
- case TEXT:
- case ANNOT_EXIT: {
- console.error(
- "Stray `}` with no preceding `{`: " + text,
- );
- throw "";
- }
- case ANNOT: {
- state = ANNOT_EXIT;
- break;
- }
- }
- break;
- }
- default: {
- switch (state) {
- case TEXT:
- case ANNOT: {
- const comp = components[components.length - 1];
- comp[comp.length - 1] += c;
- break;
- }
- case ANNOT_EXIT: {
- state = TEXT;
- components.push([c]);
- break;
- }
- }
- break;
- }
- }
- }
- let ret = lang ? `<ruby lang="${lang}">` : "<ruby>";
- const annotLangAttr = annotLang ? ` lang="${annotLang}"` : "";
- const annotLang1Attr = annotLang1 ? ` lang="${annotLang1}"` : "";
- for (const comp of components) {
- if (comp.length > 2) {
- ret += "<ruby>".repeat(comp.length - 2);
- }
- for (const [i, subcomp] of comp.entries()) {
- switch (i) {
- case 0: {
- ret += subcomp;
- break;
- }
- case 1: {
- ret += `<rp>(</rp><rt${annotLangAttr}>${subcomp}</rt><rp>)</rp>`;
- break;
- }
- default: {
- ret += `</ruby><rp>(</rp><rt${annotLang1Attr}>${subcomp}</rt><rp>)</rp>`;
- break;
- }
- }
- }
- }
- ret += "</ruby>";
- return ret;
- },
- );
- function figTypeToText(type, abbr) {
- switch (type) {
- case "fig":
- return abbr
- ? '<abbr class="fig-type" title="Figure">Fig.</abbr>'
- : "Figure";
- case "table":
- return "Table";
- case "list":
- return "List";
- default: {
- console.error(`Unknown figure type: "${type}"`);
- throw "";
- }
- }
- }
- function figTypeInitial(type) {
- switch (type) {
- case "list":
- return '<abbr class="fig-type" title="List">L</abbr>';
- default: {
- console.error(`Cannot make initial of figure type "${type}"`);
- throw "";
- }
- }
- }
- eleventyConfig.addShortcode("figref", (id, type = "fig", li, noInit) =>
- li
- ? `<a href="#${id}-${li}">(${
- noInit ? "" : figTypeInitial(type) + "{{{#" + id + "--N}}}"
- }${li}.)</a>`
- : `<a href="#${id}">${figTypeToText(
- type,
- true,
- )} {{{#${id}--N}}}</a>`,
- );
- eleventyConfig.addPairedShortcode("fig", (content, id, type = "fig") => {
- const $ = cheerio.load(
- `<figure id="${id}" data-fig-type="${type}">${content}</figure>`,
- null,
- false,
- );
- const firstP = $(`#${id} > figcaption > p`)[0];
- $(firstP).prepend(" ");
- $(firstP).prepend(
- `<b><a href="#${id}" class="figcaption-label">${figTypeToText(
- type,
- )} {{{#${id}--N}}}</a>:</b>`,
- );
- return $.html();
- });
- eleventyConfig.addPairedShortcode("figdomain", content => {
- const $ = cheerio.load(content, null, false);
- const figs = $("figure[id][data-fig-type]");
- const ordering = new Map();
- for (const fig of figs) {
- const id = $(fig).attr("id");
- const figType = $(fig).attr("data-fig-type");
- if (ordering.has(figType)) {
- ordering.get(figType).push(id);
- } else {
- ordering.set(figType, [id]);
- }
- }
- ordering.forEach(ids =>
- ids.forEach(
- (id, i) =>
- (content = content.replaceAll(
- `{{{#${id}--N}}}`,
- `${1 + i}`,
- )),
- ),
- );
- return content;
- });
- eleventyConfig.addPairedShortcode("glosslist", content => {
- const $ = cheerio.load(
- `<dl class="gloss-list">${content}</dl>`,
- null,
- false,
- );
- for (const dt of $("dl:first > dt")) {
- const text = $(dt).text().trim();
- const innerHtml = $(dt).html();
- const id = "gloss-" + slugify(text);
- $(dt).attr("id", id);
- $(dt).attr("itemprop", "hasDefinedTerm");
- $(dt).html(`<a href="#${id}">${innerHtml}</a>`);
- }
- return $.html();
- });
- eleventyConfig.addShortcode(
- "gloss",
- (term, text) =>
- `<a class="gloss-ref" href="/glossary/#gloss-${slugify(term)}">${
- text ? text : term
- }</a>`,
- );
- const CODEPOINT_CAPITAL_A = 0x0041;
- const CODEPOINT_SMALL_A = 0x0061;
- const ROMAN = [
- ["M", 1_000],
- ["CM", 900],
- ["D", 500],
- ["CD", 400],
- ["C", 100],
- ["XC", 90],
- ["L", 50],
- ["XL", 40],
- ["X", 10],
- ["IX", 9],
- ["V", 5],
- ["IV", 4],
- ["I", 1],
- ];
- function liMarker(n, type = "1") {
- if (type !== "1" && n < 1) {
- console.error(`n < 1 for marker type "${type}"`);
- throw "";
- }
- const letterMarker = baseCodepoint => {
- const n0 = n - 1;
- if (n0 < 26) {
- return String.fromCodePoint(baseCodepoint + n0);
- }
- const residue = n0 % 26;
- return (
- liMarker((n0 - residue) / 26, type) +
- String.fromCodePoint(baseCodepoint + residue)
- );
- };
- const romanMarker = lower =>
- ROMAN.reduce(
- ([m, s], [r, rVal]) => {
- const q = Math.trunc(m / rVal);
- return [
- m - q * rVal,
- s + (lower ? r.toLowerCase() : r).repeat(q),
- ];
- },
- [n, ""],
- )[1];
- switch (type) {
- case "1":
- return "" + n;
- case "A":
- return letterMarker(CODEPOINT_CAPITAL_A);
- case "a":
- return letterMarker(CODEPOINT_SMALL_A);
- case "I":
- return romanMarker();
- case "i":
- return romanMarker(true);
- default: {
- console.error(`Unrecognized <li> marker type: "${type}"`);
- throw "";
- }
- }
- }
- eleventyConfig.addPairedShortcode("ol", (content, baseId, type = "1") => {
- const $ = cheerio.load(
- `<ol type="${type}">${content}</ol>`,
- null,
- false,
- );
- $("ol:first > li").each((i, elem) =>
- $(elem).attr("id", `${baseId}-${liMarker(i + 1, type)}`),
- );
- return $.html();
- });
- eleventyConfig.addPairedShortcode(
- "enmark",
- (content, id) =>
- `<span class="enmark-span">${content}<sup class="enmark"><a id="${id}--MARK" href="#${id}">[?]</a></sup></span>`,
- );
- eleventyConfig.addPairedShortcode(
- "en",
- (content, id) =>
- `<li class="en" id="${id}"><a class="en-arrow" href="#${id}--MARK" aria-label="back to the main text">↩︎</a> ${content}</li>`,
- );
- eleventyConfig.addPairedShortcode("endomain", (content, lvl) => {
- const $ = cheerio.load(content, null, false);
- const firstHeading = $("h1, h2, h3, h4, h5, h6")[0];
- const level = lvl
- ? lvl
- : Math.min(parseInt(firstHeading.tagName[1], 10) + 1, 6);
- const ens = $("li.en");
- const enIds = [];
- for (const en of ens) {
- enIds.push($(en).attr("id"));
- }
- const enMarks = $("sup.enmark > a");
- const enMarkIds = new Set();
- for (const enMark of enMarks) {
- const id = $(enMark).attr("id");
- if (enMarkIds.has(id)) {
- $(enMark).removeAttr("id");
- } else {
- enMarkIds.add(id);
- }
- const enId = $(enMark).attr("href").slice(1);
- const ix = enIds.indexOf(enId);
- $(enMark).text(`[${1 + ix}]`);
- }
- const firstHeadingId = $(firstHeading).attr("id");
- const olId = `${firstHeadingId}--ENS`;
- $(
- `<section role="doc-endnotes">${makeHeading(
- "Endnotes",
- `${firstHeadingId}-endnotes`,
- level,
- )}<ol type="1" id="${olId}"></ol></section>`,
- ).insertBefore($("li.en").get(0));
- ens.remove();
- ens.appendTo(`#${olId}`);
- return $.html();
- });
- eleventyConfig.addTransform("hyphens", function (content) {
- if (this.page.url === "/atom.xml") {
- return content;
- }
- const $ = cheerio.load(content);
- replaceHyphenMinuses($("body")[0]);
- return $.html();
- });
- const externalUriRe = /^([a-zA-Z][a-zA-Z0-9\+\.\-]*:.|\/\/)/;
- eleventyConfig.addTransform("addrels", function (content) {
- if (this.page.url === "/atom.xml") {
- return content;
- }
- const $ = cheerio.load(content);
- $("a").each((_, a) => {
- const href = $(a).attr("href");
- if (externalUriRe.test(href)) {
- let rel = ($(a).attr("rel") ?? "").trim();
- if (rel.length > 0) {
- rel += " ";
- }
- rel += "external noopener noreferrer";
- $(a).attr("rel", rel);
- }
- });
- return $.html();
- });
- const hasLetterRe = /[a-zA-Z]/;
- eleventyConfig.addTransform("datetimes", function (content) {
- if (this.page.url === "/atom.xml") {
- return content;
- }
- const $ = cheerio.load(content);
- $("time[datetime]").each((_, time) => {
- const datetime = $(time).attr("datetime");
- if (!$(time).attr("title") && hasLetterRe.test(datetime)) {
- $(time).attr("title", datetime);
- }
- });
- return $.html();
- });
- eleventyConfig.addTransform("titlelangs", function (content) {
- if (this.page.url === "/atom.xml") {
- return content;
- }
- const $ = cheerio.load(content);
- $("[lang]").each((_, elem) => {
- const lang = $(elem).attr("lang");
- const title = $(elem).attr("title");
- if (!lang || !!title) {
- return;
- }
- const newTitle = langToTitle(lang);
- $(elem).attr("title", newTitle);
- });
- return $.html();
- });
- eleventyConfig.setFrontMatterParsingOptions({
- language: "json",
- });
- return {
- dir: {
- input: "src",
- output: "dist",
- },
- htmlTemplateEngine: "njk",
- };
- }
|