Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

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

feat: latex render support

RivoLink 513c0a7d 9893aae0

+583 -13
+2 -1
ARCHITECTURE.md
··· 16 16 - `theme_picker.rs` — theme picker state with preview cache 17 17 18 18 - `src/markdown/` 19 - - `mod.rs` — Markdown parsing and render preparation (headings, lists, blockquotes, code blocks) 19 + - `mod.rs` — Markdown parsing and render preparation (headings, lists, blockquotes, code blocks, LaTeX) 20 + - `latex.rs` — LaTeX-to-Unicode conversion: `unicodeit` + postprocessing for `\frac`, `\sqrt`, `^{}`, `_{}` 20 21 - `toc.rs` — TOC extraction and normalization 21 22 - `tables.rs` — table rendering with alignment support 22 23 - `width.rs` — width-aware helpers
+45
Cargo.lock
··· 9 9 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 10 11 11 [[package]] 12 + name = "aho-corasick" 13 + version = "1.1.4" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 16 + dependencies = [ 17 + "memchr", 18 + ] 19 + 20 + [[package]] 12 21 name = "allocator-api2" 13 22 version = "0.2.21" 14 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 710 719 "sha2", 711 720 "syntect", 712 721 "unicode-width 0.1.14", 722 + "unicodeit", 713 723 ] 714 724 715 725 [[package]] ··· 1079 1089 ] 1080 1090 1081 1091 [[package]] 1092 + name = "regex" 1093 + version = "1.12.3" 1094 + source = "registry+https://github.com/rust-lang/crates.io-index" 1095 + checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" 1096 + dependencies = [ 1097 + "aho-corasick", 1098 + "memchr", 1099 + "regex-automata", 1100 + "regex-syntax", 1101 + ] 1102 + 1103 + [[package]] 1104 + name = "regex-automata" 1105 + version = "0.4.14" 1106 + source = "registry+https://github.com/rust-lang/crates.io-index" 1107 + checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" 1108 + dependencies = [ 1109 + "aho-corasick", 1110 + "memchr", 1111 + "regex-syntax", 1112 + ] 1113 + 1114 + [[package]] 1082 1115 name = "regex-syntax" 1083 1116 version = "0.8.10" 1084 1117 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1670 1703 version = "0.2.0" 1671 1704 source = "registry+https://github.com/rust-lang/crates.io-index" 1672 1705 checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1706 + 1707 + [[package]] 1708 + name = "unicodeit" 1709 + version = "0.2.1" 1710 + source = "registry+https://github.com/rust-lang/crates.io-index" 1711 + checksum = "1c58ef1816c0901804d0e1e4df281cd4e3d7eb12a0616fd19ec851f5a48bcf4b" 1712 + dependencies = [ 1713 + "aho-corasick", 1714 + "cfg-if", 1715 + "memchr", 1716 + "regex", 1717 + ] 1673 1718 1674 1719 [[package]] 1675 1720 name = "untrusted"
+1
Cargo.toml
··· 20 20 serde = { version = "1.0", features = ["derive"] } 21 21 semver = "1.0" 22 22 sha2 = "0.10" 23 + unicodeit = "0.2"
+2
README.md
··· 147 147 - ✅ TOC sidebar with active section tracking and two-level navigation 148 148 - ✅ Search with match highlighting, `/`, `Ctrl+F`, and `n` / `N` 149 149 - ✅ Code blocks `┌─ lang ───┐` 150 + - ✅ LaTeX math rendering — inline `$...$` and display `$$...$$` with Unicode conversion via `unicodeit` 151 + - ✅ LaTeX code blocks `` ```latex `` / `` ```tex `` rendered as formula blocks 150 152 - ✅ Bold, italic, strikethrough, blockquotes, lists, and horizontal rules 151 153 - ✅ YAML frontmatter is ignored in both preview and TOC 152 154 - ✅ Native stdin input with bounded size
+76 -1
TESTING.md
··· 48 48 - tables with left, center, and right alignment 49 49 - fenced code blocks with language labels 50 50 - wide characters such as `東京` 51 + - inline math formulas with `$...$` 52 + - display math blocks with `$$...$$` 51 53 52 54 ### Navigation And Search 53 55 ··· 90 92 91 93 ## Notes 92 94 93 - The fixture intentionally includes repeated search terms, loose list items, ordered lists starting at non-`1` values, tables, code blocks, and wide characters because those are easy places for terminal Markdown renderers to regress. 95 + The fixture intentionally includes repeated search terms, loose list items, ordered lists starting at non-`1` values, tables, code blocks, wide characters, and math formulas because those are easy places for terminal Markdown renderers to regress. 94 96 95 97 ## Manual Fixture 96 98 ··· 213 215 primary: tokyo-signal 214 216 secondary: unicode-width-check 215 217 ``` 218 + 219 + ### Math Inline 220 + 221 + The Pythagorean theorem states that $a^2 + b^2 = c^2$ in a right triangle. 222 + 223 + Einstein's famous equation $E = mc^2$ relates energy and mass. 224 + 225 + This paragraph mixes **bold with $x^2 + y^2$** and *italic with $\alpha + \beta$* and `code` to check style interactions. 226 + 227 + The area of a circle is $A = \pi r^2$ and its circumference is $C = 2\pi r$. 228 + 229 + A sum $\sum_{i=1}^{n} x_i$ and an integral $\int_0^1 f(x)\,dx$ inline. 230 + 231 + ### Math Display 232 + 233 + $$E = mc^2$$ 234 + 235 + $$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$ 236 + 237 + $$\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}$$ 238 + 239 + $$\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}$$ 240 + 241 + $$\nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t}$$ 242 + 243 + ### Math in Context 244 + 245 + #### Math in Blockquote 246 + 247 + > Euler's identity: $e^{i\pi} + 1 = 0$ 248 + > 249 + > As a display block: 250 + > 251 + > $$e^{i\pi} + 1 = 0$$ 252 + 253 + #### Math in List 254 + 255 + - Newton's first law: $F = 0 \Rightarrow \Delta v = 0$ 256 + - Newton's second law: $F = ma$ 257 + - Gravitation: $F = G\frac{m_1 m_2}{r^2}$ 258 + 259 + 1. Quadratic: $ax^2 + bx + c = 0$ 260 + 2. Solution: $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$ 261 + 262 + #### Math with Unicode Symbols 263 + 264 + Symbols render correctly: ∀x ∈ ℝ, ∃y such that x + y = 0. 265 + 266 + Greek letters: α β γ δ ε π σ ω and uppercase Γ Δ Σ Ω. 267 + 268 + Superscripts and subscripts: x⁰ x¹ x² x³ x⁴ aₙ = aₙ₋₁ + aₙ₋₂. 269 + 270 + Operators: ≤ ≥ ≠ ≈ ± × ÷ → ⇒ ⇔ ∪ ∩ ⊂ ∅ ∞. 271 + 272 + #### LaTeX Code Block 273 + 274 + ```latex 275 + \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} 276 + ``` 277 + 278 + ```latex 279 + \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} 280 + 281 + \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2} 282 + ``` 283 + 284 + ```tex 285 + \nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t} 286 + ``` 287 + 288 + #### Dollar Sign (No Math) 289 + 290 + This costs $5.00 and that costs $10 each. 216 291 217 292 ### Wide Characters 218 293
+223
src/markdown/latex.rs
··· 1 + pub(crate) fn to_unicode(text: &str) -> String { 2 + let preprocessed = strip_command_spaces(text); 3 + let converted = unicodeit::replace(&preprocessed); 4 + postprocess(&converted) 5 + } 6 + 7 + fn strip_command_spaces(input: &str) -> String { 8 + let mut result = String::with_capacity(input.len()); 9 + let chars: Vec<char> = input.chars().collect(); 10 + let len = chars.len(); 11 + let mut i = 0; 12 + 13 + while i < len { 14 + if chars[i] == '\\' && i + 1 < len && chars[i + 1].is_ascii_alphabetic() { 15 + result.push('\\'); 16 + i += 1; 17 + while i < len && chars[i].is_ascii_alphabetic() { 18 + result.push(chars[i]); 19 + i += 1; 20 + } 21 + if i < len && chars[i] == ' ' { 22 + let next = chars.get(i + 1).copied().unwrap_or(' '); 23 + if next.is_ascii_alphabetic() || next == '\\' || next == '{' { 24 + i += 1; 25 + } 26 + } 27 + continue; 28 + } 29 + result.push(chars[i]); 30 + i += 1; 31 + } 32 + 33 + result 34 + } 35 + 36 + fn postprocess(input: &str) -> String { 37 + let mut result = String::with_capacity(input.len()); 38 + let mut i = 0; 39 + 40 + while i < input.len() { 41 + if input[i..].starts_with("\\frac{") { 42 + if let Some((output, end)) = parse_frac(input, i) { 43 + result.push_str(&output); 44 + i = end; 45 + continue; 46 + } 47 + result.push_str("\\frac{"); 48 + i += 6; 49 + continue; 50 + } 51 + 52 + if input[i..].starts_with("√{") { 53 + let brace_start = i + '√'.len_utf8() + 1; 54 + if let Some((group, end)) = read_brace_group(input, brace_start) { 55 + result.push('√'); 56 + result.push('('); 57 + result.push_str(&postprocess(group)); 58 + result.push(')'); 59 + i = end; 60 + continue; 61 + } 62 + } 63 + 64 + if input[i..].starts_with("^{") { 65 + if let Some((output, end)) = convert_script(input, i + 2, to_superscript) { 66 + result.push_str(&output); 67 + i = end; 68 + continue; 69 + } 70 + result.push_str("^{"); 71 + i += 2; 72 + continue; 73 + } 74 + 75 + if input[i..].starts_with("_{") { 76 + if let Some((output, end)) = convert_script(input, i + 2, to_subscript) { 77 + result.push_str(&output); 78 + i = end; 79 + continue; 80 + } 81 + result.push_str("_{"); 82 + i += 2; 83 + continue; 84 + } 85 + 86 + if input[i..].starts_with('^') && i + 1 < input.len() { 87 + let next = input[i + 1..].chars().next().unwrap(); 88 + if next != '{' { 89 + i += 1; 90 + continue; 91 + } 92 + } 93 + 94 + let ch = input[i..].chars().next().unwrap(); 95 + result.push(ch); 96 + i += ch.len_utf8(); 97 + } 98 + 99 + result 100 + } 101 + 102 + fn parse_frac(input: &str, start: usize) -> Option<(String, usize)> { 103 + let after_frac = start + 6; 104 + let (num, after_num) = read_brace_group(input, after_frac)?; 105 + if after_num >= input.len() || input.as_bytes()[after_num] != b'{' { 106 + return None; 107 + } 108 + let (den, after_den) = read_brace_group(input, after_num + 1)?; 109 + let num = postprocess(num); 110 + let den = postprocess(den); 111 + let mut out = String::new(); 112 + wrap_if_multi(&mut out, &num); 113 + out.push('/'); 114 + wrap_if_multi(&mut out, &den); 115 + Some((out, after_den)) 116 + } 117 + 118 + fn wrap_if_multi(out: &mut String, s: &str) { 119 + if s.chars().count() > 1 { 120 + out.push('('); 121 + out.push_str(s); 122 + out.push(')'); 123 + } else { 124 + out.push_str(s); 125 + } 126 + } 127 + 128 + fn convert_script( 129 + input: &str, 130 + brace_start: usize, 131 + mapper: fn(char) -> char, 132 + ) -> Option<(String, usize)> { 133 + let (group, end) = read_brace_group(input, brace_start)?; 134 + let group = postprocess(group); 135 + let mapped: String = group.chars().map(mapper).collect(); 136 + let all_converted = mapped 137 + .chars() 138 + .zip(group.chars()) 139 + .all(|(m, g)| m != g || g.is_ascii_digit()); 140 + if all_converted { 141 + Some((mapped, end)) 142 + } else { 143 + Some((format!("({group})"), end)) 144 + } 145 + } 146 + 147 + fn read_brace_group(input: &str, start: usize) -> Option<(&str, usize)> { 148 + let bytes = input.as_bytes(); 149 + let mut depth: u32 = 1; 150 + let mut i = start; 151 + while i < bytes.len() && depth > 0 { 152 + match bytes[i] { 153 + b'{' => depth += 1, 154 + b'}' => depth -= 1, 155 + _ => {} 156 + } 157 + if depth > 0 { 158 + i += 1; 159 + } 160 + } 161 + if depth == 0 { 162 + Some((&input[start..i], i + 1)) 163 + } else { 164 + None 165 + } 166 + } 167 + 168 + fn to_superscript(ch: char) -> char { 169 + match ch { 170 + '0' => '⁰', 171 + '1' => '¹', 172 + '2' => '²', 173 + '3' => '³', 174 + '4' => '⁴', 175 + '5' => '⁵', 176 + '6' => '⁶', 177 + '7' => '⁷', 178 + '8' => '⁸', 179 + '9' => '⁹', 180 + '+' => '⁺', 181 + '-' | '−' => '⁻', 182 + '=' => '⁼', 183 + '(' => '⁽', 184 + ')' => '⁾', 185 + 'n' => 'ⁿ', 186 + 'i' => 'ⁱ', 187 + _ => ch, 188 + } 189 + } 190 + 191 + fn to_subscript(ch: char) -> char { 192 + match ch { 193 + '0' => '₀', 194 + '1' => '₁', 195 + '2' => '₂', 196 + '3' => '₃', 197 + '4' => '₄', 198 + '5' => '₅', 199 + '6' => '₆', 200 + '7' => '₇', 201 + '8' => '₈', 202 + '9' => '₉', 203 + '+' => '₊', 204 + '-' | '−' => '₋', 205 + '=' => '₌', 206 + '(' => '₍', 207 + ')' => '₎', 208 + 'a' => 'ₐ', 209 + 'e' => 'ₑ', 210 + 'i' => 'ᵢ', 211 + 'j' => 'ⱼ', 212 + 'k' => 'ₖ', 213 + 'n' => 'ₙ', 214 + 'o' => 'ₒ', 215 + 'p' => 'ₚ', 216 + 'r' => 'ᵣ', 217 + 's' => 'ₛ', 218 + 't' => 'ₜ', 219 + 'u' => 'ᵤ', 220 + 'x' => 'ₓ', 221 + _ => ch, 222 + } 223 + }
+149 -11
src/markdown/mod.rs
··· 1 + mod latex; 1 2 mod tables; 2 3 pub(crate) mod toc; 3 4 pub(crate) mod width; ··· 485 486 code_buf.clear(); 486 487 } 487 488 489 + fn push_latex_block_lines( 490 + lines: &mut Vec<Line<'static>>, 491 + content: &str, 492 + render_width: usize, 493 + theme: &MarkdownTheme, 494 + blockquote_depth: usize, 495 + list_stack: &[ListKind], 496 + item_stack: &mut [ItemState], 497 + ) { 498 + let prefix = if !item_stack.is_empty() { 499 + list_item_prefix(blockquote_depth > 0, list_stack, item_stack) 500 + } else if blockquote_depth > 0 { 501 + block_prefix(true) 502 + } else { 503 + Vec::new() 504 + }; 505 + let prefix_width: usize = prefix 506 + .iter() 507 + .map(|span| display_width(span.content.as_ref())) 508 + .sum(); 509 + let available_width = render_width.saturating_sub(prefix_width); 510 + let frame_style = Style::default().fg(theme.code_frame); 511 + let label_style = Style::default().fg(theme.code_label); 512 + let gutter_style = Style::default().fg(theme.code_gutter); 513 + let content_style = Style::default().fg(theme.latex_block_fg); 514 + 515 + let label = "latex"; 516 + let rendered_content = latex::to_unicode(content); 517 + let content_lines: Vec<&str> = rendered_content.lines().collect(); 518 + let total_lines = content_lines.len().max(1); 519 + let digit_width = total_lines.to_string().len(); 520 + let gutter_width = digit_width + 2; 521 + 522 + let max_text = content_lines 523 + .iter() 524 + .map(|l| display_width(l)) 525 + .max() 526 + .unwrap_or(0); 527 + let max_inner_width = available_width 528 + .saturating_sub(2) 529 + .max(UnicodeWidthStr::width(label) + 3); 530 + let min_inner = (UnicodeWidthStr::width(label) + 3) 531 + .max(44) 532 + .min(max_inner_width); 533 + let inner_width = (max_text + 2 + gutter_width) 534 + .max(min_inner) 535 + .min(max_inner_width); 536 + let content_width = inner_width.saturating_sub(gutter_width + 1); 537 + 538 + let header_width = UnicodeWidthStr::width(label) + 3; 539 + let top_bar = "─".repeat(inner_width.saturating_sub(header_width)); 540 + let mut header = prefix.clone(); 541 + header.extend([ 542 + Span::styled("┌─ ".to_string(), frame_style), 543 + Span::styled(format!("{label} "), label_style), 544 + Span::styled(format!("{top_bar}┐"), frame_style), 545 + ]); 546 + lines.push(Line::from(header)); 547 + 548 + for (i, content_line) in content_lines.iter().enumerate() { 549 + let line_num = i + 1; 550 + let num_gutter = Span::styled(format!("│{:>w$}│", line_num, w = digit_width), gutter_style); 551 + let blank_gutter = Span::styled(format!("│{:>w$}│", "", w = digit_width), gutter_style); 552 + let content_spans = vec![Span::styled(content_line.to_string(), content_style)]; 553 + 554 + let mut first_prefix = prefix.clone(); 555 + first_prefix.push(num_gutter); 556 + 557 + let mut cont_prefix = prefix.clone(); 558 + cont_prefix.push(blank_gutter); 559 + 560 + push_wrapped_code_lines( 561 + lines, 562 + content_spans, 563 + first_prefix, 564 + cont_prefix, 565 + gutter_style, 566 + content_width, 567 + ); 568 + } 569 + 570 + let mut footer = prefix; 571 + footer.push(Span::styled( 572 + format!( 573 + "└{}┴{}┘", 574 + "─".repeat(gutter_width - 2), 575 + "─".repeat(inner_width.saturating_sub(gutter_width - 1)) 576 + ), 577 + frame_style, 578 + )); 579 + lines.push(Line::from(footer)); 580 + lines.push(Line::from("")); 581 + } 582 + 488 583 fn inline_text_style( 489 584 theme: &MarkdownTheme, 490 585 blockquote_depth: usize, ··· 617 712 Style::default() 618 713 .fg(theme.inline_code_fg) 619 714 .bg(theme.inline_code_bg), 715 + )); 716 + } 717 + 718 + fn push_inline_latex_span(spans: &mut Vec<Span<'static>>, text: &str, theme: &MarkdownTheme) { 719 + let rendered = latex::to_unicode(text); 720 + spans.push(Span::styled( 721 + format!(" {rendered} "), 722 + Style::default() 723 + .fg(theme.latex_inline_fg) 724 + .bg(theme.latex_inline_bg), 620 725 )); 621 726 } 622 727 ··· 862 967 } 863 968 MdEvent::End(TagEnd::CodeBlock) => { 864 969 in_code = false; 865 - push_code_block_lines( 866 - &mut lines, 867 - &mut code_buf, 868 - &mut code_lang, 869 - CodeBlockRenderContext { 870 - ss, 871 - theme, 970 + if code_lang == "latex" || code_lang == "tex" { 971 + push_latex_block_lines( 972 + &mut lines, 973 + &code_buf, 872 974 render_width, 873 975 theme_colors, 874 976 blockquote_depth, 875 - list_stack: &list_stack, 876 - }, 877 - &mut item_stack, 878 - ); 977 + &list_stack, 978 + &mut item_stack, 979 + ); 980 + code_buf.clear(); 981 + code_lang.clear(); 982 + } else { 983 + push_code_block_lines( 984 + &mut lines, 985 + &mut code_buf, 986 + &mut code_lang, 987 + CodeBlockRenderContext { 988 + ss, 989 + theme, 990 + render_width, 991 + theme_colors, 992 + blockquote_depth, 993 + list_stack: &list_stack, 994 + }, 995 + &mut item_stack, 996 + ); 997 + } 879 998 last_block = LastBlock::Other; 880 999 } 881 1000 MdEvent::Code(text) => { ··· 935 1054 &mut item_stack, 936 1055 render_width, 937 1056 ); 1057 + } 1058 + MdEvent::InlineMath(text) => { 1059 + push_inline_latex_span(&mut spans, text.as_ref(), theme_colors); 1060 + } 1061 + MdEvent::DisplayMath(text) => { 1062 + if !spans.is_empty() { 1063 + lines.push(Line::from(std::mem::take(&mut spans))); 1064 + } 1065 + trim_paragraph_gap_before_block(&mut lines, last_block); 1066 + push_latex_block_lines( 1067 + &mut lines, 1068 + text.as_ref(), 1069 + render_width, 1070 + theme_colors, 1071 + blockquote_depth, 1072 + &list_stack, 1073 + &mut item_stack, 1074 + ); 1075 + last_block = LastBlock::Other; 938 1076 } 939 1077 _ => {} 940 1078 }
+70
src/tests/markdown.rs
··· 362 362 assert!(rendered[header_idx].starts_with(" ")); 363 363 assert!(rendered[code_idx].starts_with(" ")); 364 364 } 365 + 366 + #[test] 367 + fn inline_latex_renders_with_latex_style() { 368 + let (ss, theme) = test_assets(); 369 + let (lines, _) = parse_markdown("The formula $x^2 + y^2$ is here.\n", &ss, &theme); 370 + 371 + let latex_line = lines 372 + .iter() 373 + .find(|line| line_plain_text(line).contains("x² + y²")) 374 + .expect("expected a line containing inline latex content"); 375 + 376 + let latex_span = latex_line 377 + .spans 378 + .iter() 379 + .find(|span| span.content.contains("x² + y²")) 380 + .expect("expected a span with latex content"); 381 + 382 + assert!( 383 + latex_span.style.bg.is_some(), 384 + "inline latex should have a background color" 385 + ); 386 + } 387 + 388 + #[test] 389 + fn display_latex_renders_in_framed_block() { 390 + let (ss, theme) = test_assets(); 391 + let (lines, _) = parse_markdown("$$E = mc^2$$\n", &ss, &theme); 392 + let rendered = rendered_non_empty_lines(&lines); 393 + 394 + assert!( 395 + rendered.iter().any(|line| line.contains("┌─ latex")), 396 + "expected latex block header" 397 + ); 398 + assert!( 399 + rendered.iter().any(|line| line.contains("E = mc²")), 400 + "expected latex content" 401 + ); 402 + assert!( 403 + rendered.iter().any(|line| line.contains("└")), 404 + "expected latex block footer" 405 + ); 406 + } 407 + 408 + #[test] 409 + fn inline_latex_is_searchable() { 410 + let (ss, theme) = test_assets(); 411 + let (lines, _) = parse_markdown("Check $\\alpha + \\beta$ here.\n", &ss, &theme); 412 + let searchable: Vec<String> = lines.iter().map(line_plain_text).collect(); 413 + 414 + assert!( 415 + searchable.iter().any(|line| line.contains("α + β")), 416 + "latex content should be searchable" 417 + ); 418 + } 419 + 420 + #[test] 421 + fn display_latex_in_blockquote_has_quote_prefix() { 422 + let (ss, theme) = test_assets(); 423 + let (lines, _) = parse_markdown("> $$F = ma$$\n", &ss, &theme); 424 + let rendered = rendered_non_empty_lines(&lines); 425 + 426 + let header = rendered 427 + .iter() 428 + .find(|line| line.contains("┌─ latex")) 429 + .expect("expected latex block header in blockquote"); 430 + assert!( 431 + header.starts_with('▏'), 432 + "latex block in blockquote should have quote prefix" 433 + ); 434 + }
+15
src/theme.rs
··· 89 89 pub(crate) blockquote_text: Color, 90 90 pub(crate) text: Color, 91 91 pub(crate) strong_text: Color, 92 + pub(crate) latex_inline_fg: Color, 93 + pub(crate) latex_inline_bg: Color, 94 + pub(crate) latex_block_fg: Color, 92 95 } 93 96 94 97 const BASE_LIGHT_UI: UiTheme = UiTheme { ··· 190 193 blockquote_text: Color::Rgb(114, 116, 158), 191 194 text: Color::Rgb(58, 68, 78), 192 195 strong_text: Color::Rgb(26, 32, 40), 196 + latex_inline_fg: Color::Rgb(128, 68, 148), 197 + latex_inline_bg: Color::Rgb(236, 226, 240), 198 + latex_block_fg: Color::Rgb(108, 58, 128), 193 199 }; 194 200 195 201 const BASE_DARK_MARKDOWN: MarkdownTheme = MarkdownTheme { ··· 219 225 blockquote_text: Color::Rgb(148, 148, 195), 220 226 text: Color::Rgb(208, 210, 218), 221 227 strong_text: Color::Rgb(245, 245, 255), 228 + latex_inline_fg: Color::Rgb(200, 160, 225), 229 + latex_inline_bg: Color::Rgb(38, 28, 48), 230 + latex_block_fg: Color::Rgb(195, 155, 220), 222 231 }; 223 232 224 233 pub(crate) const ARCTIC_THEME: AppTheme = AppTheme { ··· 291 300 blockquote_text: Color::Rgb(160, 168, 188), 292 301 text: Color::Rgb(212, 218, 212), 293 302 strong_text: Color::Rgb(246, 248, 246), 303 + latex_inline_fg: Color::Rgb(192, 162, 218), 304 + latex_inline_bg: Color::Rgb(34, 28, 42), 305 + latex_block_fg: Color::Rgb(188, 158, 214), 294 306 }, 295 307 }; 296 308 ··· 364 376 blockquote_text: Color::Rgb(131, 148, 150), 365 377 text: Color::Rgb(147, 161, 161), 366 378 strong_text: Color::Rgb(238, 232, 213), 379 + latex_inline_fg: Color::Rgb(108, 113, 196), 380 + latex_inline_bg: Color::Rgb(14, 48, 58), 381 + latex_block_fg: Color::Rgb(108, 113, 196), 367 382 }, 368 383 }; 369 384