From a43f9e47fd702dd1dbf962ef32e305df39f9616e Mon Sep 17 00:00:00 2001 From: johnlockejrr Date: Thu, 24 Sep 2026 11:25:36 +0300 Subject: [PATCH 1/2] =?UTF-8?q?Shift=20primary=20stress=20off=20furtive=20?= =?UTF-8?q?pata=E1=B8=A5=20syllables.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Postpositive accents (e.g. lone WLC Segolta) and the milraʿ fallback land on the final letter that hosts only epenthetic furtive pataḥ; move stress one syllable left so forms like הָרָקִיעַ֒ / רוּחַ stress the preceding vowel. Co-authored-by: Cursor --- src/utils/syllabifier.ts | 49 ++++++++++++++++++++++++++++++++ test/syllable.isAccented.test.ts | 30 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/utils/syllabifier.ts b/src/utils/syllabifier.ts index db28fb3..f8554c6 100644 --- a/src/utils/syllabifier.ts +++ b/src/utils/syllabifier.ts @@ -560,6 +560,52 @@ const setIsAccented = (syllable: Syllable) => { syllable.isAccented = isAccented; }; +/** + * Strip taamim / meteg / sof pasuq so furtive-pataḥ detection is orthography-based. + * Postpositive accents (e.g. lone Segolta in WLC) sit on the final letter without + * implying that the furtive host is stress-bearing. + */ +const TAAMIM_METEG_SOF = /[\u{0591}-\u{05AF}\u{05BD}\u{05C3}]/gu; +const FURTIVE_END = /(?:\u{05D7}|\u{05E2}|\u{05D4}\u{05BC})\u{05B7}$/u; + +const plainHebrewForFurtive = (text: string): string => text.replace(TAAMIM_METEG_SOF, ""); + +/** + * True if this syllable is the epenthetic furtive-pataḥ host (never stressed). + * Matches the same final ח/ע/הּ + pataḥ pattern used by {@link Syllable.structure}. + * + * Uses array position instead of {@link Syllable.isFinal}, which requires the + * syllable's parent Word (not yet set while {@link syllabify} is running). + */ +const isFurtiveSyllable = (syllable: Syllable, isWordFinal: boolean): boolean => { + if (!isWordFinal || syllable.isClosed) { + return false; + } + return FURTIVE_END.test(plainHebrewForFurtive(syllable.text)); +}; + +/** + * Move primary stress off a furtive-pataḥ syllable onto the preceding one. + * + * Postpositive accents (lone Segolta, etc.) and the milraʿ fallback both land + * on the final letter; for רָקִיעַ / רוּחַ that letter hosts only furtive pataḥ, + * which is never stress-bearing (cf. same lemmas with zaqef/atnaḥ on קִ / וּ). + */ +const shiftAccentOffFurtive = (syllables: Syllable[]): void => { + const last = syllables.length - 1; + for (let i = 0; i < syllables.length; i++) { + const syl = syllables[i]; + if (!syl.isAccented || !isFurtiveSyllable(syl, i === last)) { + continue; + } + syl.isAccented = false; + if (i > 0) { + syllables[i - 1].isAccented = true; + } + return; + } +}; + /** * * @remarks a step to get a Cluster's original position before filtering out latin @@ -620,6 +666,9 @@ export const syllabify = (clusters: Cluster[], options: SylOpts, isWordInConstru syllables[syllables.length - 1].isAccented = true; } + // After taam rules + milraʿ fallback: furtive pataḥ never bears stress. + shiftAccentOffFurtive(syllables); + // for each cluster, set its syllable syllables.forEach((s) => s.clusters.forEach((c) => (c.parent = s))); return latinClusters.length ? reinsertLatin(syllables, latinClusters) : syllables; diff --git a/test/syllable.isAccented.test.ts b/test/syllable.isAccented.test.ts index 5e8e71d..70fb0e2 100644 --- a/test/syllable.isAccented.test.ts +++ b/test/syllable.isAccented.test.ts @@ -255,4 +255,34 @@ describe("Test if a syllable is accented", () => { }); }); }); + + describe("Furtive pataḥ never bears primary stress", () => { + // Epenthetic furtive pataḥ on final ח/ע/הּ is not a stress position. + // Postpositive accents and the milraʿ fallback often land on that letter + // (especially lone Segolta in WLC); stress belongs on the preceding syllable. + + test("WLC lone Segolta on furtive (הָרָקִיעַ֒)", () => { + testIsAccented("הָרָקִיעַ֒", [false, false, true, false]); + }); + + test("MAM doubled Segolta still stresses pre-furtive", () => { + testIsAccented("הָרָקִ֒יעַ֒", [false, false, true, false]); + }); + + test("zaqef on קִ is unchanged", () => { + testIsAccented("לָרָקִ֔יעַ", [false, false, true, false]); + }); + + test("atnah on קִ is unchanged", () => { + testIsAccented("לָרָקִ֑יעַ", [false, false, true, false]); + }); + + test("unaccented רוּחַ stresses shureq, not furtive", () => { + testIsAccented("רוּחַ", [true, false]); + }); + + test("unaccented מַדּוּעַ stresses long vowel, not furtive", () => { + testIsAccented("מַדּוּעַ", [false, true, false]); + }); + }); }); From aa9e3c188b4542c033d19ec00ebd21c34f0692c5 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Sat, 26 Sep 2026 22:47:00 -0400 Subject: [PATCH 2/2] refactor furtive patach accent logic --- src/utils/syllabifier.ts | 58 +++++++--------------------------------- 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/src/utils/syllabifier.ts b/src/utils/syllabifier.ts index f8554c6..f4a2498 100644 --- a/src/utils/syllabifier.ts +++ b/src/utils/syllabifier.ts @@ -392,6 +392,15 @@ const setIsClosed = (syllable: Syllable, index: number, arr: Syllable[]) => { }; const setIsAccented = (syllable: Syllable) => { + // if the syllable is final and has a furtive pataḥ, it should not be accented + if (!syllable.next && /(?:\u{05D7}|\u{05E2}|\u{05D4}\u{05BC}).?\u{05B7}/u.test(syllable.text)) { + syllable.isAccented = false; + if (syllable.prev?.value) { + syllable.prev.value.isAccented = true; + return; + } + } + if (syllable.isAccented) { return; } @@ -560,52 +569,6 @@ const setIsAccented = (syllable: Syllable) => { syllable.isAccented = isAccented; }; -/** - * Strip taamim / meteg / sof pasuq so furtive-pataḥ detection is orthography-based. - * Postpositive accents (e.g. lone Segolta in WLC) sit on the final letter without - * implying that the furtive host is stress-bearing. - */ -const TAAMIM_METEG_SOF = /[\u{0591}-\u{05AF}\u{05BD}\u{05C3}]/gu; -const FURTIVE_END = /(?:\u{05D7}|\u{05E2}|\u{05D4}\u{05BC})\u{05B7}$/u; - -const plainHebrewForFurtive = (text: string): string => text.replace(TAAMIM_METEG_SOF, ""); - -/** - * True if this syllable is the epenthetic furtive-pataḥ host (never stressed). - * Matches the same final ח/ע/הּ + pataḥ pattern used by {@link Syllable.structure}. - * - * Uses array position instead of {@link Syllable.isFinal}, which requires the - * syllable's parent Word (not yet set while {@link syllabify} is running). - */ -const isFurtiveSyllable = (syllable: Syllable, isWordFinal: boolean): boolean => { - if (!isWordFinal || syllable.isClosed) { - return false; - } - return FURTIVE_END.test(plainHebrewForFurtive(syllable.text)); -}; - -/** - * Move primary stress off a furtive-pataḥ syllable onto the preceding one. - * - * Postpositive accents (lone Segolta, etc.) and the milraʿ fallback both land - * on the final letter; for רָקִיעַ / רוּחַ that letter hosts only furtive pataḥ, - * which is never stress-bearing (cf. same lemmas with zaqef/atnaḥ on קִ / וּ). - */ -const shiftAccentOffFurtive = (syllables: Syllable[]): void => { - const last = syllables.length - 1; - for (let i = 0; i < syllables.length; i++) { - const syl = syllables[i]; - if (!syl.isAccented || !isFurtiveSyllable(syl, i === last)) { - continue; - } - syl.isAccented = false; - if (i > 0) { - syllables[i - 1].isAccented = true; - } - return; - } -}; - /** * * @remarks a step to get a Cluster's original position before filtering out latin @@ -666,9 +629,6 @@ export const syllabify = (clusters: Cluster[], options: SylOpts, isWordInConstru syllables[syllables.length - 1].isAccented = true; } - // After taam rules + milraʿ fallback: furtive pataḥ never bears stress. - shiftAccentOffFurtive(syllables); - // for each cluster, set its syllable syllables.forEach((s) => s.clusters.forEach((c) => (c.parent = s))); return latinClusters.length ? reinsertLatin(syllables, latinClusters) : syllables;