Scowl like Thucydides

Finds Ancient Greek text, converts it to archaic majuscule with updated OU/EI logic, and applies a custom font.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Scowl like Thucydides
// @namespace    http://tathamei.com/
// @version      3.1
// @description  Finds Ancient Greek text, converts it to archaic majuscule with updated OU/EI logic, and applies a custom font.
// @author       Tathamei
// @match        *://*/*
// @exclude      https://tathamei.com/*
// @exclude      https://docs.google.com/*
// @exclude      https://suno.com/*
// @exclude      https://www.google.com/search*
// @exclude      https://google.com/search*
// @license      Proprietary
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';

  // ======================= 1) FONT & CLASS =======================
  GM_addStyle(`
    @font-face {
      font-family: 'ArchaicGreekFont';
      /* Replace with your actual base64 font data */
      src: url(data:font/woff2;base64,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) format('woff2');
      font-weight: normal;
      font-style: normal;
    }
  .archaic-greek-text {
    font-family: 'ArchaicGreekFont', sans-serif !important;
    font-size: calc(1em + 2px) !important;
  }
`);

  // Editable guard (don’t touch inputs/editors)
  const EDITABLE_SELECTOR =
    'input, textarea, select, option, [contenteditable]:not([contenteditable="false"]), [role="textbox"], .ProseMirror, .ql-editor, .public-DraftEditor-content';
  function isEditableRoot(el) {
    if (!el) return false;
    if (el.isContentEditable) return true;
    return !!el.closest(EDITABLE_SELECTOR);
  }

  // ======================= 2) MAPS (Majuscule + Macrons) =======================
  const archaicMajusculeMap = {
    // --- VOWELS ---
    // Alpha
    'α':'Α','ἀ':'Α','ά':'Α','ὰ':'Α','ἄ':'Α','ἂ':'Α','ᾶ':'Α','ἆ':'Α',
    'Ἀ':'Α','Ἄ':'Α','Ἂ':'Α','Ἆ':'Α',
    'ἁ':'ͰΑ','ᾁ':'ͰΑΙ','ἇ':'ͰΑ','ἅ':'ͰΑ','ἃ':'ͰΑ',
    'ᾷ':'ΑΙ','ᾴ':'ΑΙ','ᾳ':'ΑΙ','ᾀ':'ΑΙ',
    'Ἁ':'ͰA','Ἅ':'ͰA',

    // Epsilon
    'ε':'Ε','ἐ':'Ε','έ':'Ε','ὲ':'Ε','ἔ':'Ε','ἒ':'Ε',
    'Ἐ':'Ε','Ἔ':'Ε','Ἒ':'Ε',
    'ἑ':'ͰΕ','ἓ':'ͰΕ','ἕ':'ͰΕ',
    'Ἑ':'ͰΕ','Ἕ':'ͰΕ',

    // Eta → Epsilon (archaic)
    'η':'Ε','ἠ':'Ε','ή':'Ε','ὴ':'Ε','ῆ':'Ε','ἤ':'Ε','ἢ':'Ε','ἦ':'Ε',
    'Η':'Ε','Ἠ':'Ε','Ἤ':'Ε','Ἢ':'Ε','Ἦ':'Ε',
    'ἡ':'ͰΕ','ᾗ':'ͰΕΙ','ἧ':'ͰΕ','ἥ':'ͰΕ','ἣ':'ͰΕ','ᾑ':'ͰΕΙ',
    'ῄ':'ΕΙ','ᾖ':'ΕΙ','ῇ':'ΕΙ','ῃ':'ΕΙ','ᾐ':'ΕΙ',
    'Ἡ':'ͰΕ',

    // Iota
    'ι':'Ι','ἰ':'Ι','ί':'Ι','ὶ':'Ι','ῖ':'Ι','ἴ':'Ι','ἶ':'Ι','ϊ':'Ι','ΐ':'Ι',
    'Ἰ':'Ι','Ἴ':'Ι','Ἲ':'Ι','Ἶ':'Ι',
    'ἳ':'ͰΙ','ἱ':'ͰΙ','ἵ':'ͰΙ','ἷ':'ͰΙ',
    'Ἱ':'ͰΙ','Ἵ':'ͰΙ',

    // Omicron
    'ο':'Ο','ὀ':'Ο','ό':'Ο','ὸ':'Ο','ὄ':'Ο','ὂ':'Ο',
    'Ὀ':'Ο','Ὄ':'Ο','Ὂ':'Ο',
    'ὁ':'ͰΟ','ὅ':'ͰΟ','ὃ':'ͰΟ',
    'Ὁ':'ͰΟ','Ὅ':'ͰΟ',

    // Omega → Omicron
    'ω':'Ο','ὠ':'Ο','ώ':'Ο','ὼ':'Ο','ῶ':'Ο','ὦ':'Ο','ὢ':'Ο',
    'Ω':'Ο','Ὠ':'Ο','Ὤ':'Ο','Ὢ':'Ο','Ὦ':'Ο',
    'ὡ':'ͰΟ','ὧ':'ͰΟ','ὥ':'ͰΟ',
    'ᾧ':'ͰΟΙ','ῳ':'ΟΙ','ῷ':'ΟΙ','ᾠ':'ΟΙ','ᾤ':'ΟΙ','ῴ':'ΟΙ',
    'Ὡ':'ͰΟ',

    // Upsilon
    'υ':'Υ','ὐ':'Υ','ύ':'Υ','ὺ':'Υ','ὔ':'Υ','ὒ':'Υ','ῦ':'Υ','ὖ':'Υ',
    'ὕ':'ͰΥ','ὑ':'ͰΥ','ὗ':'ͰΥ',
    'Ὑ':'ͰΥ','Ὓ':'ͰΥ','Ὗ':'ͰΥ','Ὕ':'ͰΥ',

    // Consonants
    'β':'Β','γ':'Γ','δ':'Δ','ζ':'Ζ','θ':'Θ','κ':'Κ','λ':'Λ','μ':'Μ','ν':'Ν','π':'Π','ρ':'Ρ','ς':'Σ','σ':'Σ','τ':'Τ','φ':'Φ','χ':'Χ','ͱ':'Ͱ',
    // Rho with rough
    'ῥ':'ͰΡ',

    // Compounds
    'ξ':'ΧΣ','Ξ':'ΧΣ','ψ':'ΦΣ','Ψ':'ΦΣ'
  };

  const archaicMap_MacronsOn = {
    // Alpha (macron)
    'ᾶ':'Ā','ἆ':'Ā','Ἆ':'Ā','ἇ':'ͰĀ','ᾷ':'ĀΙ',
    // Eta → Ē
    'η':'Ē','ἠ':'Ē','ή':'Ē','ὴ':'Ē','ῆ':'Ē','ἤ':'Ē','ἢ':'Ē','ἦ':'Ē',
    'Η':'Ē','Ἠ':'Ē','Ἤ':'Ē','Ἢ':'Ē','Ἦ':'Ē',
    'ἡ':'ͰĒ','ᾗ':'ͰĒΙ','ἧ':'ͰĒ','ἥ':'ͰĒ','ἣ':'ͰĒ','ᾑ':'ͰĒΙ',
    'ῄ':'ĒΙ','ᾖ':'ĒΙ','ῇ':'ĒΙ','ῃ':'ĒΙ','ᾐ':'ĒΙ',
    'Ἡ':'ͰĒ',
    // Iota
    'ῖ':'Ī','ἶ':'Ī','Ἶ':'Ī','ἷ':'ͰĪ',
    // Omega → Ō
    'ω':'Ō','ὠ':'Ō','ώ':'Ō','ὼ':'Ō','ῶ':'Ō','ὦ':'Ō','ὢ':'Ō',
    'Ω':'Ō','Ὠ':'Ō','Ὤ':'Ō','Ὢ':'Ō','Ὦ':'Ō',
    'ὡ':'ͰŌ','ὧ':'ͰŌ','ὥ':'ͰŌ',
    'ᾧ':'ͰŌΙ','ῳ':'ŌΙ','ῷ':'ŌΙ','ᾠ':'ŌΙ','ᾤ':'ŌΙ','ῴ':'ŌΙ',
    'Ὡ':'ͰŌ',
    // Upsilon
    'ῦ':'Ȳ','ὖ':'Ȳ','ὗ':'ͰȲ','Ὗ':'ͰȲ'
  };

  let MACRONS_ON = false;
  function mapChar(ch) {
    if (MACRONS_ON && archaicMap_MacronsOn[ch] != null) return archaicMap_MacronsOn[ch];
    return (archaicMajusculeMap[ch] != null) ? archaicMajusculeMap[ch] : ch;
  }

  // ======================= 3) OU & EI LOGIC (updated) =======================
  // Charsets for ο and υ variants (explicit)
  const OMICRONS = "οὀὈὄὌὂὊόὸὁὉὅὍὃ";
  const UPSILONS = "υὐὔὒὖῦὕὗὑὙὛὝὟύὺ";
  const OU_ANY_RE     = new RegExp("[" + OMICRONS + "][" + UPSILONS + "]", "g");
  const OU_ANY_RE_ONE = new RegExp("[" + OMICRONS + "][" + UPSILONS + "]");

  // For EI pairs (ε + ι variants)
  const EPSILONS = "εἐἑέὲἔἒΕἘἙἜἝἚ";
  const IOTAS    = "ιἰἱίὶῖἴἳἶἷϊΐΙἸἹἼἺἾἵἽ";
  const EI_ANY_RE     = new RegExp("[" + EPSILONS + "][" + IOTAS + "]", "g");
  const EI_ANY_RE_ONE = new RegExp("[" + EPSILONS + "][" + IOTAS + "]");

  // Rough-breathing upsilons (lower + upper)
  const ROUGH_UPSILON = "ὑὕὓὗὙὝὛὟ";

  // Normalize (strip diacritics, keep Greek letters), lowercase
  function normalizeForLookup(word) {
    return word.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
  }

  // ---- OU “keep” & “collapse” sets (updated) ----
  // Keep whole-word OU (true diphthongs / particles)
  const KEEP_TRUE_OU_WORDS = new Set([
    'ουν','ου','ους','βους','τουτο','που',
    'ουκ','ουχ','ουχι','ουδε' // negative particle and friends
  ]);
  // Collapse ALL OU in entire lexeme (e.g., βουλή*, δοῦναι)
  const COLLAPSE_WHOLE_WORDS = new Set(['βουλη','βουλης','βουλην','δουναι']);

  // OU endings (updated: includes -ουντι)
  const ENDINGS = [
    { re: /ουσιν$/,  ouLen: 5 },
    { re: /ουσι$/,   ouLen: 4 },
    { re: /ουμεν$/,  ouLen: 5 },
    { re: /ουσα$/,   ouLen: 4 },
    { re: /ουντος$/, ouLen: 6 },
    { re: /ουντες$/, ouLen: 6 },
    { re: /ουντα$/,  ouLen: 5 },
    { re: /ουν$/,    ouLen: 3 }, // guarded by KEEP_TRUE_OU_WORDS (οὖν)
    { re: /ους$/,    ouLen: 3 },
    { re: /ου$/,     ouLen: 2 },
    { re: /ουμαι$/,  ouLen: 5 },
    { re: /ουται$/,  ouLen: 5 },
    { re: /ουνται$/, ouLen: 6 },
    { re: /ουμεθα$/, ouLen: 7 },
    { re: /ουσθε$/,  ouLen: 6 },
    { re: /ουσθαι$/, ouLen: 6 },
    { re: /ουμην$/,  ouLen: 5 },
    { re: /ουτο$/,   ouLen: 4 }, // guarded for demonstrative τοῦτο
    { re: /ουντο$/,  ouLen: 5 },
    { re: /ουντι$/,  ouLen: 5 }  // NEW
  ];

  // Reposition rough breathing for OU diphthongs:
  // α/ε/ο + (rough upsilon) → ͱ + (lowercase base) + υ
  function repositionRoughOU(s) {
    const chars = Array.from(s);
    const out = [];
    for (let i = 0; i < chars.length; i++) {
      const c = chars[i];
      const nextExists = i + 1 < chars.length;
      const n = nextExists ? chars[i + 1] : '';
      const isBase = (c === 'α' || c === 'Α' || c === 'ε' || c === 'Ε' || c === 'ο' || c === 'Ο');
      const isRoughU = nextExists && ROUGH_UPSILON.includes(n);
      if (isBase && isRoughU) {
        out.push('ͱ');      // small heta placeholder (later unified to Ͱ)
        out.push(c.toLowerCase());
        out.push('υ');
        i++;                // consume the rough‑upsilon char
        continue;
      }
      out.push(c);
    }
    return out.join('');
  }

  // NEW: collapse INTERNAL spurious OU from contractions (ε+ο, ο+ε, ο+ο)
  function collapseInternalOU(coreWord, showMacrons) {
    if (!OU_ANY_RE_ONE.test(coreWord)) return null;

    const norm = normalizeForLookup(coreWord);
    if (KEEP_TRUE_OU_WORDS.has(norm)) return null;

    const replacement = showMacrons ? 'Ō' : 'Ο';

    // participle stem: -οῦντ-
    const idxNT = norm.indexOf('ουντ');
    if (idxNT !== -1) {
      return coreWord.slice(0, idxNT) + replacement + coreWord.slice(idxNT + 2);
    }

    // middle/passive participle & related: -ούμεν-
    const idxME = norm.indexOf('ουμε');
    if (idxME !== -1) {
      return coreWord.slice(0, idxME) + replacement + coreWord.slice(idxME + 2);
    }

    // contracted οῦ anywhere inside
    const rawCirc = coreWord.indexOf('οῦ');
    if (rawCirc !== -1) {
      return coreWord.slice(0, rawCirc) + replacement + coreWord.slice(rawCirc + 2);
    }

    return null;
  }

  // Collapse the LAST ending-OU ONLY (index via normalized string)
  function collapseEndingOU(coreWord, showMacrons) {
    const norm = normalizeForLookup(coreWord);
    if (KEEP_TRUE_OU_WORDS.has(norm)) return null; // keep οὖν/οὐ/οὖς/βοῦς/τοῦτο/...

    for (const { re, ouLen } of ENDINGS) {
      const m = norm.match(re);
      if (!m) continue;
      if (norm === 'τουτο') continue; // guard demonstrative

      const start = norm.length - ouLen;      // position of ending's 'ο'
      const replacement = showMacrons ? 'Ō' : 'Ο';
      return coreWord.slice(0, start) + replacement + coreWord.slice(start + 2);
    }
    return null;
  }

  // ---- ἐν assimilation before β / π (no punctuation crossing) ----
  function applyEnAssimilation(tokens) {
    const isWhitespace  = s => /^[ \n\r\t]+$/.test(s);
    const isPunct       = s => /^[.,?!;:᾽'"]+$/.test(s);
    const startsWithBetaOrPi = s => {
      if (!s) return false;
      const look = s.replace(/^[\(\[\{«"“]+/, '');
      const first = look[0];
      return first === 'β' || first === 'Β' || first === 'π' || first === 'Π';
    };

    const out = tokens.slice();
    for (let i = 0; i < out.length; i++) {
      if (out[i] === 'ἐν') {
        let j = i + 1;
        while (j < out.length && isWhitespace(out[j])) j++;
        if (j < out.length && !isPunct(out[j]) && startsWithBetaOrPi(out[j])) {
          out[i] = 'ἐμ';
        }
      }
    }
    return out;
  }

  // ---- EI (spurious) helpers & rules (ported) ----
  // “keep as true ει” — exact forms
  const KEEP_TRUE_EI_WORDS = new Set([
    'ει',      // εἰ “if”
    'εις',     // prep. εἰς
    'ημεις',   // ἡμεῖς
    'υμεις',   // ὑμεῖς
    'ταθαμει', // project-specific keep from converter
    'δειν'     // δειν
  ]);
  // keep by stem prefix
  const KEEP_TRUE_EI_PREFIXES = ['πειρ']; // (σπειρ removed so verbs can collapse)

  // forced collapse (exact)
  const FORCE_EI_COLLAPSE_EXACT = new Set(['ειμι','ειναι']); // εἰμί, εἶναι

  // forced collapse (prefix stems)
  const FORCE_EI_COLLAPSE_PREFIXES = [
    'ξειν','στειν','κτειν','ιμειρ','μειζ','φθειρ',
    'τειν', // τείνω family
    'νειμ'  // νεῖμαι family
  ];

  // forced collapse (regex families) + guard
  const FORCE_EI_COLLAPSE_REGEXPS = [
    { re: /^χειρ(ων|ον|ονος|ονι|ονα|ιστ)/, guard: (original) => !/ῶ/.test(original) },
    { re: /^σπειρ(ω|εις|ει|ομεν|ετε|ουσιν?|ειν)$/, guard: null } // verbs only (not σπεῖρα)
  ];

  // LN aorists suffix families (augment-gated)
  const LN_ACTIVE_SUFFIXES = [
    // -ειλα
    'ειλα','ειλας','ειλε','ειλεν','ειλαμεν','ειλατε','ειλαν',
    // -εινα
    'εινα','εινας','εινε','εινεν','ειναμεν','εινατε','ειναν',
    // -ειρα
    'ειρα','ειρας','ειρε','ειρεν','ειραμεν','ειρατε','ειραν',
    // -ειμα (e.g., ἔνειμα)
    'ειμα','ειμας','ειμε','ειμεν','ειμαμεν','ειματε','ειμαν'
  ];
  const PREVERB_PREFIXES = [
    'απ','κατ','μετ','παρ','προσ','συν','εκ','εξ','εν','αν',
    'υπ','υπερ','προ','περι','παρα','εισ','αμφ','ανα','καθ','δια','δι'
  ];
  function hasAugmentLike(norm) {
    if (!norm) return false;
    if (norm.startsWith('ε') || norm.startsWith('η')) return true;
    for (const pv of PREVERB_PREFIXES) {
      if (norm.startsWith(pv + 'ε') || norm.startsWith(pv + 'η')) return true;
    }
    return false;
  }

  function isNumeralEis(word) {
    return word === 'εἷς' || word === 'Εἷς' || word === 'εἷΣ' || word === 'ΕἷΣ';
  }
  function isNonSpuriousEimi(original) {
    return original === 'εἶμι' || original === 'Εἶμι';
  }

  function collapseLiquidNasalAoristEI(coreWord, replacer) {
    const norm = normalizeForLookup(coreWord);
    if (!hasAugmentLike(norm)) return null;
    for (const suf of LN_ACTIVE_SUFFIXES) {
      if (norm.endsWith(suf)) {
        const start = norm.length - suf.length; // index of suffix's 'ε'
        return coreWord.slice(0, start) + replacer + coreWord.slice(start + 2);
      }
    }
    return null;
  }

  function collapseForcedLexemeEI(coreWord, replacer) {
    if (isNonSpuriousEimi(coreWord)) return null; // DO NOT collapse εἶμι
    const norm = normalizeForLookup(coreWord);

    // exact forms
    if (FORCE_EI_COLLAPSE_EXACT.has(norm)) {
      return coreWord.replace(EI_ANY_RE, replacer);
    }
    // patterned families (e.g., χείρων/χείριστος), with guard (avoid χερῶν)
    for (const { re, guard } of FORCE_EI_COLLAPSE_REGEXPS) {
      if (re.test(norm) && (!guard || guard(coreWord))) {
        return coreWord.replace(EI_ANY_RE, replacer);
      }
    }
    // stem prefixes (μείζ‑, φθείρ‑, τείν‑, νείμ‑, …)
    for (const p of FORCE_EI_COLLAPSE_PREFIXES) {
      if (norm.startsWith(p)) {
        return coreWord.replace(EI_ANY_RE, replacer);
      }
    }
    return null;
  }

  function collapseSpuriousEI(coreWord, replacer) {
    if (!EI_ANY_RE_ONE.test(coreWord)) return null;

    // εἷς → Ͱ + replacer + Σ (majuscule stream wants heta)
    if (isNumeralEis(coreWord)) return 'Ͱ' + replacer + 'Σ';

    // LN aorists (augment-gated)
    const ln = collapseLiquidNasalAoristEI(coreWord, replacer);
    if (ln !== null) return ln;

    // keep true diphthongs
    const norm = normalizeForLookup(coreWord);
    if (KEEP_TRUE_EI_WORDS.has(norm)) return null;
    for (const keepPref of KEEP_TRUE_EI_PREFIXES) {
      if (norm.startsWith(keepPref)) return null;
    }

    // forced lexemes/stems/patterns
    const forced = collapseForcedLexemeEI(coreWord, replacer);
    if (forced !== null) return forced;

    // generic spurious endings (collapse only the ending’s ει)
    const EI_ENDINGS = [
      { re: /ειν$/, eiLen: 3 },
      { re: /εις$/, eiLen: 3 },
      { re: /ει$/,  eiLen: 2 },
      { re: /ειται$/,  eiLen: 5 }, { re: /εισθε$/,  eiLen: 6 }, { re: /εισθαι$/, eiLen: 6 },
      { re: /ειτε$/,   eiLen: 4 }, { re: /ειτο$/,   eiLen: 4 }, { re: /ειμην$/,  eiLen: 5 },
      { re: /εισο$/,   eiLen: 4 }, { re: /ειμεν$/,  eiLen: 5 }, { re: /εισαν$/,  eiLen: 5 },
      { re: /εια$/,    eiLen: 3 },
      { re: /εις$/,    eiLen: 3 } // βασιλεῖς/πρέσβεις
    ];

    for (const { re, eiLen } of EI_ENDINGS) {
      const m = norm.match(re);
      if (!m) continue;
      const start = norm.length - eiLen; // index of ending's 'ε'
      return coreWord.slice(0, start) + replacer + coreWord.slice(start + 2);
    }
    return null;
  }

  // Per‑word OU handling with lexeme/ending/internal rules
  function processWordForArchaicOU(word, showMacrons) {
    const punctuationRegex = /[.,?!;:᾽'"]+$/;
    const trailing = (word.match(punctuationRegex) || [''])[0];
    let core = word.replace(punctuationRegex, '');
    if (!core) return word;

    // 0) Restore rough‑OU placement
    core = repositionRoughOU(core);

    // 1) Try INTERNAL contracted OU first
    const internal = collapseInternalOU(core, showMacrons);
    if (internal !== null) {
      core = internal;
    }

    // 2) If no OU now, skip
    if (!OU_ANY_RE_ONE.test(core)) {
      return core + trailing;
    }

    const norm = normalizeForLookup(core);

    // 3) Whole‑word keep (οὐκ, οὐ, οὖν, βοῦς, τοῦτο, που, …)
    if (KEEP_TRUE_OU_WORDS.has(norm)) {
      return core + trailing;
    }

    // 4) Whole‑lexeme force collapse (βουλή*, δοῦναι)
    if (COLLAPSE_WHOLE_WORDS.has(norm)) {
      const replacement = showMacrons ? 'Ō' : 'Ο';
      const replaced = core.replace(OU_ANY_RE, replacement);
      return replaced + trailing;
    }

    // 5) Ending‑only collapse
    const collapsed = collapseEndingOU(core, showMacrons);
    if (collapsed !== null) return collapsed + trailing;

    // 6) Default: leave true OU; mapping will make ΟΥ
    return core + trailing;
  }

  // Per‑word EI handling (collapse to Ε or Ē before mapping)
  function processWordForArchaicEI(word, showMacrons) {
    const punctuationRegex = /[.,?!;:᾽'"]+$/;
    const trailing = (word.match(punctuationRegex) || [''])[0];
    let core = word.replace(punctuationRegex, '');
    if (!core) return word;

    const replacer = showMacrons ? 'Ē' : 'Ε';
    const collapsed = collapseSpuriousEI(core, replacer);
    if (collapsed !== null) return collapsed + trailing;

    return core + trailing;
  }

  // ======================= 4) CORE CONVERTER =======================
  const diphthongVowelBases = new Set(['α','ε','ο','η','ω','Α','Ε','Ο','Η','Ω']);
  const roughBreathingSecond = new Set([
    'ἱ','ἵ','ἳ','ἷ','Ἱ','Ἵ','Ἳ','Ἷ',  // iota rough
    'ὑ','ὕ','ὓ','ὗ','Ὑ','Ὕ','Ὓ','Ὗ'   // upsilon rough
  ]);

  // Convert a *single Greek word* to archaic majuscule with current macron mode
  function convertToArchaic(word) {
    const normalizedInput = word.normalize('NFC');

    // OU + EI morphology (internal/ending/lexeme rules; augment‑gated LN aorists; keeps)
    const afterOU = processWordForArchaicOU(normalizedInput, MACRONS_ON);
    const prepped  = processWordForArchaicEI(afterOU, MACRONS_ON);

    // Map char‑by‑char; handle rough second‑vowel diphthongs for big H (Ͱ) placement
    let out = '';
    for (let i = 0; i < prepped.length; i++) {
      const ch = prepped[i];
      const prev = i > 0 ? prepped[i - 1] : null;

      if (prev && diphthongVowelBases.has(prev) && roughBreathingSecond.has(ch)) {
        const prevOut = mapChar(prev);
        // Remove previously appended prevOut (we're going to prepend Ͱ instead)
        out = out.slice(0, -prevOut.length);
        const currNoBreath = mapChar(ch).replace(/[ͱͰ]/g, '');
        out += 'Ͱ' + prevOut + currNoBreath;
        continue;
      }

      out += mapChar(ch);
    }

    // Unify small heta → big; force Ω/ω → Ο; κφ → ΧΦ after uppercasing
    out = out.replace(/ͱ/g, 'Ͱ').replace(/[Ωω]/g, 'Ο');
    out = out.replace(/ΚΦ/g, 'ΧΦ');

    return out;
  }

  // ======================= 5) NODE PROCESSING =======================
  // Detect nodes to process (Greek blocks or Latin macron letters)
//  const ALL_GREEK_CHARS_REGEX = /([\u0100\u0112\u012a\u014c\u016a\u0232\u0370-\u03FF\u1F00-\u1FFF]+)/gu;
const ALL_GREEK_CHARS_REGEX = /([\u0100\u0101\u0112\u0113\u012A\u012B\u014C\u014D\u016A\u016B\u0232\u0233\u0370\u0371\u0390\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039A\u039B\u039C\u039D\u039F\u03A0\u03A1\u03A3\u03A4\u03A5\u03A6\u03A7\u03A9\u03AC\u03AD\u03AE\u03AF\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03B9\u03BA\u03BB\u03BC\u03BD\u03BE\u03BF\u03C0\u03C1\u03C2\u03C3\u03C4\u03C5\u03C6\u03C7\u03C8\u03c9\u03CA\u03CC\u03CE\u03CD\u1F00\u1F02\u1F04\u1F06\u1F08\u1F0A\u1F0C\u1F0E\u1F10\u1F12\u1F14\u1F18\u1F1A\u1F1C\u1F20\u1F21\u1F22\u1F24\u1F26\u1F28\u1F2A\u1F2C\u1F2E\u1F30\u1F34\u1F36\u1F38\u1F3A\u1F3C\u1F3E\u1F40\u1F42\u1F44\u1F48\u1F4A\u1F4C\u1F50\u1F52\u1F54\u1F56\u1F5D\u1F60\u1F62\u1F66\u1F68\u1F6A\u1F6C\u1F6E\u1F70\u1F72\u1F74\u1F76\u1F78\u1F7A\u1F7C\u1F80\u1FB6\u1FC6\u1FD6\u1FE6\u1FF6\u{10144}\u{10145}\u002c\u002e\u0022\u003a\u0027\u003b])/gu;

  function processAndWrap(rootNode) {
    if (![Node.ELEMENT_NODE, Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(rootNode.nodeType)) {
      return;
    }

    const walker = document.createTreeWalker(
      rootNode,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode(node) {
          const p = node.parentElement;
          if (!p) return NodeFilter.FILTER_REJECT;
          if (isEditableRoot(p)) return NodeFilter.FILTER_REJECT;

          const tag = p.tagName ? p.tagName.toUpperCase() : '';
          if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA') return NodeFilter.FILTER_REJECT;
          if (p.closest('.archaic-greek-text')) return NodeFilter.FILTER_REJECT;

          if (ALL_GREEK_CHARS_REGEX.test(node.nodeValue)) {
            ALL_GREEK_CHARS_REGEX.lastIndex = 0;
            return NodeFilter.FILTER_ACCEPT;
          }
          return NodeFilter.FILTER_SKIP;
        }
      }
    );

    const nodes = [];
    let n;
    while ((n = walker.nextNode())) nodes.push(n);

    const TOKEN_SPLIT = /([ \n\r\t.,?!;:᾽'"]+)/; // spaces & punctuation as standalone tokens
    const GREEK_RE = /[\u0370-\u03FF\u1F00-\u1FFF]/; // does token contain Greek?

    for (const node of nodes) {
      const text = node.nodeValue;
      const parts = text.split(TOKEN_SPLIT);

      // ἐν → ἐμ before β/π across whitespace (no punctuation)
      const tokens = applyEnAssimilation(parts);

      const frag = document.createDocumentFragment();
      for (const tok of tokens) {
        if (!tok) continue;

        // whitespace or punctuation → keep as text
        if (/^[ \n\r\t]+$/.test(tok) || /^[.,?!;:᾽'"]+$/.test(tok)) {
          frag.appendChild(document.createTextNode(tok));
          continue;
        }

        // If token contains Greek, convert & wrap; else keep as text
        if (GREEK_RE.test(tok)) {
          const span = document.createElement('span');
          span.className = 'archaic-greek-text';
          span.setAttribute('data-original', tok);
          span.textContent = convertToArchaic(tok);
          frag.appendChild(span);
        } else {
          frag.appendChild(document.createTextNode(tok));
        }
      }

      if (node.parentNode) node.parentNode.replaceChild(frag, node);
    }
  }

  function reconvertAll(root = document) {
    root.querySelectorAll('.archaic-greek-text').forEach(span => {
      const original = span.getAttribute('data-original');
      if (original != null) {
        span.textContent = convertToArchaic(original);
      }
    });
  }

  // ======================= 6) DYNAMIC CONTENT HANDLING =======================
  // Initial pass
  processAndWrap(document.body);

  // Queue + throttle for observer flush
  const pending = new Set();
  let flushTimer = null;
  const FLUSH_DELAY = 50;
  const BATCH_SIZE = 20;

  function enqueue(node) {
    let el = null;
    if (node.nodeType === Node.ELEMENT_NODE) el = node;
    else if (node.nodeType === Node.TEXT_NODE) el = node.parentElement;
    if (!el) return;
    if (!document.body.contains(el)) return;
    if (isEditableRoot(el)) return;
    pending.add(el);
  }

  function minimizeRoots(nodes) {
    const set = new Set(nodes);
    return nodes.filter(n => {
      let p = n.parentElement;
      while (p) {
        if (set.has(p)) return false;
        p = p.parentElement;
      }
      return true;
    });
  }

  function scheduleFlush() {
    if (flushTimer) return;
    flushTimer = setTimeout(flush, FLUSH_DELAY);
  }

  function flush() {
    flushTimer = null;
    const unique = Array.from(pending);
    pending.clear();

    const roots = minimizeRoots(unique);
    let index = 0;

    function step() {
      const end = Math.min(index + BATCH_SIZE, roots.length);
      for (; index < end; index++) {
        processAndWrap(roots[index]);
      }
      if (index < roots.length) {
        setTimeout(step, 16);
      }
    }
    step();
  }

  // ======================= 7) MACRON TOGGLE UI =======================
  (function injectMacronToggle() {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.id = 'macronToggle';
    btn.textContent = 'Macrons: ' + (MACRONS_ON ? 'ON' : 'OFF');
    btn.style.cssText = [
      'position:fixed',
      'right:14px',
      'bottom:14px',
      'z-index:2147483647',
      'padding:8px 12px',
      'border-radius:10px',
      'border:1px solid rgba(255,255,255,.2)',
      'background:#111',
      'color:#eee',
      'font:600 12px system-ui,-apple-system,Segoe UI,Roboto,sans-serif',
      'box-shadow:0 2px 8px rgba(0,0,0,.3)',
      'cursor:pointer',
      'opacity:.85'
    ].join(';');

    btn.addEventListener('mouseenter', () => btn.style.opacity = '1');
    btn.addEventListener('mouseleave', () => btn.style.opacity = '.85');

    btn.addEventListener('click', () => {
      MACRONS_ON = !MACRONS_ON;
      btn.textContent = 'Macrons: ' + (MACRONS_ON ? 'ON' : 'OFF');
      reconvertAll();
    });

    document.documentElement.appendChild(btn);
  })();

  // ======================= 8) MUTATION OBSERVER =======================
  const observer = new MutationObserver((mutations) => {
    const all = mutations.concat(observer.takeRecords());
    for (const m of all) {
      if (m.type === 'childList') {
        m.addedNodes.forEach(enqueue);
        enqueue(m.target);
      } else if (m.type === 'characterData') {
        enqueue(m.target);
        enqueue(m.target.parentElement);
      }
    }
    scheduleFlush();
  });

  observer.observe(document.body, {
    childList: true,
    characterData: true,
    subtree: true
  });
})();