this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

WIP attempt at paragraph-based segmentation

paragraph-based segmentation (\n\n) of source text into cards, even assuming some of the card texts are malformed

+58 -18
+58 -18
crates/tala/src/main.rs
··· 174 174 Error(String), 175 175 } 176 176 177 - /// A segment of the source file: either a fully-parsed card or unparsed text. 177 + /// A segment of the source file: either a card fragment or unparsed text. 178 178 #[derive(Clone)] 179 179 enum Seg { 180 - Card { card_idx: usize, start: usize, end: usize }, 180 + Card { start: usize, end: usize }, 181 181 Text { start: usize, end: usize }, 182 182 } 183 183 ··· 190 190 } 191 191 } 192 192 193 - fn make_segments(source: &str, cards: &[tala_format::CardEntry]) -> Vec<Seg> { 193 + /// True if a source fragment looks like a card definition by prefix alone. 194 + fn is_card_frag(s: &str) -> bool { 195 + let s = s.trim_start(); 196 + s.starts_with("#card[") || s.starts_with("#card(") 197 + || s.starts_with("#cloze[") || s.starts_with("#cloze(") 198 + || s.starts_with("#img_cloze(") 199 + } 200 + 201 + /// Split source into paragraph segments (blank-line separated) and classify each. 202 + /// Typst is never called here — classification is a pure prefix check. 203 + fn make_segments(source: &str) -> Vec<Seg> { 204 + let bytes = source.as_bytes(); 205 + let len = bytes.len(); 194 206 let mut segs = Vec::new(); 195 - let mut pos = 0usize; 196 - for (ci, card) in cards.iter().enumerate() { 197 - let cs = card.span.start.saturating_sub(1); 198 - let ce = card.span.end; 199 - if pos < cs && source[pos..cs].bytes().any(|b| !b.is_ascii_whitespace()) { 200 - segs.push(Seg::Text { start: pos, end: cs }); 207 + let mut seg_start = 0; 208 + let mut i = 0; 209 + while i < len { 210 + if bytes[i] == b'\n' && i + 1 < len && bytes[i + 1] == b'\n' { 211 + let chunk = &source[seg_start..i]; 212 + if !chunk.trim().is_empty() { 213 + if is_card_frag(chunk) { 214 + segs.push(Seg::Card { start: seg_start, end: i }); 215 + } else { 216 + segs.push(Seg::Text { start: seg_start, end: i }); 217 + } 218 + } 219 + while i < len && bytes[i] == b'\n' { i += 1; } 220 + seg_start = i; 221 + } else { 222 + i += 1; 201 223 } 202 - segs.push(Seg::Card { card_idx: ci, start: cs, end: ce }); 203 - pos = ce; 204 224 } 205 - let tail = source.len(); 206 - if pos < tail && source[pos..tail].bytes().any(|b| !b.is_ascii_whitespace()) { 207 - segs.push(Seg::Text { start: pos, end: tail }); 225 + if seg_start < len && !source[seg_start..].trim().is_empty() { 226 + if is_card_frag(&source[seg_start..]) { 227 + segs.push(Seg::Card { start: seg_start, end: len }); 228 + } else { 229 + segs.push(Seg::Text { start: seg_start, end: len }); 230 + } 208 231 } 209 232 segs 210 233 } ··· 248 271 let mut active_card_start = use_signal(|| 0usize); // byte offset of active segment in full source 249 272 let mut active_card_end = use_signal(|| 0usize); 250 273 let mut n_segs_sig = use_signal(|| 0usize); 274 + // Bumped whenever the active card changes; the bounds effect subscribes only 275 + // to this, not to source, so mid-edit parse errors can't expand the splice window. 276 + let mut active_seg_key = use_signal(|| 0u64); 251 277 252 278 // Per-card render resource: re-runs whenever source changes (300ms debounce). 253 279 let _render = use_resource(move || async move { ··· 274 300 let mut glyph_map_sig = use_signal(Vec::<([f64; 4], std::ops::Range<usize>, bool)>::new); 275 301 let mut math_spans_sig = use_signal(Vec::<std::ops::Range<usize>>::new); 276 302 277 - // Keep active segment bounds, glyph signals, and n_segs in sync. 303 + // Effect 1: update n_segs_sig whenever source changes (for keyboard nav bounds). 278 304 use_effect(move || { 279 305 let text = source.read().clone(); 280 - let idx = *active_idx.read(); 306 + let cards = tala_format::parse_cards(&text); 307 + n_segs_sig.set(make_segments(&text, &cards).len()); 308 + }); 309 + 310 + // Effect 2: update splice bounds and glyph signals ONLY when the active card 311 + // changes (active_seg_key bump), NOT on every source edit. Using peek() for 312 + // source/previews means this effect never re-subscribes to them, so a mid-edit 313 + // parse returning a bloated span cannot expand the splice window. 314 + use_effect(move || { 315 + let _key = *active_seg_key.read(); 316 + let text = source.peek().clone(); 317 + let idx = *active_idx.peek(); 281 318 let cards = tala_format::parse_cards(&text); 282 319 let segs = make_segments(&text, &cards); 283 - n_segs_sig.set(segs.len()); 284 320 if let Some(seg) = segs.get(idx) { 285 321 active_card_start.set(seg.start()); 286 322 active_card_end.set(seg.end()); 287 323 } 288 - let pvs = previews.read(); 324 + let pvs = previews.peek(); 289 325 match segs.get(idx) { 290 326 Some(Seg::Card { card_idx, .. }) => { 291 327 if let Some(Ok(data)) = pvs.get(*card_idx) { ··· 318 354 let cur_idx = *active_idx.read(); 319 355 let n = *n_segs_sig.read(); 320 356 let mut ai = active_idx; 357 + let mut ask = active_seg_key; 321 358 spawn(async move { 322 359 let js = if going_up { 323 360 "var t=document.querySelector('.card-row.active textarea');\ ··· 335 372 }; 336 373 if new_i != cur_idx { 337 374 ai.set(new_i); 375 + { let k = *ask.peek(); ask.set(k + 1); }; 338 376 document::eval( 339 377 "setTimeout(()=>document.querySelector('.card-row.active textarea')?.focus(),0)" 340 378 ).await.ok(); ··· 437 475 onclick: move |_| { 438 476 if !is_active { 439 477 active_idx.set(i); 478 + { let k = *active_seg_key.peek(); active_seg_key.set(k + 1); }; 440 479 spawn(async move { 441 480 document::eval( 442 481 "setTimeout(()=>document.querySelector('.card-row.active textarea')?.focus(),0)" ··· 513 552 onclick: move |_| { 514 553 if !is_active { 515 554 active_idx.set(i); 555 + { let k = *active_seg_key.peek(); active_seg_key.set(k + 1); }; 516 556 spawn(async move { 517 557 document::eval( 518 558 "setTimeout(()=>document.querySelector('.card-row.active textarea')?.focus(),0)"