UNPKG

hebrew-transliteration

Version:
426 lines 17.3 kB
import { Cluster } from "havarotjs/cluster"; import { Syllable } from "havarotjs/syllable"; import { clusterSplitGroup, hebChars } from "havarotjs/utils/regularExpressions"; import { Word } from "havarotjs/word"; import { transliterateMap as map } from "./hebCharsTrans.js"; 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; } if (location === "before-syllable") { const isDoubled = syl.clusters.map((c) => isDageshChazaq(c, schema)).includes(true); if (isDoubled) { const [first, ...rest] = text.split(""); return `${first}${mark}${rest.join("")}`; } 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 && prevCluster.hasSheva) { return false; } const prevWord = cluster.syllable?.word?.prev?.value; if (prevWord && 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) => { return [...input].map((char) => (char in map ? 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