this repo has no description
1
fork

Configure Feed

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

fix: blank insertion correctly handles display math and quoted strings

- expand_math_selection: balance typst string literals (`"..."`) — an odd
quote count in the selection now advances end to include the closing `"`
- insert_blank_wrap_math: for display math (`$ ... $`), keep the blank
inside the single block (`$ left #blank[$sel$] right $`) instead of
splitting into two block-level math elements that render on separate lines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+111 -27
+111 -27
crates/tala/src/editor.rs
··· 471 471 end += 1; 472 472 } 473 473 474 + // 4. Balance any unclosed typst string literal (`"..."`) in sel_start..end. 475 + // Quotes use the same character for open and close, so an odd count means 476 + // the selection ends inside a string. 477 + let quote_count = b[sel_start..end].iter().filter(|&&c| c == b'"').count(); 478 + if quote_count % 2 != 0 { 479 + while end < len { 480 + end += 1; 481 + if b[end - 1] == b'"' { 482 + break; 483 + } 484 + } 485 + } 486 + 474 487 (sel_start, end) 475 488 } 476 489 477 490 /// Split an equation at `sel_start..sel_end` (fragment-relative), wrapping the 478 - /// selection in `#blank[$...$]` and leaving the rest as separate `$...$` spans. 491 + /// selection in `#blank[...]`. 492 + /// 493 + /// For **display math** (`$ ... $`): the blank is inserted *inside* the single 494 + /// display math block so the equation stays on one line. The blank's content 495 + /// uses inline math `$...$`. 496 + /// `$ A = #blank[$B$] $` 497 + /// 498 + /// For **inline math** (`$...$`): the equation is split into separate inline 499 + /// spans around the blank (existing behaviour). 500 + /// `$A +$ #blank[$b$] $+ C$` 501 + /// 479 502 /// `eq_start..eq_end` are the fragment-relative bounds of the full equation node 480 503 /// (including the `$` delimiters). Empty left or right parts are omitted. 481 504 fn insert_blank_wrap_math( ··· 486 509 eq_end: usize, 487 510 ) -> String { 488 511 // Detect display math (`$ ... $`) vs inline math (`$...$`). 489 - let (open, close) = if source[eq_start..].starts_with("$ ") && source[..eq_end].ends_with(" $") 490 - { 491 - ("$ ", " $") 492 - } else { 493 - ("$", "$") 494 - }; 512 + let is_display = source[eq_start..].starts_with("$ ") 513 + && source[..eq_end].ends_with(" $"); 514 + let (open, close) = if is_display { ("$ ", " $") } else { ("$", "$") }; 495 515 let content_start = eq_start + open.len(); 496 516 let content_end = eq_end - close.len(); 497 517 ··· 499 519 let selected = source[sel_start..sel_end].trim(); 500 520 let right_inner = source[sel_end..content_end].trim_start(); 501 521 502 - let left_part = if left_inner.is_empty() { 503 - String::new() 504 - } else { 505 - format!("{}{}{} ", open, left_inner, close) 506 - }; 507 - let blank_part = format!("#blank[{}{}{}]", open, selected, close); 508 - let right_part = if right_inner.is_empty() { 509 - String::new() 522 + if is_display { 523 + // Keep everything inside one display math block. 524 + // The blank's content is inline math so it doesn't break the block. 525 + let left_sep = if left_inner.is_empty() { "" } else { " " }; 526 + let right_sep = if right_inner.is_empty() { "" } else { " " }; 527 + format!( 528 + "{}$ {}{}#blank[${}$]{}{} ${}", 529 + &source[..eq_start], 530 + left_inner, 531 + left_sep, 532 + selected, 533 + right_sep, 534 + right_inner, 535 + &source[eq_end..] 536 + ) 510 537 } else { 511 - format!(" {}{}{}", open, right_inner, close) 512 - }; 513 - 514 - format!( 515 - "{}{}{}{}{}", 516 - &source[..eq_start], 517 - left_part, 518 - blank_part, 519 - right_part, 520 - &source[eq_end..] 521 - ) 538 + let left_part = if left_inner.is_empty() { 539 + String::new() 540 + } else { 541 + format!("{}{}{} ", open, left_inner, close) 542 + }; 543 + let blank_part = format!("#blank[{}{}{}]", open, selected, close); 544 + let right_part = if right_inner.is_empty() { 545 + String::new() 546 + } else { 547 + format!(" {}{}{}", open, right_inner, close) 548 + }; 549 + format!( 550 + "{}{}{}{}{}", 551 + &source[..eq_start], 552 + left_part, 553 + blank_part, 554 + right_part, 555 + &source[eq_end..] 556 + ) 557 + } 522 558 } 523 559 524 560 /// Remove whitespace between a card/cloze head and its first `[` content block. ··· 1827 1863 assert_eq!(result, "hello #blank[world]"); 1828 1864 } 1829 1865 1830 - // --- insert_blank_wrap_math --- 1866 + // --- insert_blank_wrap_math (inline) --- 1831 1867 1832 1868 #[test] 1833 1869 fn wrap_math_middle_selection() { ··· 1852 1888 assert_eq!(result, "#blank[$e^x$] $+ 1$"); 1853 1889 } 1854 1890 1891 + // --- insert_blank_wrap_math (display) --- 1892 + 1893 + #[test] 1894 + fn wrap_display_math_rhs() { 1895 + // User selects the RHS of a display math equation. 1896 + // Result must stay inside one display math block, not split into two. 1897 + let src = r#"$ C_(i j)^"SPICE" = - C_(i j)^"Maxwell" $"#; 1898 + let eq_end = src.len(); 1899 + // sel covers `- C_(i j)^"Maxwell"` (content after `= `) 1900 + let sel_start = src.find("- C_").unwrap(); 1901 + let sel_end = src.rfind('"').unwrap() + 1; 1902 + let result = insert_blank_wrap_math(src, sel_start, sel_end, 0, eq_end); 1903 + assert_eq!( 1904 + result, 1905 + r#"$ C_(i j)^"SPICE" = #blank[$- C_(i j)^"Maxwell"$] $"# 1906 + ); 1907 + } 1908 + 1909 + #[test] 1910 + fn wrap_display_math_full() { 1911 + // Entire content selected. 1912 + let src = "$ A = B $"; 1913 + let sel_start = 2; // 'A' 1914 + let sel_end = 7; // after 'B' 1915 + let result = insert_blank_wrap_math(src, sel_start, sel_end, 0, src.len()); 1916 + assert_eq!(result, "$ #blank[$A = B$] $"); 1917 + } 1918 + 1919 + #[test] 1920 + fn wrap_display_math_with_right() { 1921 + // Left and right both non-empty. 1922 + let src = "$ a + b + c $"; 1923 + // 'b' is at byte index 6 in src ("$ a + b + c $") 1924 + let result = insert_blank_wrap_math(src, 6, 7, 0, src.len()); 1925 + assert_eq!(result, "$ a + #blank[$b$] + c $"); 1926 + } 1927 + 1855 1928 // --- expand_math_selection --- 1856 1929 1857 1930 #[test] ··· 1885 1958 // simulate a hit that catches "vec[1" = 0..5, missing close "]" 1886 1959 let (s, e) = expand_math_selection(eq, 0, 5); // "vec[1" 1887 1960 assert_eq!(&eq[s..e], "vec[1, 2, 3]"); 1961 + } 1962 + 1963 + #[test] 1964 + fn expand_unbalanced_string_literal() { 1965 + // Selection covers `- C_(i j)^"Maxwell` but misses the closing `"` 1966 + let eq = r#"C_(i j)^"SPICE" = - C_(i j)^"Maxwell""#; 1967 + // Find index of the `-` and the `l` at end of Maxwell 1968 + let sel_start = eq.find("- C_").unwrap(); 1969 + let sel_end = eq.rfind('"').unwrap(); // index of closing `"` -- select up to but not including it 1970 + let (s, e) = expand_math_selection(eq, sel_start, sel_end); 1971 + assert_eq!(&eq[s..e], r#"- C_(i j)^"Maxwell""#); 1888 1972 } 1889 1973 1890 1974 // ── format_card_frag / map_cursor_after_format ────────────────────────────