src/lib/satteri-lang.mjs (2274B)
1 /**
2 * satteri-lang — `lang` on the passages of a post that are not in the post's language.
3 *
4 * The article element carries one lang, docLang() of the body. A screen reader picks its
5 * voice from lang (WCAG 3.1.2), so without this two kinds of passage are read in the wrong
6 * one:
7 *
8 * sections The bilingual essays split themselves with a heading that reads exactly
9 * EN or KR. Everything after it, up to the next heading of the same level or
10 * higher, is in that language. AI Odyssey is an English document by its prose
11 * ratio, so its KR half — 1,754 Hangul characters — would be read by an
12 * English voice.
13 * headings A heading in the other script, like the English h3s of a Korean post.
14 *
15 * Only top-level blocks and headings are marked, and only when they differ from the
16 * article: a matching lang would make the reader switch voices for nothing. Paragraphs are
17 * not judged one by one, because a single quoted Korean word would flip an English
18 * paragraph. Code is left alone; it is not prose in either language.
19 *
20 * It runs last among the hast plugins, so it sees demoted headings and baked diagrams.
21 */
22 import { docLang, langOf } from './hangul.ts';
23
24 const MARKERS = { EN: 'en', KR: 'ko' };
25 const isCode = (node) =>
26 node.tagName === 'pre' || (node.tagName === 'figure' && [].concat(node.properties?.className ?? []).includes('code'));
27
28 export default function satteriLang() {
29 return {
30 name: 'lang',
31 after(root, ctx) {
32 const doc = docLang(ctx.source);
33 let section = null;
34 for (const node of root.children) {
35 if (node.type !== 'element') continue;
36 const depth = /^h[1-6]$/.test(node.tagName) ? Number(node.tagName[1]) : 0;
37 if (depth) {
38 if (section && depth <= section.depth) section = null;
39 const text = ctx.textContent(node).trim();
40 if (MARKERS[text]) {
41 section = { lang: MARKERS[text], depth };
42 continue;
43 }
44 const own = langOf(text, doc);
45 if (own) ctx.setProperty(node, 'lang', own);
46 continue;
47 }
48 if (section && section.lang !== doc && !isCode(node)) ctx.setProperty(node, 'lang', section.lang);
49 }
50 },
51 };
52 }