ΚΤΕΜΑ ΤΕ ΕΣ ΑΙΕΙ ΘΟΚΥΔΙΔΟ

Finds Ancient Greek text, transliterates it into an archaic majuscule style, and applies a custom font.

// ==UserScript==
// @name         ΚΤΕΜΑ ΤΕ ΕΣ ΑΙΕΙ ΘΟΚΥΔΙΔΟ
// @namespace    http://lunacy.wtf/
// @version      3.0
// @description  Finds Ancient Greek text, transliterates it into an archaic majuscule style, and applies a custom font.
// @author       Lunacy
// @match        *://*/*
// @exclude      https://lunacy.wtf/*
// @exclude      https://docs.google.com/*
// @exclude      https://suno.com/*
// @license      Proprietary
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ======================= 1. FONT AND STYLE INJECTION (From Script 2) =======================
    // This part injects the custom font and the CSS class that will be used to apply it.
    // The placeholder for the base64 font data must be replaced with your actual font file.
    GM_addStyle(`
      @font-face {
        font-family: 'ArchaicGreekFont';
        /* IMPORTANT: Replace the placeholder below with your actual base64 font data. */
        /* To convert a font: use a tool like https://www.base64encode.org/ to encode your .woff2 file and paste the result here. */
        src: url(data:font/woff2;base64,AAEAAAANAIAAAwBQRkZUTalYYsMAAA98AAAAHEdERUYAFQAUAAAPmAAAABxPUy8yWXxhVQAAAVgAAABgY21hcAxyTV0AAAI4AAACQmdhc3D//wADAAAPdAAAAAhnbHlml1ZrcgAABMAAAAcsaGVhZCuG5X0AAADcAAAANmhoZWEGGQMkAAABFAAAACRobXR4PUUEGQAAAbgAAAB+bG9jYRxaGtQAAAR8AAAAQm1heHAAZQBaAAABOAAAACBuYW1lh7oD8QAAC+wAAAJtcG9zdL4gM5sAAA5cAAABFQABAAAAAQAAwEm9zl8PPPUACwPoAAAAAOTAyM0AAAAA5MDY9AAAAAAC5wLXAAAACAACAAAAAAAAAAEAAALXAAAAWgMEAAAAAALnAAEAAAAAAAAAAAAAAAAAAAAfAAEAAAAgACkAAwAAAAAAAgAAAAEAAQAAAEAALgAAAAAABAITAZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAACMAgAAAAAAAAAAAAAAUGZFZACAAQD//wMg/zgAWgLXAAAAAAABAAAAAAAAAnUAAAAgAAEBbAAhAAAAAAFNAAACMwAPAfAANQIzABACMwAQAcgAOALDADUCMgA2AoAAHgEMAF4B3AA4AdoAPwMEAB0CTwA1AoAAHgHgADQBsgAmAd4ADAIwAAoCFAAYAjQAGwKGACIByAA4AjMADwEMAAACgAAeAhQAGAIUABgB4AA0ADQAAAAAAAUAAAADAAAALAAAAAQAAACcAAEAAAAAATwAAwABAAAALAADAAoAAACcAAQAcAAAABgAEAADAAgBAAESASoBTAFqAjIDcAOWA50DoQOn//8AAAEAARIBKgFMAWoCMgNwA5EDmAOfA6P///8Z/wb+8P7P/rL96/yZ/HL8cvxx/HAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAACgAAAAAAAAAAwAAAEAAAABAAAAABkAAAESAAABEgAAABgAAAEqAAABKgAAABoAAAFMAAABTAAAABsAAAFqAAABagAAABwAAAIyAAACMgAAAB0AAANwAAADcAAAAAkAAAORAAADlgAAAAMAAAOYAAADnQAAAAoAAAOfAAADoQAAABAAAAOjAAADpwAAABMAAQFEAAEBRQAAAB4AAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqACoAKgBCAH4AkgCoAL4A1gDsASwBOgFSAWQBfAGSAcQB1gH4AhACIgJAAnQCjgKqAsgC3AMUAzgDXAN6A5YAAAACACEAAAEqApoAAwAHAC6xAQAvPLIHBADtMrEGBdw8sgMCAO0yALEDAC88sgUEAO0ysgcGAfw8sgECAO0yMxEhESczESMhAQnox8cCmv1mIQJYAAACAA8ANAIkAnkABgAJAAAJASMnIwcjAQczARoBClY4+jhVAQtduQJ4/bx5eQGJyQADADUANAG7AnYAEQAbACgAABM7ATIWFRQGBx4BFRQOASsCExUzMj4CNTQjBxUzMj4CNTQuAiM1Tm1JYzAlMEQ3SyqMTk5tHCgSCH1ObSEvFQgIFS8hAnVOTSs7DQ5MPTNKHwHzoxEZFAhd8bQUIBwODhocEgAAAAABABAANAIjAnYABQAANwkBIwsBEAEKAQlWs7U0AkL9vgGH/nkAAAAAAgAQADQCIwJ3AAIABQAACQEhAQMlARoBCf3tAQqRASACdv2+AYj+xgEAAAABADgANAGQAnYACwAAEyEVIxUzFSMVIRUhOAFI+urqAQr+qAJ1TqNOtE4AAAEANQA0Ao4CdgALAAATIRUhESEVITUhESE2Alj++wEF/agBBP78AnZP/lxPTwGkAAEANgA0AfwCdgALAAATMxUhNTMRIzUhFSM3TgEoTk7+2E4Cdfn5/b/6+gAAAwAeADQCYgJ2ABIAHQAoAAABMhceAhQOAiIuAjQ+ATc2FyIHBhQWMjY0JyYHNDc2MhYUBiInJgFAPjE2UC0tUGxybFAtLVA2MT5WOzx4qng8O40QEC8hIS8QEAJ1FBZQbHJsUC0tUGxybFAWFEo+P7F+frE/PtQVEBAgLiAQEAABAF4ANACuAnUAAwAANyMTM61PAU80AkEAAAAAAQA4ADQB0QJ2AAoAABMzFTczCQEjJxUjOE7iZf7/AQVp4k4CdvDw/uf+1/39AAAAAQA/ADQBuAJ2AAYAABMzESUVBSNATwEo/thPAnb+DV5PXgAAAQAdADQC5wJ5AAkAACUjCwMjGwIC51V8lJd5VcafnTQBZf7AAUD+mwJE/pkBZwAAAQA1ADQCGQJ2AAcAABMBETMRAREjNgGTT/5tTwJ1/noBhv2/AYb+egAAAAACAB4ANAJiAnYAEgAdAAABMhceAhQOAiIuAjQ+ATc2FyIHBhQWMjY0JyYBQD4xNlAtLVBscmxQLS1QNjE+Vjs8eKp4PDsCdRQWUGxybFAtLVBscmxQFhRKPj+xfn6xPz4AAAAAAQA0ADQBrAJ2AAcAABMhESM1IxEjNQF2TtpOAnX+2dn+DQAAAgAmADQBngJ2AAsAFAAAEzsBMhYVFAYrARUjExUzMjY1NCYjJ05tTm1tPn1OTnYlPz8uAnV2RURn2wHzyjonJ0IAAAABAAwAMwGuAnYACQAAARUHFwcXFS0CAa7v7+/v/l4BBv76AnVOW3d4XE6eg4MAAAABAAoANAImAnYABwAANxEjNSEVIxHw5gIc5jUB805O/g0AAAABABgANAH8AnYADgAAEzMWFzY3MwYCBxUjNSYCGG1WLy9WbVFuDE4MbgJ1j4iIj2f+/2tubmsBAQAAAAADABsANAIZAnYAEQAXAB0AABMzFR4BFxYGBxUjNS4BNz4BNxUOAQcGFzcVNicuAfNOXHgCAXpdTl16AQJ4XEBGAgOLTooCAkYCdWcIWFFUZAloaAlkVFFYCEQEMDlxDuzsDnE5MAAAAAEAIgA0AmQCdgALAAATMxc3MwMTIycHIxMiZru7Zu7uZru7Zu4CdePj/uD+3+PjASEAAAACADgANAGQAtcAAwAPAAATIRUhByEVIxUzFSMVIRUhVgEM/vQeAUj66uoBCv6oAtc/I06jTrROAAMADwA0AiQC1wADAAoADQAAEyEVIRcBIycjByMBBzOSAQz+9IgBClY4+jhVAQtduQLXPyD9vHl5AYnJAAIAAAA0AQwC1wADAAcAABEhFSETIxMzAQz+9K1PAU8C1z/9nAJBAAADAB4ANAJiAtcAAwAWACEAABMhFSEXMhceAhQOAiIuAjQ+ATc2FyIHBhQWMjY0Jya6AQz+9IY+MTZQLS1QbHJsUC0tUDYxPlY7PHiqeDw7Atc/IxQWUGxybFAtLVBscmxQFhRKPj+xfn6xPz4AAAAAAgAYADQB/ALXAAMAEgAAEyEVIQczFhc2NzMGAgcVIzUmAoQBDP70bG1WLy9WbVFuDE4MbgLXPyOPiIiPZ/7/a25uawEBAAAAAgAYADQB/ALXAAMAEgAAEyEVIQczFhc2NzMGAgcVIzUmAoQBDP70bG1WLy9WbVFuDE4MbgLXPyOPiIiPZ/7/a25uawEBAAAAAwA0ADQBrAJ2AAIABQANAAATFyM3BzcDIREjNSMRI/du220sWe8Bdk7aTgIZ8I9mAQEi/tnZ/g0AAAAAAgA0ADQBrAJ2AAsADwAAEyERIzUjFSM1IxEjExUzNTUBdjVeNWBO414Cdf7YU1Pa/g0B81NTAAAAAA4ArgABAAAAAAAAABoANgABAAAAAAABAA8AcQABAAAAAAACAAcAkQABAAAAAAADACsA8QABAAAAAAAEAA8BPQABAAAAAAAFAA8BbQABAAAAAAAGABUBqQADAAEECQAAADQAAAADAAEECQABAB4AUQADAAEECQACAA4AgQADAAEECQADAFYAmQADAAEECQAEAB4BHQADAAEECQAFAB4BTQADAAEECQAGACoBfQBDAG8AcAB5AHIAaQBnAGgAdAAgACgAYwApACAAMgAwADIANQAsACAATAB1AG4AYQBjAHkAAENvcHlyaWdodCAoYykgMjAyNSwgTHVuYWN5AABPAGwAZAAgAEEAdAB0AGkAYwAgAEcAcgBlAGUAawAAT2xkIEF0dGljIEdyZWVrAABSAGUAZwB1AGwAYQByAABSZWd1bGFyAABGAG8AbgB0AEYAbwByAGcAZQAgADIALgAwACAAOgAgAE8AbABkACAAQQB0AHQAaQBjACAARwByAGUAZQBrACAAOgAgADEAMgAtADgALQAyADAAMgA1AABGb250Rm9yZ2UgMi4wIDogT2xkIEF0dGljIEdyZWVrIDogMTItOC0yMDI1AABPAGwAZAAgAEEAdAB0AGkAYwAgAEcAcgBlAGUAawAAT2xkIEF0dGljIEdyZWVrAABWAGUAcgBzAGkAbwBuACAAMAAwADEALgAwADAAMAAAVmVyc2lvbiAwMDEuMDAwAABPAGwAZABBAHQAdABpAGMARwByAGUAZQBrAC0AUgBlAGcAdQBsAGEAcgAAT2xkQXR0aWNHcmVlay1SZWd1bGFyAAAAAAACAAAAAAAA/7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAABAAIBAgEDAQQBBQEGAQcBCAEJAQoBCwEMAQ0BDgEPARABEQESARMBFAEVARYBFwEYARkBGgEbARwBHQEeBUFscGhhBEJldGEFR2FtbWEHdW5pMDM5NAdFcHNpbG9uBFpldGEHdW5pMDM3MAVUaGV0YQRJb3RhBUthcHBhBkxhbWJkYQJNdQJOdQdPbWljcm9uAlBpA1JobwVTaWdtYQNUYXUHVXBzaWxvbgNQaGkDQ2hpB0VtYWNyb24HQW1hY3JvbgdJbWFjcm9uB09tYWNyb24HVW1hY3Jvbgd1bmkwMjMyBnUxMDE0NAZ1MTAxNDUAAAAAAAAB//8AAgAAAAEAAAAA39bLMQAAAADkwMjNAAAAAOTAzsQAAQAAAAAAAAAMABQABAAAAAIAAAABAAAAAQAA
) format('woff2');

        font-weight: normal;
        font-style: normal;
      }
      .archaic-greek-text {
        font-family: 'ArchaicGreekFont', sans-serif !important;
      }
    `);


  // ====== EDITABLE-FIELD GUARD ======
  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
    'ῦ':'Ȳ','ὖ':'Ȳ','ὗ':'ͰȲ','Ὗ':'ͰȲ'
  };

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

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

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

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

  // Lexical overrides
  // Keep whole-word OU: οὖν, οὐ, οὖς, βοῦς, τοῦτο
  const KEEP_TRUE_OU_WORDS = new Set(['ουν', 'ου', 'ους', 'βους', 'τουτο']);
  // Collapse ALL OU in lexeme (e.g., βουλή-forms, δοῦναι)
  const COLLAPSE_WHOLE_WORDS = new Set(['βουλη','βουλης','βουλην','δουναι']);

  // OU endings to collapse (ending-only)
  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 for οὖν
    { 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 }
  ];

  // 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('');
  }

  // Collapse the LAST ending-OU ONLY (indexing 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 ? 'Ō' : 'Ο';
      // Replace *that* OU (two chars) with O/Ō; any rough placeholder ͱ remains before it
      return coreWord.slice(0, start) + replacement + coreWord.slice(start + 2);
    }
    return null;
  }

  // ἐν assimilation before β / π (standalone; whitespace only gap; 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;
        // Skip whitespace only; if punctuation intervenes, do nothing
        while (j < out.length && isWhitespace(out[j])) j++;
        if (j < out.length && !isPunct(out[j]) && startsWithBetaOrPi(out[j])) {
          out[i] = 'ἐμ';
        }
      }
    }
    return out;
  }

  // Per-word OU processing with lexeme/ending 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);

    // If no OU at all (by explicit set), skip
    if (!OU_ANY_RE_ONE.test(core)) {
      return core + trailing;
    }

    const norm = normalizeForLookup(core);

    // 1) Keep whole word’s OU (οὖν, οὐ, οὖς, βοῦς, τοῦτο)
    if (KEEP_TRUE_OU_WORDS.has(norm)) {
      return core + trailing;
    }

    // 2) Collapse ALL OU for lexical spurious items (βουλή*, δοῦναι)
    if (COLLAPSE_WHOLE_WORDS.has(norm)) {
      const replacement = showMacrons ? 'Ō' : 'Ο';
      const replaced = core.replace(OU_ANY_RE, replacement);
      return replaced + trailing;
    }

    // 3) Try spurious ending collapse (only the ending’s OU)
    const collapsed = collapseEndingOU(core, showMacrons);
    if (collapsed !== null) return collapsed + trailing;

    // 4) Default: leave true OU; mapping will uppercase to ΟΥ
    return core + trailing;
  }

  // ======================= 4) CORE CONVERTER =======================
  // Extra diphthong handling carried over from your prior script:
  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 morphology (reposition + keeps + endings + lexeme collapses)
    const prepped = processWordForArchaicOU(normalizedInput, 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 =======================
  // Use this to *detect* nodes to process (Greek or macron letters present)
  const ALL_GREEK_CHARS_REGEX = /([\u0100\u0112\u012a\u014c\u016a\u0232\u0370-\u03FF\u1F00-\u1FFF]+)/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);
      // Apply ἐν→ἐμ assimilation across whitespace (no punctuation crossing)
      const tokens = applyEnAssimilation(parts);

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

        // If 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
  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 BUTTON =======================
  (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
  });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址