UNPKG

hebrew-transliteration

Version:
488 lines 20.5 kB
import { Cluster } from "havarotjs/cluster"; import { Syllable } from "havarotjs/syllable"; import { clusterSplitGroup, hebChars } from "havarotjs/utils/regularExpressions"; import { Word } from "havarotjs/word"; const taamim = /[\u{0591}-\u{05AF}\u{05BD}\u{05BF}]/gu; /** * Adds a stress marker to a syllable according to the schema settings * * @param text the transliterated text * @param syl the current syllable * @param schema the schema */ const addStressMarker = (text, syl, schema) => { if (!schema.STRESS_MARKER || !text) { return text; } if (!syl.isAccented) { return text; } const exclude = schema.STRESS_MARKER?.exclude ?? "never"; if (exclude === "single" && !syl.prev && !syl.next) { return text; } if (exclude === "final" && !syl.next) { return text; } const location = schema.STRESS_MARKER.location; const mark = schema.STRESS_MARKER.mark; // if the stress marker is already present, no need to add it again if (text.includes(mark)) { return text; } function isSchemaKey(key) { return key in schema; } if (location === "before-syllable") { const isDoubled = syl.clusters.map((c) => isDageshChazaq(c, schema)).includes(true); if (isDoubled) { const firstCluster = syl.clusters[0]; const name = firstCluster.chars[0].characterName; const output = name && isSchemaKey(name) ? schema[name] : ""; const first = text.slice(0, output.length); const rest = text.slice(output.length); return `${first}${mark}${rest}`; } return `${mark}${text}`; } if (location === "after-syllable") { return `${text}${mark}`; } const vowels = [ schema.PATAH, schema.HATAF_PATAH, schema.QAMATS, schema.HATAF_QAMATS, schema.SEGOL, schema.HATAF_SEGOL, schema.TSERE, schema.HIRIQ, schema.HOLAM, schema.QAMATS_QATAN, schema.QUBUTS, schema.QAMATS_HE, schema.SEGOL_HE, schema.TSERE_HE, schema.HIRIQ_YOD, schema.TSERE_YOD, schema.SEGOL_YOD, schema.HOLAM_VAV, schema.SHUREQ ] .filter((v) => typeof v === "string") .sort((a, b) => b.length - a.length); const vowelRgx = new RegExp(`${vowels.join("|")}`); const match = text.match(vowelRgx); if (location === "before-vowel") { return match?.length ? text.replace(match[0], `${mark}${match[0]}`) : text; } // after-vowel return match?.length ? text.replace(match[0], `${match[0]}${mark}`) : text; }; const copySyllable = (newText, old) => { const newClusters = newText.split(clusterSplitGroup).map((clusterString) => new Cluster(clusterString, true)); const oldClusters = old.clusters; // set prev and next based on old syllable if (newClusters.length === oldClusters.length) { newClusters.forEach((c, i) => ((c.prev = oldClusters[i]?.prev ?? null), (c.next = oldClusters[i]?.next ?? null))); } else { for (let i = 0; i < newClusters.length; i++) { const c = newClusters[i]; if (oldClusters[i]?.text[0] === c?.text[0]) { c.prev = oldClusters[i]?.prev ?? null; c.next = oldClusters[i]?.next ?? null; } else { c.prev = oldClusters[i]?.prev ?? null; c.next = oldClusters[i + 1]?.next ?? null; i++; } } } const newSyl = new Syllable(newClusters, { isClosed: old.isClosed, isAccented: old.isAccented, isFinal: old.isFinal }); newClusters.forEach((c) => (c.syllable = newSyl)); newSyl.prev = old.prev; newSyl.next = old.next; newSyl.word = old.word; return newSyl; }; const getDageshChazaqVal = (input, schema, isChazaq) => { if (!isChazaq) { return input; } const dagesh = schema["DAGESH_CHAZAQ"]; const syllableSeparator = schema["SYLLABLE_SEPARATOR"] ?? ""; if (typeof dagesh === "boolean") { return input + syllableSeparator + input; } return input + dagesh; }; /** * Formats the Divine Name with any Latin chars * * @param str word text * @param schema * @returns the Divine Name with any pre or proceding Latin chars */ const getDivineName = (str, schema) => { const begn = str[0]; const end = str[str.length - 1]; // if DN is pointed with a hiriq, then it is read as 'elohim const divineName = schema.DIVINE_NAME_ELOHIM && /\u{05B4}/u.test(str) ? schema.DIVINE_NAME_ELOHIM : schema.DIVINE_NAME; return `${hebChars.test(begn) ? "" : begn}${divineName}${hebChars.test(end) ? "" : end}`; }; const isDageshChazaq = (cluster, schema) => { // if there is no dagesh chazaq in the schema, then return false if (!schema.DAGESH_CHAZAQ) { return false; } // a shureq could potentially match because of dagesh if (cluster.isShureq) { return false; } // if there is no dagesh in the text, then return false if (!/\u{05BC}/u.test(cluster.text)) { return false; } // if the previous cluster has a sheva, then it is not a dagesh chazaq // likely being a Qal 2fs suffix (e.g. קָטַלְתְּ) const prevCluster = cluster.prev?.value; if (prevCluster?.hasSheva) { return false; } const prevWord = cluster.syllable?.word?.prev?.value; if (prevWord?.isInConstruct && !prevWord.syllables[prevWord.syllables.length - 1].isClosed) { return true; } // this could be a code smell, b/c the copySyllable function results are not the most predictable const prevSyllable = cluster.syllable?.prev; if (!prevSyllable) { return false; } const prevCoda = prevSyllable.value?.codaWithGemination; if (!prevCoda) { return false; } return prevCoda === cluster.syllable?.onset; }; const joinSyllableChars = (syl, sylChars, schema) => { const isInConstruct = syl.word?.isInConstruct; if (isInConstruct) { return sylChars.map(mapChars(schema)).join(""); } if (!syl.isAccented) { return sylChars.map(mapChars(schema)).join(""); } // if syllable is only punctuation (e.g. a paseq), it should not receive a stress marker const isOnlyPunctuation = syl.clusters.map((c) => c.isPunctuation).every((c) => c); if (isOnlyPunctuation) { return sylChars.map(mapChars(schema)).join(""); } return sylChars.map(mapChars(schema)).join(""); }; /** * Maps Hebrew characters to schema * * @param input - text to be transliterated * @param schema - a {@link Schema} for transliterating the input * @returns transliteration of characters * */ const mapChars = (schema) => (input) => { const map = { // niqqud "\u{05B0}": "VOCAL_SHEVA", // HEBREW POINT SHEVA (U+05B0) "\u{05B1}": "HATAF_SEGOL", // HEBREW POINT HATAF SEGOL (U+05B1) "\u{05B2}": "HATAF_PATAH", // HEBREW POINT HATAF PATAH (U+05B2) "\u{05B3}": "HATAF_QAMATS", // HEBREW POINT HATAF QAMATS (U+05B3) "\u{05B4}": "HIRIQ", // HEBREW POINT HIRIQ (U+05B4) "\u{05B5}": "TSERE", // HEBREW POINT TSERE (U+05B5) "\u{05B6}": "SEGOL", // HEBREW POINT SEGOL (U+05B6) "\u{05B7}": "PATAH", // HEBREW POINT PATAH (U+05B7) "\u{05B8}": "QAMATS", // HEBREW POINT QAMATS (U+05B8) "\u{05B9}": "HOLAM", // HEBREW POINT HOLAM (U+05B9) "\u{05BA}": "HOLAM", // HEBREW POINT HOLAM HASER FOR VAV (U+05BA) "\u{05BB}": "QUBUTS", // HEBREW POINT QUBUTS (U+05BB) "\u{05BC}": "DAGESH", // HEBREW POINT DAGESH OR MAPIQ (U+05BC) // "\u{05BD}": "", // HEBREW POINT METEG (U+05BD) "\u{05BE}": "MAQAF", // HEBREW PUNCTUATION MAQAF (U+05BE) "\u{05C0}": "PASEQ", // HEBREW PUNCTUATION PASEQ (U+05C0) "\u{05C3}": "SOF_PASUQ", // HEBREW PUNCTUATION SOF PASUQ (U+05C3) "\u{05C7}": "QAMATS_QATAN", // HEBREW POINT QAMATS QATAN (U+05C7) // consonants א: "ALEF", // HEBREW LETTER ALEF (U+05D0) ב: "BET", // HEBREW LETTER BET (U+05D1) ג: "GIMEL", // HEBREW LETTER GIMEL (U+05D2) ד: "DALET", // HEBREW LETTER DALET (U+05D3) ה: "HE", // HEBREW LETTER HE (U+05D4) ו: "VAV", // HEBREW LETTER VAV (U+05D5) ז: "ZAYIN", // HEBREW LETTER ZAYIN (U+05D6) ח: "HET", // HEBREW LETTER HET (U+05D7) ט: "TET", // HEBREW LETTER TET (U+05D8) י: "YOD", // HEBREW LETTER YOD (U+05D9) ך: "FINAL_KAF", // HEBREW LETTER FINAL KAF (U+05DA) כ: "KAF", // HEBREW LETTER KAF (U+05DB) ל: "LAMED", // HEBREW LETTER LAMED (U+05DC) ם: "FINAL_MEM", // HEBREW LETTER FINAL MEM (U+05DD) מ: "MEM", // HEBREW LETTER MEM (U+05DE) ן: "FINAL_NUN", // HEBREW LETTER FINAL NUN (U+05DF) נ: "NUN", // HEBREW LETTER NUN (U+05E0) ס: "SAMEKH", // HEBREW LETTER SAMEKH (U+05E1) ע: "AYIN", // HEBREW LETTER AYIN (U+05E2) ף: "FINAL_PE", // HEBREW LETTER FINAL PE (U+05E3) פ: "PE", // HEBREW LETTER PE (U+05E4) ץ: "FINAL_TSADI", // HEBREW LETTER FINAL TSADI (U+05E5) צ: "TSADI", // HEBREW LETTER TSADI (U+05E6) ק: "QOF", // HEBREW LETTER QOF (U+05E7) ר: "RESH", // HEBREW LETTER RESH (U+05E8) ש: "SHIN", // HEBREW LETTER SHIN (U+05E9) ת: "TAV" // HEBREW LETTER TAV (U+05EA) // "\u{05EF}": "", // HEBREW YOD TRIANGLE (U+05EF) // װ: "", // HEBREW LIGATURE YIDDISH DOUBLE VAV (U+05F0) // ױ: "", // HEBREW LIGATURE YIDDISH VAV YOD (U+05F1) // ײ: "" // HEBREW LIGATURE YIDDISH DOUBLE YOD (U+05F2) }; function isMapKey(key) { return key in map; } return [...input].map((char) => (isMapKey(char) ? schema[map[char]] : char)).join(""); }; const materFeatures = (syl, schema) => { const mater = syl.clusters.filter((c) => c.isMater)[0]; const prev = mater.prev instanceof Cluster ? mater.prev : null; const materText = mater.text; const prevText = (prev?.text || "").replace(taamim, ""); // string comprised of all non-mater clusters in a syl with a mater let noMaterText = syl.clusters .filter((c) => !c.isMater) .map((c) => cluterRules(c, schema)) .join(""); // workaround for maqaf const hasMaqaf = mater.text.includes("־"); noMaterText = hasMaqaf ? noMaterText.concat("־") : noMaterText; if (/י/.test(materText)) { // hiriq if (/\u{05B4}/u.test(prevText)) { return replaceWithRegex(noMaterText, /\u{05B4}/u, schema.HIRIQ_YOD); } // tsere if (/\u{05B5}/u.test(prevText)) { return replaceWithRegex(noMaterText, /\u{05B5}/u, schema.TSERE_YOD); } // segol if (/\u{05B6}/u.test(prevText)) { return replaceWithRegex(noMaterText, /\u{05B6}/u, schema.SEGOL_YOD); } } if (/ו/u.test(materText)) { // holam if (/\u{05B9}/u.test(prevText)) { return replaceWithRegex(noMaterText, /\u{05B9}/u, schema.HOLAM_VAV); } } if (/ה/.test(materText)) { // qamets if (/\u{05B8}/u.test(prevText)) { return replaceWithRegex(noMaterText, /\u{05B8}/u, schema.QAMATS_HE); } } return materText; }; const removeTaamim = (input) => input.replace(taamim, ""); /** * Replaces part of a string and transliterates the remaining characters according to the schema * * @example * ```ts * // replaces בָ as VA, but only when matching the regex sequence * const betAndQamats = /\u{05D1}\u{05B8}/u * replaceAndTransliterate("דָּבָר", betAndQamats, "VA", schema); * // dāVAr * ``` * * @param input the text to be transliterated * @param regex the regex used as the search value * @param replaceValue the string to replace the regex * @param schema the Schema */ const replaceAndTransliterate = (input, regex, replaceValue, schema) => { const sylSeq = replaceWithRegex(input, regex, replaceValue); return [...sylSeq].map(mapChars(schema)).join(""); }; /** * Wrapper around `String.replace()` to constrain to a RegExp * * @param input the string to be modified * @param regex the regex to be used as the search value * @param replaceValue the string to replace the regex */ const replaceWithRegex = (input, regex, replaceValue) => input.replace(regex, replaceValue); // MAIN RULES const cluterRules = (cluster, schema) => { let clusterText = removeTaamim(cluster.text); const clusterFeatures = schema.ADDITIONAL_FEATURES?.filter((seq) => seq.FEATURE === "cluster"); if (clusterFeatures) { for (const feature of clusterFeatures) { const heb = new RegExp(feature.HEBREW, "u"); if (feature.FEATURE === "cluster" && heb.test(clusterText)) { const transliteration = feature.TRANSLITERATION; const passThrough = feature.PASS_THROUGH ?? true; if (typeof transliteration === "string") { return replaceAndTransliterate(clusterText, heb, transliteration, schema); } if (!passThrough) { return transliteration(cluster, feature.HEBREW, schema); } clusterText = transliteration(cluster, feature.HEBREW, schema); } } } const syl = cluster.syllable; clusterText = cluster.hasSheva && syl?.isClosed ? clusterText.replace(/\u{05B0}/u, "") : clusterText; // mappiq he if (/ה\u{05BC}$/mu.test(clusterText)) { return replaceWithRegex(clusterText, /ה\u{05BC}/u, schema.HE); } if (syl?.isFinal && !syl?.isClosed) { const furtiveChet = /\u{05D7}\u{05B7}$/mu; if (furtiveChet.test(clusterText)) { return replaceWithRegex(clusterText, furtiveChet, "\u{05B7}\u{05D7}"); } const furtiveAyin = /\u{05E2}\u{05B7}$/mu; if (furtiveAyin.test(clusterText)) { return replaceWithRegex(clusterText, furtiveAyin, "\u{05B7}\u{05E2}"); } const furtiveHe = /\u{05D4}\u{05BC}\u{05B7}$/mu; if (furtiveHe.test(clusterText)) { return replaceWithRegex(clusterText, furtiveHe, "\u{05B7}\u{05D4}\u{05BC}"); } } // dagesh chazaq const isDageshChazq = isDageshChazaq(cluster, schema); if (schema.BET_DAGESH && /ב\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ב\u{05BC}/u, getDageshChazaqVal(schema.BET_DAGESH, schema, isDageshChazq)); } if (schema.GIMEL_DAGESH && /ג\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ג\u{05BC}/u, getDageshChazaqVal(schema.GIMEL_DAGESH, schema, isDageshChazq)); } if (schema.DALET_DAGESH && /ד\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ד\u{05BC}/u, getDageshChazaqVal(schema.DALET_DAGESH, schema, isDageshChazq)); } if (schema.KAF_DAGESH && /כ\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /כ\u{05BC}/u, getDageshChazaqVal(schema.KAF_DAGESH, schema, isDageshChazq)); } if (schema.KAF_DAGESH && /ך\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ך\u{05BC}/u, getDageshChazaqVal(schema.KAF_DAGESH, schema, isDageshChazq)); } if (schema.PE_DAGESH && /פ\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /פ\u{05BC}/u, getDageshChazaqVal(schema.PE_DAGESH, schema, isDageshChazq)); } if (schema.TAV_DAGESH && /ת\u{05BC}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ת\u{05BC}/u, getDageshChazaqVal(schema.TAV_DAGESH, schema, isDageshChazq)); } if (/ש\u{05C1}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ש\u{05C1}/u, getDageshChazaqVal(schema.SHIN, schema, isDageshChazq)); } if (/ש\u{05C2}/u.test(clusterText)) { return replaceWithRegex(clusterText, /ש\u{05C2}/u, getDageshChazaqVal(schema.SIN, schema, isDageshChazq)); } if (isDageshChazq) { const consonant = cluster.chars[0].text; const consonantDagesh = new RegExp(consonant + "\u{05BC}", "u"); return replaceWithRegex(clusterText, consonantDagesh, getDageshChazaqVal(consonant, schema, isDageshChazq)); } if (cluster.isShureq) { return clusterText.replace("וּ", schema.SHUREQ); } return clusterText; }; export const sylRules = (syl, schema) => { /** The syllable's original text with taamim removed */ const baseSyllableText = syl.text.replace(taamim, ""); const syllableFeatures = schema.ADDITIONAL_FEATURES?.filter((seq) => seq.FEATURE === "syllable"); if (syllableFeatures) { for (const feature of syllableFeatures) { const heb = new RegExp(feature.HEBREW, "u"); if (feature.FEATURE === "syllable" && heb.test(baseSyllableText)) { const transliteration = feature.TRANSLITERATION; const passThrough = feature.PASS_THROUGH ?? true; if (typeof transliteration === "string") { return replaceAndTransliterate(baseSyllableText, heb, transliteration, schema); } if (!passThrough) { return transliteration(syl, feature.HEBREW, schema); } const newText = transliteration(syl, feature.HEBREW, schema); // if the transliteration just returns the syllable.text, then no need to copy the syllable if (newText !== baseSyllableText) { syl = copySyllable(newText, syl); } } } } const hasMater = syl.clusters.map((c) => c.isMater).includes(true); if (hasMater) { const materSyl = materFeatures(syl, schema); const text = joinSyllableChars(syl, [...materSyl], schema); return addStressMarker(text, syl, schema); } const clusters = syl.clusters.map((cluster) => cluterRules(cluster, schema)); const joined = joinSyllableChars(syl, clusters, schema).replace(taamim, ""); // syllable is 3ms sufx const mSSuffix = /\u{05B8}\u{05D9}\u{05D5}/u; if (syl.isFinal && mSSuffix.test(baseSyllableText)) { const text = joined.replace(schema["QAMATS"] + schema["YOD"] + schema["VAV"], schema.MS_SUFX); return addStressMarker(text, syl, schema); } if (schema.SEGOL_HE && /\u{05B6}\u{05D4}/u.test(baseSyllableText)) { const text = joined.replace(schema["SEGOL"] + schema["HE"], schema.SEGOL_HE); return addStressMarker(text, syl, schema); } if (schema.TSERE_HE && /\u{05B5}\u{05D4}/u.test(baseSyllableText)) { const text = joined.replace(schema["TSERE"] + schema["HE"], schema.TSERE_HE); return addStressMarker(text, syl, schema); } if (schema.PATAH_HE && /\u{05B7}\u{05D4}/u.test(baseSyllableText)) { const text = joined.replace(schema["PATAH"] + schema["HE"], schema.PATAH_HE); return addStressMarker(text, syl, schema); } const text = joined.replace(taamim, ""); return addStressMarker(text, syl, schema); }; export const wordRules = (word, schema) => { if (word.isDivineName) { return getDivineName(word.text, schema); } if (word.hasDivineName) { return `${sylRules(word.syllables[0], schema)}-${getDivineName(word.text, schema)}`; } if (word.isNotHebrew) { return word.text; } const wordFeatures = schema.ADDITIONAL_FEATURES?.filter((seq) => seq.FEATURE === "word"); if (wordFeatures) { const text = word.text.replace(taamim, ""); for (const feature of wordFeatures) { const heb = new RegExp(feature.HEBREW, "u"); if (heb.test(text)) { const transliteration = feature.TRANSLITERATION; const passThrough = feature.PASS_THROUGH ?? true; if (typeof transliteration === "string") { return replaceAndTransliterate(text, heb, transliteration, schema); } if (!passThrough) { return transliteration(word, feature.HEBREW, schema); } return new Word(transliteration(word, feature.HEBREW, schema), schema); } } return word; } return word; }; //# sourceMappingURL=rules.js.map