Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ba4e90c
feat(core): add bidirectional MultiFlowAnchorMap and Dibur HaMatchil …
Amlaach Oct 31, 2026
48efa9d
feat(typeset): implement commentary lead prevention and synchronized …
Amlaach Oct 31, 2026
7efbd50
feat(cli): wire multi-flow anchor definitions into CLI typesetting ex…
Amlaach Oct 31, 2026
a12d6de
feat(bridge): implement bidirectional serialization of MultiFlowAncho…
Amlaach Oct 31, 2026
6789d40
test(typeset): add comprehensive Living Tzurat HaDaf integration test…
Amlaach Oct 31, 2026
7aad3f9
docs(changelog): document v0.31.0 Living Tzurat HaDaf release with sy…
Amlaach Oct 31, 2026
49b4c93
test(fixtures): add multi-flow anchor fixture for Berakhot 2a
Amlaach Oct 31, 2026
bf2818a
test(fixtures): add multi-flow anchor fixture for Shabbat 31a
Amlaach Oct 31, 2026
0852e0d
test(fixtures): add multi-flow anchor fixture for Pesachim 99b
Amlaach Oct 31, 2026
02e0873
test(fixtures): add multi-flow anchor fixture for Yoma 2a
Amlaach Oct 31, 2026
e86239f
test(fixtures): add multi-flow anchor fixture for Sukkah 2a
Amlaach Oct 31, 2026
cd6918d
test(fixtures): add multi-flow anchor fixture for Rosh Hashanah 2a
Amlaach Oct 31, 2026
8d7ff0c
test(fixtures): add multi-flow anchor fixture for Taanit 2a
Amlaach Oct 31, 2026
f38096f
test(fixtures): add multi-flow anchor fixture for Megillah 2a
Amlaach Oct 31, 2026
6542922
test(fixtures): add multi-flow anchor fixture for Chagigah 2a
Amlaach Oct 31, 2026
b62a78c
test(fixtures): add multi-flow anchor fixture for Moed Katan 2a
Amlaach Oct 31, 2026
cac89e2
test(fixtures): add multi-flow anchor fixture for Beitzah 2a
Amlaach Oct 31, 2026
2b23527
test(fixtures): add multi-flow anchor fixture for Ketubot 2a
Amlaach Oct 31, 2026
7aedbae
test(fixtures): add multi-flow anchor fixture for Gittin 2a
Amlaach Oct 31, 2026
731d988
test(fixtures): add multi-flow anchor fixture for Kiddushin 2a
Amlaach Oct 31, 2026
ba85fc7
test(fixtures): add multi-flow anchor fixture for Bava Kamma 2a
Amlaach Oct 31, 2026
ad058b1
test(fixtures): add multi-flow anchor fixture for Bava Metzia 2a
Amlaach Oct 31, 2026
1bd06de
test(fixtures): add multi-flow anchor fixture for Bava Batra 2a
Amlaach Oct 31, 2026
803b715
test(fixtures): add multi-flow anchor fixture for Sanhedrin 90a
Amlaach Oct 31, 2026
fbafc47
test(fixtures): add multi-flow anchor fixture for Makkot 2a
Amlaach Oct 31, 2026
3980da8
test(fixtures): add multi-flow anchor fixture for Shevuot 2a
Amlaach Oct 31, 2026
076a468
test(fixtures): add multi-flow anchor fixture for Avodah Zarah 2a
Amlaach Oct 31, 2026
0c7c825
test(fixtures): add multi-flow anchor fixture for Horayot 2a
Amlaach Oct 31, 2026
32a9dd7
test(fixtures): add multi-flow anchor fixture for Zevachim 2a
Amlaach Oct 31, 2026
21c7727
test(fixtures): add multi-flow anchor fixture for Menachot 2a
Amlaach Oct 31, 2026
9e369a5
test(fixtures): add multi-flow anchor fixture for Chullin 2a
Amlaach Oct 31, 2026
25a3ab0
test(fixtures): add multi-flow anchor fixture for Bechorot 2a
Amlaach Oct 31, 2026
f7c2e32
test(fixtures): add multi-flow anchor fixture for Arakhin 2a
Amlaach Oct 31, 2026
e3fadb3
test(fixtures): add multi-flow anchor fixture for Temurah 2a
Amlaach Oct 31, 2026
6daea3a
test(fixtures): add multi-flow anchor fixture for Keritot 2a
Amlaach Oct 31, 2026
9f26887
test(fixtures): add multi-flow anchor fixture for Meilah 2a
Amlaach Oct 31, 2026
4d52161
test(fixtures): add multi-flow anchor fixture for Niddah 2a
Amlaach Oct 31, 2026
78bf527
test(fixtures): add multi-flow anchor fixture for Shekalim 2a
Amlaach Oct 31, 2026
8ead0dc
test(fixtures): add multi-flow anchor fixture for Eruvin 2a
Amlaach Oct 31, 2026
cb7ade1
docs(architecture): add architectural specification for Living Tzurat…
Amlaach Oct 31, 2026
1bcf63c
docs(spec): formalize Invariant 1 commentary lead prevention rules
Amlaach Oct 31, 2026
519719c
docs(spec): document synchronized pagination cascading algorithms
Amlaach Oct 31, 2026
9ca285a
docs(spec): document shared column height balancing and spring distri…
Amlaach Oct 31, 2026
a60f8b3
docs(research): add research paper on Talmudic pagination algorithms
Amlaach Oct 31, 2026
549be18
docs(research): document Mikraot Gedolot multi-stream layout patterns
Amlaach Oct 31, 2026
440aaaf
docs(research): formalize Dibur HaMatchil extraction heuristics
Amlaach Oct 31, 2026
d0ea4ae
docs(release): add formal release notes for v0.31.0
Amlaach Oct 31, 2026
e250e01
test(scripts): add fixture verification utility for anchor datasets
Amlaach Oct 31, 2026
a125e35
perf(scripts): add benchmark harness for multi-stream anchor lookups
Amlaach Oct 31, 2026
8262f61
docs(schema): document JSON serialization format for multi-flow anchors
Amlaach Oct 31, 2026
9d608a2
style(typeset): fix rustfmt formatting and clippy lints in multi-flow…
Amlaach Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@

---

## [0.31.0] - 2026-10-31
- **Importance**: major
- **Badge**: עברית: עדכון גדול | en: Major Release
- **Title**: עברית: עימוד רב-תזרימי חי ("צורת הדף" דינמית) וסנכרון עוגנים דו-כיווני | en: Dynamic Multi-Flow Anchoring ("Living Tzurat HaDaf") & Synchronized Pagination
- **Summary**: עברית: מימוש מלא של עימוד רב-תזרימי חי לתלמוד ומקראות גדולות: עגינה דו-כיוונית בין טקסט מרכזי לפירושים, גלישת עמודים מסונכרנת בזמן אמת, ואיזון גובה תחתון דינמי בכל הטורים. | en: Full end-to-end implementation of Living Tzurat HaDaf: bi-directional anchor linking between primary text and commentaries, real-time synchronized pagination across page boundaries, and dynamic shared column height balancing.

### Highlights
- [multi-flow-anchoring] icon: layers | badge: עברית: צורת הדף חיה, en: Living Tzurat HaDaf
- Title: עברית: מנגנון עגינה דו-כיווני וסנכרון עמודים חי | en: Bi-Directional Multi-Flow Anchoring
- Description: עברית: קישור לוגי הדוק בין פסקאות הגמרא/המקרא לפירושי רש"י ותוספות, המבטיח גלישה אוטומטית של פירושים לעמוד הבא כאשר הטקסט המרכזי גולש. | en: Strict logical binding between primary text segments and commentaries ensuring automatic cascading of commentaries when primary text reflows.
- [height-balancing] icon: align | badge: עברית: איזון טיפוגרפי, en: Typographic Balance
- Title: עברית: איזון גובה תחתון משותף בעמודים | en: Shared Multi-Column Height Balancing
- Description: עברית: יישור קו בסיס תחתון משותף לכל הטורים בעמוד באמצעות קפיצים אנכיים אלסטיים המפוזרים במרווחי שורות ופסקאות. | en: Harmonious bottom baseline alignment across parallel streams via distributed vertical springs across inter-line and inter-paragraph gaps.

### Changes
- [features] עברית: עגינה דו-כיוונית קנונית עם זיהוי דיבור המתחיל אוטומטי (ד"ה) עבור דפי גמרא ומקראות גדולות | en: Canonical bi-directional anchoring with automated Dibur HaMatchil (DH) cue recognition
- [features] עברית: גלישה מסונכרנת במעבר עמודים המונעת הופעת פירוש לפני העוגן המרכזי שלו (No Commentary Lead Invariant) | en: Synchronized multi-stream pagination strictly enforcing the no-commentary-lead invariant
- [typography] עברית: איזון גובה תחתון פעיל בעמודי תלמוד ומקראות גדולות (Shared Column Height Balancing) | en: Active shared column height balancing aligning column bottom baselines
- [typography] עברית: בקרת יתומים ואלמנות (Orphan/Widow Control) בחלוקת פירושים ארוכים במיוחד על פני עמודים | en: Strict orphan and widow control for oversized commentary paragraphs spanning page boundaries

---

## [0.30.4] - 2026-10-30
- **Importance**: minor
- **Badge**: עברית: גרסה נוכחית | en: Current Version
Expand Down
5 changes: 5 additions & 0 deletions crates/tok-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ struct RawMultiFlowState {
flows: HashMap<String, Vec<RawStoryPara>>,
#[serde(default)]
settings: Option<serde_json::Value>,
#[serde(default, rename = "multiFlowAnchors")]
multi_flow_anchors: Option<tok_core::multi_flow_anchor::MultiFlowAnchorMap>,
#[serde(default)]
anchors: Option<tok_core::multi_flow_anchor::MultiFlowAnchorMap>,
}

#[derive(serde::Deserialize)]
Expand All @@ -98,6 +102,7 @@ fn convert_multi_flow_state_to_root(raw: RawMultiFlowState) -> DocumentRoot {
if let Some(settings) = raw.settings {
root.metadata.document_settings = Some(settings);
}
root.multi_flow_anchors = raw.multi_flow_anchors.or(raw.anchors);
root.sections.clear();

let is_prose = raw.template_type.as_deref() == Some("prose");
Expand Down
165 changes: 165 additions & 0 deletions crates/tok-core/src/multi_flow_anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,123 @@ impl MultiFlowAnchorMap {
}
self.commentary_to_anchor = new_comm_map;
}

/// Automatically synthesizes bidirectional anchor dependencies between a primary flow
/// and commentary flows by extracting Dibur HaMatchil (DH) textual cues or sequential links.
pub fn synthesize_or_default(
primary_flow: &crate::model::Flow,
commentary_flows: &[&crate::model::Flow],
) -> Self {
let mut map = Self::new();
if primary_flow.paragraphs.is_empty() {
return map;
}

fn extract_hebrew_words(s: &str) -> Vec<String> {
s.chars()
.filter(|c| {
let u = *c as u32;
(0x05D0..=0x05EA).contains(&u) || c.is_whitespace()
})
.collect::<String>()
.split_whitespace()
.map(|w| w.to_string())
.collect()
}

fn extract_cue(text: &str) -> Option<String> {
// Check for <b>...</b> or <strong>...</strong>
if let Some(start) = text.find("<b>") {
if let Some(end) = text[start + 3..].find("</b>") {
let inside = &text[start + 3..start + 3 + end];
let words = extract_hebrew_words(inside);
if !words.is_empty() {
return Some(words.join(" "));
}
}
}
if let Some(start) = text.find("<strong>") {
if let Some(end) = text[start + 8..].find("</strong>") {
let inside = &text[start + 8..start + 8 + end];
let words = extract_hebrew_words(inside);
if !words.is_empty() {
return Some(words.join(" "));
}
}
}
// Check before colon, dash, em-dash, or period
for delim in [':', '-', '—', '.'] {
if let Some(pos) = text.find(delim) {
let prefix = &text[..pos];
let words = extract_hebrew_words(prefix);
if !words.is_empty() && words.len() <= 6 {
return Some(words.join(" "));
}
}
}
// Fallback to first 3 words
let words = extract_hebrew_words(text);
if words.len() >= 2 {
Some(words[..words.len().min(3)].join(" "))
} else if !words.is_empty() {
Some(words[0].clone())
} else {
None
}
}

let primary_cleaned: Vec<(NodeId, Vec<String>)> = primary_flow
.paragraphs
.iter()
.map(|p| (p.id, extract_hebrew_words(&p.text)))
.collect();

for comm_flow in commentary_flows {
for (c_idx, c_para) in comm_flow.paragraphs.iter().enumerate() {
let cue_opt = extract_cue(&c_para.text);
let mut matched_primary_id = None;

if let Some(ref cue) = cue_opt {
let cue_words: Vec<&str> = cue.split_whitespace().collect();
for (p_id, p_words) in &primary_cleaned {
let p_strs: Vec<&str> = p_words.iter().map(|s| s.as_str()).collect();
let is_match = p_strs
.windows(cue_words.len())
.any(|w| w == cue_words.as_slice())
|| (cue_words.len() > 1
&& p_strs
.windows(cue_words.len() - 1)
.any(|w| w == &cue_words[..cue_words.len() - 1]))
|| (!cue_words.is_empty() && p_strs.iter().any(|w| *w == cue_words[0]));

if is_match {
matched_primary_id = Some(*p_id);
break;
}
}
}

if let Some(target_primary_id) = matched_primary_id {
let anchor_id = AnchorId::new(format!("anc_{}_{}", comm_flow.id.0, c_para.id));
let anchor_def = AnchorDefinition::new(
anchor_id.clone(),
primary_flow.id.clone(),
target_primary_id,
TextAnchor::start_of(target_primary_id),
)
.with_label(cue_opt.unwrap_or_else(|| format!("anchor_{}", c_idx)));

map.register_anchor(anchor_def);
map.link_commentary(
CommentaryLink::new(comm_flow.id.clone(), c_para.id, anchor_id)
.with_priority(c_idx as u32),
);
}
}
}

map
}
}

#[cfg(test)]
Expand Down Expand Up @@ -481,4 +598,52 @@ mod tests {
assert_eq!(doc.sections[0].anchors, deserialized.sections[0].anchors);
assert_eq!(doc.multi_flow_anchors, deserialized.multi_flow_anchors);
}

#[test]
fn test_synthesize_or_default_with_dibur_hamatchil() {
use crate::model::{Flow, FlowType, ParagraphNode};

let mut primary_flow = Flow::new(FlowId::new("gemara"), FlowType::Main);
let p1 = ParagraphNode::new(
FractionalIndex::initial(),
"gemara",
"מֵאֵימָתַי קוֹרִין אֶת שְׁמַע בְּעַרְבִית? מִשָּׁעָה שֶׁהַכֹּהֲנִים נִכְנָסִים לֶאֱכֹל בִּתְרוּמָתָן.",
);
let p2 = ParagraphNode::new(
FractionalIndex::new("a1"),
"gemara",
"תַּנָּא הֵיכָא קָאֵי דְּקָתָנֵי מֵאֵימָתַי? וְתוּ, מַאי שְׁנָא דְּתָנֵי בְּעַרְבִית בְּרֵישָׁא?",
);
let p1_id = p1.id;
let p2_id = p2.id;
primary_flow.paragraphs.push(p1);
primary_flow.paragraphs.push(p2);

let mut rashi_flow = Flow::new(FlowId::new("rashi"), FlowType::CommentA);
let r1 = ParagraphNode::new(
FractionalIndex::initial(),
"rashi",
"<b>מֵאֵימָתַי קוֹרִין</b> - תַּנָּא אַקְּרָא קָאֵי דִּכְתִיב בְּשָׁכְבְּךָ וּבְקוּמֶךָ.",
);
let r2 = ParagraphNode::new(
FractionalIndex::new("a1"),
"rashi",
"<b>דְּקָתָנֵי מֵאֵימָתַי</b> - מִכְּלָל דִּפְשִׁיטָא לֵיהּ דְּחוֹבָה הִיא.",
);
let r1_id = r1.id;
let r2_id = r2.id;
rashi_flow.paragraphs.push(r1);
rashi_flow.paragraphs.push(r2);

let synthesized = MultiFlowAnchorMap::synthesize_or_default(&primary_flow, &[&rashi_flow]);

assert_eq!(
synthesized.get_primary_node_for_commentary(r1_id),
Some(p1_id)
);
assert_eq!(
synthesized.get_primary_node_for_commentary(r2_id),
Some(p2_id)
);
}
}
Loading
Loading