12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338 |
- 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,
- },
- );
- const inkscapeCmd = `inkscape --export-type=svg --vacuum-defs -l -D -T --export-area-snap --export-overwrite ${svgPath}`;
- await exec(inkscapeCmd).catch(() =>
- exec(inkscapeCmd),
- );
- 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;
- }
- function enOrdSuffix(n) {
- const last = n % 10;
- const penultimate = Math.trunc(n / 10) % 10;
- if (penultimate !== 1) {
- if (last === 1) {
- return "st";
- }
- if (last === 2) {
- return "nd";
- }
- if (last === 3) {
- return "rd";
- }
- }
- return "th";
- }
- function lowerToGreek(letter) {
- if (letter === letter.toUpperCase()) {
- return letter;
- }
- const letterPoint = letter.codePointAt(0);
- const offset = (() => {
- if (letterPoint < 0x72) {
- return 0x350;
- }
- if (letterPoint === 0x79) {
- return 0x364;
- }
- if (letterPoint === 0x7a) {
- return 0x35f;
- }
- return 0x351;
- })();
- return String.fromCodePoint(letterPoint + offset);
- }
- 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 -%}
- {%- if not peaust.data.listless -%}
- {%- 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>
- {%- endif -%}
- {%- 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;
- const QUO_RE = /&[lr][sd]quo;|[\u2018\u2019\u201c\u201d]/g;
- function swapQuoteLevels(s) {
- QUO_RE.lastIndex = 0;
- let seenLsquo = false;
- return s.replaceAll(QUO_RE, match => {
- switch (match) {
- case "‘":
- case "\u2018": {
- seenLsquo = true;
- return "“";
- }
- case "’":
- case "\u2019": {
- const ret = seenLsquo ? "”" : "’";
- seenLsquo = false;
- return ret;
- }
- case "“":
- case "\u201c":
- return "‘";
- case "”":
- case "\u201d":
- return "’";
- }
- });
- }
- 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}">${swapQuoteLevels(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-zA-Z]'*([1-9][0-9]*)?)+$/;
- const FORM_SECT_RE_G = /([a-zA-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>(?<norm>=)?(?<curly>\{)?([cosip][1-9]?)+\}?)?$/;
- const DELTA_RE_G = /(-[1-9][0-9]*)|(0)|(\+[1-9][0-9]*)/g;
- const FUN_RE = /^[TSDP]$/;
- const FORTE_LUT = new Map([
- ["2-2", [0, 2]],
- ["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-21", [0, 2, 4, 6]],
- ["4-22", [0, 2, 4, 7]],
- ["4-22B", [0, 3, 5, 7]],
- ["4-23", [0, 2, 5, 7]],
- ["4-Z29", [0, 1, 3, 7]],
- ["4-Z29B", [0, 4, 6, 7]],
- ["5-23", [0, 2, 3, 5, 7]],
- ["5-23B", [0, 2, 4, 5, 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 1:
- return ["no1", "with omitted root"];
- 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",
- acci === "b" ? "subtonic" : "leading‐tone",
- ];
- }
- })();
- 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 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 parseCpath(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)",
- ];
- }
- 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;
- FUN_RE.lastIndex = 0;
- switch ("" + ty) {
- case "undefined":
- break;
- case "0":
- return [text, undefined];
- case "c": {
- const cpathMatch = CPATH_RE.exec("" + text);
- if (cpathMatch) {
- return parseCpath(cpathMatch);
- }
- console.error("Couldn't parse cpath:", text);
- throw "";
- }
- case "fun": {
- const funMatch = FUN_RE.exec(text);
- if (!funMatch) {
- return;
- }
- const [s, t] = (() => {
- switch (text) {
- case "T":
- return [
- "Ⓣ",
- "“tonic” function",
- ];
- case "S":
- return [
- "Ⓢ",
- "“subdominant” or “pre‐dominant” function",
- ];
- case "D":
- return [
- "Ⓓ",
- "“dominant” function",
- ];
- case "P":
- return [
- "Ⓟ",
- "cadential function without “dominant” function",
- ];
- default: {
- console.error(
- "Unrecognized harmonic function:",
- text,
- );
- throw "";
- }
- }
- })();
- return [`<span class="piggpar">${s}</span>`, t];
- }
- case "mot": {
- const [motText, norm] = text.startsWith("=")
- ? [text.slice(1), true]
- : [text, false];
- return [
- `${norm ? "⇕" : ""}${motText}`,
- `An instance of ${motionName(motText)} motion${
- norm ? " (normalized)" : ""
- }`,
- ];
- }
- case "->": {
- const arrowMatch = ARROW_RE.exec(text);
- if (!arrowMatch) {
- return;
- }
- const { deltas, motions, norm, 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" />' +
- (norm ? "<mo>⇕</mo>" : "") +
- (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 = enOrdSuffix(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 + " " : ""}` +
- (n === 1
- ? "root of a chord"
- : `${n}${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]) =>
- `${lowerToGreek(letter)}${prime(
- p.length,
- )}${n ? "<sub>" + n + "</sub>" : ""}`,
- )
- .join("⁣") +
- "⟫",
- matches.length > 1
- ? `A musical form with ${matches.length} sections`
- : `The ${lowerToGreek(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 no-wrap"><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 ${
- acci === "b"
- ? "flattened "
- : acci === "#"
- ? "sharpened "
- : ""
- }${n}${enOrdSuffix(n)} degree of a key or mode`,
- ];
- }
- const intCollectMatch = INT_COLLECT_RE.exec(text);
- if (intCollectMatch) {
- const [lBrack, rBrack, collectTy] = (() => {
- switch (intCollectMatch[1]) {
- case "{":
- return ["{", "}", "set"];
- case "[":
- return ["[", "]", "row"];
- case "<":
- return ["⟨", "⟩", "sequence"];
- default: {
- console.error(
- "Unrecognized intCollectMatch[1]:",
- intCollectMatch[1],
- );
- throw "";
- }
- }
- })();
- const ints = text.match(INT_RE_G).map(m => +m);
- const intStrs = ints.map(subOctaveIntToStr);
- const joined = intStrs.every(s => s)
- ? intStrs.join("⁣")
- : ints.map(i => `${i}`.replace("-", "−")).join(", ");
- return [
- `${lBrack}${joined}${rBrack}`,
- `The ${collectTy} of pitchclasses, in integer notation: ${lBrack}${joined}${rBrack}`,
- ];
- }
- const forteMatch = FORTE_RE.exec(text);
- if (forteMatch) {
- const [, forte, inv, q] = forteMatch;
- const m = forte + (inv ? inv : "");
- 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}${
- q ? "" : "<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) {
- return parseCpath(cpathMatch);
- }
- 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, dots, 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}${
- dots ? dots.replaceAll(".", "𝅭") : ""
- }${accent ? "𝅻" : ""}`;
- })
- .join("");
- return [
- `${
- brack ? "⟨⁠" : ""
- }<span class="mus-notes">${joined}</span>${
- brack ? "⁠⟩" : ""
- }`,
- undefined,
- ];
- }
- const motionMatch = MOTION_RE.exec(text);
- if (motionMatch) {
- const [, norm, 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 [
- `${norm ? "⇕" : ""}${lBrack}${joined}${rBrack}`,
- `An instance of polyphonic motion: ${fullJoined}${
- full.length > 1
- ? sq
- ? ""
- : " (in no particular order)"
- : " motion"
- }${norm ? " (normalized)" : ""}`,
- ];
- }
- }
- 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) {
- const ed = parseInt(entry.fields.edition, 10);
- refs += `, <span itemprop="version">${ed}<sup>${enOrdSuffix(
- ed,
- )}</sup> 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.addShortcode(
- "caesura",
- () => '<span style="font-style: normal;">‖</span>',
- );
- 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";
- case "verse":
- return "Poem";
- 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",
- };
- }
|