max / quasi
3 files changed,
+116 insertions,
-14 deletions
| @@ -786,7 +786,7 @@ | |||
| 786 | 786 | ||
| 787 | 787 | [[package]] | |
| 788 | 788 | name = "docengine" | |
| 789 | - | version = "0.6.0" | |
| 789 | + | version = "0.7.0" | |
| 790 | 790 | dependencies = [ | |
| 791 | 791 | "ammonia", | |
| 792 | 792 | "pulldown-cmark", | |
| @@ -5159,6 +5159,14 @@ | |||
| 5159 | 5159 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5160 | 5160 | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" | |
| 5161 | 5161 | ||
| 5162 | + | [[patch.unused]] | |
| 5163 | + | name = "synckit-client" | |
| 5164 | + | version = "0.8.0" | |
| 5165 | + | ||
| 5166 | + | [[patch.unused]] | |
| 5167 | + | name = "synckit-config" | |
| 5168 | + | version = "0.2.0" | |
| 5169 | + | ||
| 5162 | 5170 | [[patch.unused]] | |
| 5163 | 5171 | name = "kberg" | |
| 5164 | 5172 | version = "0.1.0" | |
| @@ -5170,11 +5178,3 @@ | |||
| 5170 | 5178 | [[patch.unused]] | |
| 5171 | 5179 | name = "tagtree" | |
| 5172 | 5180 | version = "0.4.0" | |
| 5173 | - | ||
| 5174 | - | [[patch.unused]] | |
| 5175 | - | name = "synckit-client" | |
| 5176 | - | version = "0.8.0" | |
| 5177 | - | ||
| 5178 | - | [[patch.unused]] | |
| 5179 | - | name = "synckit-config" | |
| 5180 | - | version = "0.2.0" |
| @@ -349,16 +349,63 @@ | |||
| 349 | 349 | Style::default().fg(tui.theme().content_primary) | |
| 350 | 350 | } | |
| 351 | 351 | ||
| 352 | - | /// Markdown source as spans: the words, each under the marks that were over it. | |
| 352 | + | /// Markdown source as spans: the words, each under the marks that were over it | |
| 353 | + | /// and in the shape of the block it came from. | |
| 353 | 354 | /// | |
| 354 | 355 | /// `base` is what the prose takes where the source said nothing, so the same | |
| 355 | 356 | /// function serves a rich node standing alone and one sitting inside a row's | |
| 356 | 357 | /// run, where the part's role has already decided the colour. | |
| 357 | 358 | fn rich_spans(tui: &Tui, source: &str, base: Style) -> Vec<Span<'static>> { | |
| 358 | - | docengine::render_runs(source) | |
| 359 | - | .into_iter() | |
| 360 | - | .map(|run| Span::styled(run.text, mark(tui, base, run.emphasis))) | |
| 361 | - | .collect() | |
| 359 | + | let mut spans = Vec::new(); | |
| 360 | + | // A marker belongs at the head of a line and nowhere else, and a run knows | |
| 361 | + | // its block but not its position. The separator runs are what carry the | |
| 362 | + | // breaks, so the run before this one is what says whether a line just | |
| 363 | + | // started. | |
| 364 | + | let mut starting = true; | |
| 365 | + | for run in docengine::render_runs(source) { | |
| 366 | + | if starting && let Some(marker) = marker(run.block) { | |
| 367 | + | spans.push(Span::styled( | |
| 368 | + | marker, | |
| 369 | + | Style::default().fg(tui.theme().content_muted), | |
| 370 | + | )); | |
| 371 | + | } | |
| 372 | + | starting = run.text.ends_with('\n'); | |
| 373 | + | let style = style_of(tui, base, &run); | |
| 374 | + | spans.push(Span::styled(run.text, style)); | |
| 375 | + | } | |
| 376 | + | spans | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | /// What a block puts in front of its first line, where a webview would have used | |
| 380 | + | /// a bullet glyph or an indent. | |
| 381 | + | /// | |
| 382 | + | /// The description carries no marker of its own, deliberately: what a bullet | |
| 383 | + | /// looks like is the renderer's answer, and this is a terminal's. | |
| 384 | + | fn marker(block: docengine::Block) -> Option<&'static str> { | |
| 385 | + | match block { | |
| 386 | + | docengine::Block::Item => Some("- "), | |
| 387 | + | docengine::Block::Quote => Some("> "), | |
| 388 | + | docengine::Block::Prose | docengine::Block::Heading(_) => None, | |
| 389 | + | } | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | /// One run's block and marks as a style over `base`. | |
| 393 | + | /// | |
| 394 | + | /// The block decides the ground the run is drawn on and the marks are added to | |
| 395 | + | /// it, which is the order a stylesheet uses: a heading with `**bold**` inside it | |
| 396 | + | /// is bold on top of heading weight rather than instead of it. | |
| 397 | + | fn style_of(tui: &Tui, base: Style, run: &docengine::TextRun) -> Style { | |
| 398 | + | let ground = match run.block { | |
| 399 | + | // The three markdown levels a terminal can tell apart, which is as many | |
| 400 | + | // as `layout::Heading` has: a rich node's `######` and its `###` land in | |
| 401 | + | // the same place because a cell has one size and only so much colour. | |
| 402 | + | docengine::Block::Heading(1) => tui.heading(layout::Heading::Page), | |
| 403 | + | docengine::Block::Heading(2) => tui.heading(layout::Heading::Section), | |
| 404 | + | docengine::Block::Heading(_) => tui.heading(layout::Heading::Subsection), | |
| 405 | + | docengine::Block::Quote => Style::default().fg(tui.theme().content_secondary), | |
| 406 | + | docengine::Block::Prose | docengine::Block::Item => base, | |
| 407 | + | }; | |
| 408 | + | mark(tui, ground, run.emphasis) | |
| 362 | 409 | } | |
| 363 | 410 | ||
| 364 | 411 | /// One run's marks as a style over `base`. |
| @@ -168,6 +168,61 @@ | |||
| 168 | 168 | assert_eq!(buf[(16, 0)].bg, prose, "and the prose after it should"); | |
| 169 | 169 | } | |
| 170 | 170 | ||
| 171 | + | #[test] | |
| 172 | + | fn a_heading_inside_a_rich_node_is_drawn_heavier_than_the_prose_under_it() { | |
| 173 | + | // The gap this closed: `render_plain` handed over a heading's text at the | |
| 174 | + | // weight of everything around it, so a rich node's structure was gone by | |
| 175 | + | // the time a cell saw it. Weight is a thing a cell has. | |
| 176 | + | let buf = buffer(&Node::rich("# Title\n\nBody."), 40, 4); | |
| 177 | + | let out = rows(&buf); | |
| 178 | + | assert_eq!(out[0], "Title"); | |
| 179 | + | assert_eq!(out[2], "Body."); | |
| 180 | + | assert_eq!(marked(&buf, 0, Modifier::BOLD), "Title"); | |
| 181 | + | assert_eq!(marked(&buf, 2, Modifier::BOLD), ""); | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | #[test] | |
| 185 | + | fn a_deep_heading_reads_as_the_shallowest_a_terminal_can_tell_apart() { | |
| 186 | + | // Six markdown levels onto the three `layout::Heading` has. A cell has one | |
| 187 | + | // size and only so much colour, so `###` and `######` land together rather | |
| 188 | + | // than inventing distinctions nothing can draw. | |
| 189 | + | let third = buffer(&Node::rich("### Third"), 40, 2); | |
| 190 | + | let sixth = buffer(&Node::rich("###### Sixth"), 40, 2); | |
| 191 | + | assert_eq!(third[(0, 0)].fg, sixth[(0, 0)].fg); | |
| 192 | + | // And not the same as the prose it sits above, or the level bought nothing. | |
| 193 | + | let prose = buffer(&Node::rich("Third"), 40, 2); | |
| 194 | + | assert_ne!(third[(0, 0)].fg, prose[(0, 0)].fg); | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | #[test] | |
| 198 | + | fn a_list_gets_the_bullet_the_description_refused_to_carry() { | |
| 199 | + | // docengine says "this run is an item" and stops there, because what a | |
| 200 | + | // bullet looks like is the renderer's answer. This is a terminal's. | |
| 201 | + | let out = drawn(&Node::rich("- one\n- two"), 40, 4); | |
| 202 | + | assert_eq!(out[0], "- one"); | |
| 203 | + | assert_eq!(out[1], "- two"); | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | #[test] | |
| 207 | + | fn a_quote_is_marked_in_the_margin_and_only_on_its_first_line() { | |
| 208 | + | // A marker belongs at the head of a line and nowhere else. A run knows its | |
| 209 | + | // block but not its position, so the marker is placed off the break the | |
| 210 | + | // previous run ended with. | |
| 211 | + | let out = drawn(&Node::rich("> quoted\n\nafter"), 40, 4); | |
| 212 | + | assert_eq!(out[0], "> quoted"); | |
| 213 | + | assert_eq!(out[2], "after"); | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | #[test] | |
| 217 | + | fn emphasis_inside_a_heading_is_added_to_its_weight_rather_than_swapped_for_it() { | |
| 218 | + | // The order a stylesheet uses: the block decides the ground and the marks | |
| 219 | + | // go on top. A struck word in a heading is struck AND a heading. | |
| 220 | + | let buf = buffer(&Node::rich("## Ship ~~later~~"), 40, 2); | |
| 221 | + | assert_eq!(rows(&buf)[0], "Ship later"); | |
| 222 | + | assert_eq!(marked(&buf, 0, Modifier::BOLD), "Ship later"); | |
| 223 | + | assert_eq!(marked(&buf, 0, Modifier::CROSSED_OUT), "later"); | |
| 224 | + | } | |
| 225 | + | ||
| 171 | 226 | #[test] | |
| 172 | 227 | fn a_rich_block_keeps_the_breaks_the_author_wrote() { | |
| 173 | 228 | // The reason a rich node cannot go through `draw_line`: that one wraps a |