this repo has no description
1
fork

Configure Feed

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

WIP

+159 -5
+3
crates/tala-typst/src/lib.rs
··· 34 34 /// Pixel-space `[x, y, w, h]`, fragment-relative byte range, and math flag 35 35 /// for every glyph rendered from the fragment (preamble glyphs excluded). 36 36 pub glyph_map: Vec<([f32; 4], Range<usize>, bool)>, 37 + /// Fragment-relative byte ranges of every `Equation` node in the fragment. 38 + pub math_spans: Vec<Range<usize>>, 37 39 } 38 40 39 41 // ── Entry point ─────────────────────────────────────────────────────────────── ··· 86 88 height: pixmap.height(), 87 89 blank_boxes, 88 90 glyph_map, 91 + math_spans, 89 92 }) 90 93 } 91 94
+156 -5
crates/tala/src/main.rs
··· 124 124 blank_rects: Vec<[f64; 4]>, 125 125 /// Normalized rect, fragment-relative byte range, is_in_math for every glyph. 126 126 glyph_map: Vec<([f64; 4], std::ops::Range<usize>, bool)>, 127 + /// Fragment-relative byte ranges of every equation in the source. 128 + math_spans: Vec<std::ops::Range<usize>>, 127 129 } 128 130 129 131 #[component] ··· 141 143 .unwrap_or_else(|e| Err(e.to_string())) 142 144 }); 143 145 144 - // Blank rects and glyph map synced from latest preview (needed in event closures). 146 + // Blank rects, glyph map, and math spans synced from latest preview (needed in event closures). 145 147 let mut blank_rects_sig = use_signal(|| Vec::<[f64; 4]>::new()); 146 148 let mut glyph_map_sig = 147 149 use_signal(|| Vec::<([f64; 4], std::ops::Range<usize>, bool)>::new()); 150 + let mut math_spans_sig = use_signal(|| Vec::<std::ops::Range<usize>>::new()); 148 151 use_effect(move || { 149 152 if let Some(Ok(data)) = &*preview.read() { 150 153 blank_rects_sig.set(data.blank_rects.clone()); 151 154 glyph_map_sig.set(data.glyph_map.clone()); 155 + math_spans_sig.set(data.math_spans.clone()); 152 156 } 153 157 }); 154 158 ··· 325 329 drawn_boxes.write().push(drawn_rect); 326 330 return; 327 331 } 328 - if hits.iter().any(|(_, _, in_math)| *in_math) { 329 - insert_error.set(Some("Math regions not supported yet — draw over plain text only.".into())); 332 + let all_math = hits.iter().all(|(_, _, m)| *m); 333 + let any_math = hits.iter().any(|(_, _, m)| *m); 334 + let min_start = hits.iter().map(|(_, r, _)| r.start).min().unwrap(); 335 + let max_end = hits.iter().map(|(_, r, _)| r.end ).max().unwrap(); 336 + if all_math { 337 + let spans = math_spans_sig.read().clone(); 338 + let eq = spans.iter().find(|ms| ms.start <= min_start && max_end <= ms.end).cloned(); 339 + match eq { 340 + None => { 341 + insert_error.set(Some("Could not locate equation boundary.".into())); 342 + drawn_boxes.write().push(drawn_rect); 343 + } 344 + Some(eq) => { 345 + let cur = source.read().clone(); 346 + let open_len = if cur[eq.start..].starts_with("$ ") { 2 } else { 1 }; 347 + let close_len = if cur[..eq.end].ends_with(" $") { 2 } else { 1 }; 348 + let content_start = eq.start + open_len; 349 + let content_end = eq.end - close_len; 350 + let (exp_start, exp_end) = expand_math_selection( 351 + &cur[content_start..content_end], 352 + min_start - content_start, 353 + max_end - content_start, 354 + ); 355 + source.set(insert_blank_wrap_math( 356 + &cur, 357 + content_start + exp_start, 358 + content_start + exp_end, 359 + eq.start, eq.end, 360 + )); 361 + insert_error.set(None); 362 + } 363 + } 364 + return; 365 + } 366 + if any_math { 367 + insert_error.set(Some("Selection spans math and text — draw over one type only.".into())); 330 368 drawn_boxes.write().push(drawn_rect); 331 369 return; 332 370 } 333 - let min_start = hits.iter().map(|(_, r, _)| r.start).min().unwrap(); 334 - let max_end = hits.iter().map(|(_, r, _)| r.end ).max().unwrap(); 335 371 let cur = source.read().clone(); 336 372 source.set(insert_blank_wrap(&cur, min_start, max_end)); 337 373 insert_error.set(None); ··· 369 405 } 370 406 371 407 408 + /// Given a raw glyph-based `sel_start..sel_end` within `eq_content` (the inner 409 + /// text of an equation, excluding `$` delimiters), expand the selection so it: 410 + /// 1. Extends `sel_end` to the end of any incomplete identifier token. 411 + /// 2. Includes a trailing `(...)` function-call argument if the token is followed by one. 412 + /// 3. Re-balances any unclosed `()`, `[]`, `{}` pairs. 413 + fn expand_math_selection(eq_content: &str, sel_start: usize, sel_end: usize) -> (usize, usize) { 414 + let b = eq_content.as_bytes(); 415 + let len = b.len(); 416 + let mut end = sel_end.min(len); 417 + 418 + // 1. Expand to end of identifier/number token if sel_end is mid-token. 419 + let is_alnum = |c: u8| c.is_ascii_alphanumeric() || c == b'_'; 420 + if end > 0 && end < len && is_alnum(b[end - 1]) && is_alnum(b[end]) { 421 + while end < len && is_alnum(b[end]) { 422 + end += 1; 423 + } 424 + } 425 + 426 + // 2. If immediately followed by `(`, include the matched parenthesised group. 427 + if end < len && b[end] == b'(' { 428 + end += 1; 429 + let mut depth = 1usize; 430 + while end < len && depth > 0 { 431 + match b[end] { 432 + b'(' => depth += 1, 433 + b')' => { depth -= 1; } 434 + _ => {} 435 + } 436 + end += 1; 437 + } 438 + } 439 + 440 + // 3. Balance any unclosed delimiters in sel_start..end. 441 + let (mut parens, mut brackets, mut braces) = (0i32, 0i32, 0i32); 442 + for &c in &b[sel_start..end] { 443 + match c { 444 + b'(' => parens += 1, b')' => parens -= 1, 445 + b'[' => brackets += 1, b']' => brackets -= 1, 446 + b'{' => braces += 1, b'}' => braces -= 1, 447 + _ => {} 448 + } 449 + } 450 + while end < len && (parens > 0 || brackets > 0 || braces > 0) { 451 + match b[end] { 452 + b'(' => parens += 1, b')' => parens -= 1, 453 + b'[' => brackets += 1, b']' => brackets -= 1, 454 + b'{' => braces += 1, b'}' => braces -= 1, 455 + _ => {} 456 + } 457 + end += 1; 458 + } 459 + 460 + (sel_start, end) 461 + } 462 + 463 + /// Split an equation at `sel_start..sel_end` (fragment-relative), wrapping the 464 + /// selection in `#blank[$...$]` and leaving the rest as separate `$...$` spans. 465 + /// `eq_start..eq_end` are the fragment-relative bounds of the full equation node 466 + /// (including the `$` delimiters). Empty left or right parts are omitted. 467 + fn insert_blank_wrap_math( 468 + source: &str, 469 + sel_start: usize, 470 + sel_end: usize, 471 + eq_start: usize, 472 + eq_end: usize, 473 + ) -> String { 474 + // Detect display math (`$ ... $`) vs inline math (`$...$`). 475 + let (open, close) = if source[eq_start..].starts_with("$ ") && source[..eq_end].ends_with(" $") { 476 + ("$ ", " $") 477 + } else { 478 + ("$", "$") 479 + }; 480 + let content_start = eq_start + open.len(); 481 + let content_end = eq_end - close.len(); 482 + 483 + let left_inner = source[content_start..sel_start].trim_end(); 484 + let selected = source[sel_start..sel_end].trim(); 485 + let right_inner = source[sel_end..content_end].trim_start(); 486 + 487 + let left_part = if left_inner.is_empty() { String::new() } else { format!("{}{}{} ", open, left_inner, close) }; 488 + let blank_part = format!("#blank[{}{}{}]", open, selected, close); 489 + let right_part = if right_inner.is_empty() { String::new() } else { format!(" {}{}{}", open, right_inner, close) }; 490 + 491 + format!("{}{}{}{}{}", &source[..eq_start], left_part, blank_part, right_part, &source[eq_end..]) 492 + } 493 + 494 + #[cfg(test)] 495 + mod tests { 496 + use super::*; 497 + 498 + #[test] 499 + fn expand_unbalanced_paren() { 500 + // Glyph hits cover `e^(-x^2` but miss the closing `)` 501 + let eq = "e^(-x^2)"; 502 + let (s, e) = expand_math_selection(eq, 0, 7); // 0..7 = "e^(-x^2" 503 + assert_eq!(&eq[s..e], "e^(-x^2)"); 504 + } 505 + 506 + #[test] 507 + fn expand_identifier_to_function_call() { 508 + // Only `s` was hit (glyph for radical sign maps to first char of `sqrt`) 509 + let eq = "x = sqrt(pi)"; 510 + let (s, e) = expand_math_selection(eq, 4, 5); // 4..5 = "s" 511 + assert_eq!(&eq[s..e], "sqrt(pi)"); 512 + } 513 + 514 + #[test] 515 + fn expand_no_change_when_already_complete() { 516 + let eq = "a + b"; 517 + let (s, e) = expand_math_selection(eq, 4, 5); // "b" 518 + assert_eq!(&eq[s..e], "b"); 519 + } 520 + } 521 + 372 522 fn render_preview(source: &str) -> Result<PreviewData, String> { 373 523 let deck_dir = PathBuf::from(std::env::temp_dir()); 374 524 ··· 463 613 img_h: result.height, 464 614 blank_rects, 465 615 glyph_map, 616 + math_spans: result.math_spans, 466 617 }) 467 618 }