blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

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 }