1use std::sync::Arc;
7
8use rich::r#box::{ROUNDED, SIMPLE};
9use rich::cells::cell_len;
10use rich::protocol::{RegionInfo, RegionRole, enter_region};
11use rich::table::Table;
12use rich::{
13 Console, ConsoleOptions, FenceRenderer, Justify, Overflow, Panel, Renderable, Segment, Style,
14 StyleType, Syntax, Text,
15};
16
17use super::model::{
18 Align, Block, Checkbox, CodeBlock, Definition, Document, Heading, Inline, List, ListItem,
19 ListKind, TableBlock, inlines_text,
20};
21use super::{Diagnostic, Parsed, STYLES};
22
23const IMAGE_MARKER: &str = "\u{1f306} ";
25const BULLET: &str = " \u{2022} ";
27const QUOTE_PREFIX: &str = "\u{258c} ";
29const DEFINITION_INDENT: usize = 4;
31const MAX_DEPTH: usize = 24;
39const QUOTE_DEPTH: usize = 2;
40const PANEL_DEPTH: usize = 4;
41const MIN_CONTENT: usize = 12;
43
44#[derive(Clone)]
50#[must_use = "options do nothing until they are passed to what takes them"]
51pub struct RenderOptions {
52 pub hyperlinks: bool,
56 pub code_theme: Option<String>,
58 pub default_language: Option<String>,
61 pub fences: Vec<Arc<dyn FenceRenderer>>,
64 pub justify: Option<Justify>,
66}
67
68impl Default for RenderOptions {
69 fn default() -> Self {
70 RenderOptions {
71 hyperlinks: true,
72 code_theme: None,
73 default_language: None,
74 fences: Vec::new(),
75 justify: None,
76 }
77 }
78}
79
80impl std::fmt::Debug for RenderOptions {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("RenderOptions")
83 .field("hyperlinks", &self.hyperlinks)
84 .field("code_theme", &self.code_theme)
85 .field("default_language", &self.default_language)
86 .field("fences", &self.fences.len())
87 .field("justify", &self.justify)
88 .finish()
89 }
90}
91
92impl RenderOptions {
93 pub fn new() -> Self {
95 RenderOptions::default()
96 }
97
98 pub fn hyperlinks(mut self, hyperlinks: bool) -> Self {
100 self.hyperlinks = hyperlinks;
101 self
102 }
103
104 pub fn code_theme(mut self, theme: impl Into<String>) -> Self {
106 self.code_theme = Some(theme.into());
107 self
108 }
109
110 pub fn default_language(mut self, language: impl Into<String>) -> Self {
112 self.default_language = Some(language.into());
113 self
114 }
115
116 pub fn fence_renderer(mut self, renderer: Arc<dyn FenceRenderer>) -> Self {
118 self.fences.push(renderer);
119 self
120 }
121
122 pub fn justify(mut self, justify: Justify) -> Self {
124 self.justify = Some(justify);
125 self
126 }
127}
128
129#[derive(Clone, Debug)]
144#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
145pub struct DocumentView {
146 document: Document,
147 diagnostics: Vec<Diagnostic>,
148 options: RenderOptions,
149}
150
151impl DocumentView {
152 pub fn new(document: Document) -> Self {
154 DocumentView {
155 document,
156 diagnostics: Vec::new(),
157 options: RenderOptions::default(),
158 }
159 }
160
161 pub fn from_parsed(parsed: Parsed) -> Self {
163 DocumentView {
164 document: parsed.document,
165 diagnostics: parsed.diagnostics,
166 options: RenderOptions::default(),
167 }
168 }
169
170 pub fn org(source: &str) -> Self {
172 DocumentView::from_parsed(super::org::parse(source))
173 }
174
175 pub fn asciidoc(source: &str) -> Self {
177 DocumentView::from_parsed(super::asciidoc::parse(source))
178 }
179
180 pub fn man(source: &str) -> Self {
182 DocumentView::from_parsed(super::man::parse(source))
183 }
184
185 pub fn options(mut self, options: RenderOptions) -> Self {
187 self.options = options;
188 self
189 }
190
191 pub fn hyperlinks(mut self, hyperlinks: bool) -> Self {
193 self.options.hyperlinks = hyperlinks;
194 self
195 }
196
197 pub fn code_theme(mut self, theme: impl Into<String>) -> Self {
199 self.options.code_theme = Some(theme.into());
200 self
201 }
202
203 pub fn default_language(mut self, language: impl Into<String>) -> Self {
205 self.options.default_language = Some(language.into());
206 self
207 }
208
209 pub fn fence_renderer(mut self, renderer: Arc<dyn FenceRenderer>) -> Self {
211 self.options.fences.push(renderer);
212 self
213 }
214
215 pub fn document(&self) -> &Document {
217 &self.document
218 }
219
220 pub fn diagnostics(&self) -> &[Diagnostic] {
222 &self.diagnostics
223 }
224
225 pub fn render_options(&self) -> &RenderOptions {
227 &self.options
228 }
229}
230
231impl Renderable for DocumentView {
232 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
233 let painter = Painter {
234 console,
235 options,
236 doc: &self.options,
237 };
238 let lines = painter.blocks(&self.document.blocks, options.max_width, Container::Top, 0);
239 join_lines(lines)
240 }
241}
242
243struct Nested {
245 blocks: Vec<Block>,
246 options: RenderOptions,
247 depth: usize,
248}
249
250impl Renderable for Nested {
251 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
252 let painter = Painter {
253 console,
254 options,
255 doc: &self.options,
256 };
257 let lines = painter.blocks(
258 &self.blocks,
259 options.max_width,
260 Container::Boxed,
261 self.depth,
262 );
263 join_lines(lines)
264 }
265}
266
267fn join_lines(lines: Vec<Vec<Segment>>) -> Vec<Segment> {
269 let mut segments = Vec::new();
270 let last = lines.len().saturating_sub(1);
271 for (index, line) in lines.into_iter().enumerate() {
272 segments.extend(line);
273 if index != last {
274 segments.push(Segment::line());
275 }
276 }
277 segments
278}
279
280fn pad_lines(lines: &mut [Vec<Segment>], width: usize) {
283 for line in lines.iter_mut() {
284 let len: usize = line.iter().map(Segment::cell_length).sum();
285 if len < width {
286 line.push(Segment::new(" ".repeat(width - len), None));
287 }
288 }
289}
290
291#[derive(Clone, Copy, PartialEq, Eq)]
293enum Container {
294 Top,
297 Boxed,
299 Item,
301}
302
303struct Painter<'a> {
304 console: &'a Console,
305 options: &'a ConsoleOptions,
306 doc: &'a RenderOptions,
307}
308
309fn layer(outer: Option<&Style>, inner: Style) -> Style {
311 match outer {
312 Some(outer) => outer.combine(&inner),
313 None => inner,
314 }
315}
316
317impl Painter<'_> {
318 fn style(&self, name: &str) -> Style {
321 if let Some(style) = self.console.theme().get(name) {
322 return style.clone();
323 }
324 STYLES
325 .iter()
326 .chain(MARKDOWN_STYLES)
327 .find(|(key, _)| *key == name)
328 .and_then(|(_, spec)| Style::parse(spec).ok())
329 .or_else(|| {
330 self.console
331 .get_style(&StyleType::Name(name.to_string()))
332 .ok()
333 })
334 .unwrap_or_default()
335 }
336
337 fn blocks(
338 &self,
339 blocks: &[Block],
340 width: usize,
341 container: Container,
342 depth: usize,
343 ) -> Vec<Vec<Segment>> {
344 let width = width.max(1);
345 let mut out: Vec<Vec<Segment>> = Vec::new();
346 let mut previous: Option<&Block> = None;
347 for block in blocks {
348 let lines = self.block(block, width, depth);
349 if lines.is_empty() {
350 continue;
351 }
352 if let Some(previous) = previous
353 && !self.joins(previous, block, container)
354 {
355 out.push(Vec::new());
356 }
357 out.extend(lines);
358 previous = Some(block);
359 }
360 out
361 }
362
363 fn joins(&self, previous: &Block, block: &Block, container: Container) -> bool {
365 let attached = match block {
368 Block::Byline(_) => matches!(previous, Block::Heading(_) | Block::Byline(_)),
369 Block::Properties { .. } => {
370 matches!(previous, Block::Heading(_) | Block::Properties { .. })
371 }
372 _ => false,
373 };
374 if attached || matches!(previous, Block::Caption(_)) {
375 return true;
376 }
377 container == Container::Item
378 && !(matches!(previous, Block::Paragraph(_)) && matches!(block, Block::Paragraph(_)))
379 }
380
381 fn block(&self, block: &Block, width: usize, depth: usize) -> Vec<Vec<Segment>> {
382 match block {
383 Block::Heading(heading) => self.heading(heading, width),
384 Block::Byline(inlines) => {
385 let style = self.style("document.byline");
386 let mut text = self.inlines(inlines, Some(&style));
387 text.set_justify(Justify::Center);
388 self.text_lines(&text, width)
389 }
390 Block::Paragraph(inlines) => {
391 let mut text = self.inlines(inlines, None);
392 text.set_justify(self.doc.justify.unwrap_or(Justify::Left));
393 self.text_lines(&text, width)
394 }
395 Block::Caption(inlines) => {
396 let style = self.style("document.caption");
397 let text = self.inlines(inlines, Some(&style));
398 self.text_lines(&text, width)
399 }
400 Block::Lines(lines) => {
401 let mut out = Vec::new();
402 for line in lines {
403 let text = self.inlines(line, None);
404 out.extend(self.text_lines(&text, width));
405 }
406 out
407 }
408 Block::List(list) => self.list(list, width, depth),
409 Block::Definitions(definitions) => self.definitions(definitions, width, depth),
410 Block::Code(code) => self.code(code, width),
411 Block::Table(table) => self.table(table, width),
412 Block::Quote {
413 blocks,
414 attribution,
415 } => self.quote(blocks, attribution.as_deref(), width, depth),
416 Block::Admonition {
417 kind,
418 title,
419 blocks,
420 } => {
421 let style = self.style(kind.style_key());
422 let mut label = Text::styled(kind.label(), style.clone());
424 if let Some(title) = title {
425 label.append(": ", Some(style.clone().into()));
426 label = label.append_text(&self.inlines(title, Some(&style)));
427 }
428 self.panel(blocks, Some(label), style, width, depth)
429 }
430 Block::Aside { title, blocks } => {
431 let style = self.style("document.aside");
432 let label = title.as_ref().map(|title| self.inlines(title, None));
433 self.panel(blocks, label, style, width, depth)
434 }
435 Block::Indent { amount, blocks } => {
436 self.indented(blocks, *amount, width, depth, Container::Top)
437 }
438 Block::Rule => {
439 let style = self.style("markdown.hr");
440 vec![vec![Segment::new("-".repeat(width), Some(style))]]
441 }
442 Block::Image { target, alt } => {
443 let text = self.image(target, alt, None);
444 self.text_lines(&text, width)
445 }
446 Block::Properties { name, entries } => self.properties(name.as_deref(), entries, width),
447 Block::Banner {
448 left,
449 center,
450 right,
451 } => self.banner(left, center, right, width),
452 }
453 }
454
455 fn text_lines(&self, text: &Text, width: usize) -> Vec<Vec<Segment>> {
458 let justify = text.get_justify_option().unwrap_or(self.options.justify);
459 text.render_lines_wrapped_tabs(
460 self.console.theme(),
461 self.console.base_style(),
462 Some(width.max(1)),
463 justify,
464 text.get_overflow()
465 .or(self.options.overflow)
466 .unwrap_or(Overflow::Fold),
467 text.get_no_wrap().or(self.options.no_wrap).unwrap_or(false),
468 text.console_tab_size(self.console),
469 )
470 }
471
472 fn heading(&self, heading: &Heading, width: usize) -> Vec<Vec<Segment>> {
473 let level = heading.level.clamp(1, 6);
474 let style = self.style(&format!("markdown.h{level}"));
475 let mut text = Text::new("");
476 if let Some(keyword) = &heading.keyword {
477 let key = if keyword.done {
478 "document.done"
479 } else {
480 "document.todo"
481 };
482 text.append(&keyword.text, Some(style.combine(&self.style(key)).into()));
483 text.append(" ", Some(style.clone().into()));
484 }
485 if let Some(priority) = &heading.priority {
486 let priority_style = style.combine(&self.style("document.priority"));
487 text.append(&format!("[#{priority}]"), Some(priority_style.into()));
488 text.append(" ", Some(style.clone().into()));
489 }
490 self.append_inlines(&mut text, &heading.content, Some(&style));
491 if !heading.tags.is_empty() {
492 text.append(" ", None);
493 let tags = format!(":{}:", heading.tags.join(":"));
494 text.append(&tags, Some(self.style("document.tag").into()));
495 }
496 text.set_justify(if level == 1 {
497 Justify::Center
498 } else {
499 Justify::Left
500 });
501 let region = enter_region(self.console, || {
502 RegionInfo::new(RegionRole::Heading { level }).label(inlines_text(&heading.content))
503 });
504 let mut lines = self.text_lines(&text, width);
505 if let Some(region) = region {
506 for line in lines.iter_mut() {
507 region.tag(line);
508 }
509 }
510 lines
511 }
512
513 fn inlines(&self, inlines: &[Inline], base: Option<&Style>) -> Text {
515 let mut text = Text::new("");
516 self.append_inlines(&mut text, inlines, base);
517 text
518 }
519
520 fn append_inlines(&self, text: &mut Text, inlines: &[Inline], style: Option<&Style>) {
521 for inline in inlines {
522 match inline {
523 Inline::Text(content) => text.append(content, style.cloned().map(Into::into)),
524 Inline::Emphasis(content) => {
525 let inner = layer(style, self.style("markdown.em"));
526 self.append_inlines(text, content, Some(&inner));
527 }
528 Inline::Strong(content) => {
529 let inner = layer(style, self.style("markdown.strong"));
530 self.append_inlines(text, content, Some(&inner));
531 }
532 Inline::Underline(content) => {
533 let inner = layer(style, self.style("document.underline"));
534 self.append_inlines(text, content, Some(&inner));
535 }
536 Inline::Strike(content) => {
537 let inner = layer(style, self.style("markdown.s"));
538 self.append_inlines(text, content, Some(&inner));
539 }
540 Inline::Code(content) => {
541 let inner = layer(style, self.style("markdown.code"));
542 text.append(content, Some(inner.into()));
543 }
544 Inline::Link { target, content } => self.link(text, target, content, style),
545 Inline::Image { target, alt } => {
546 let image = self.image(target, alt, style);
547 *text = std::mem::take(text).append_text(&image);
548 }
549 Inline::LineBreak => text.append("\n", None),
550 Inline::Styled {
551 style: name,
552 content,
553 } => {
554 let inner = layer(style, self.style(name));
555 self.append_inlines(text, content, Some(&inner));
556 }
557 }
558 }
559 }
560
561 fn link(&self, text: &mut Text, target: &str, content: &[Inline], style: Option<&Style>) {
566 let url_style = layer(style, self.style("markdown.link_url"));
567 let shown = inlines_text(content);
568 let bare = content.is_empty() || shown == target;
569 if self.doc.hyperlinks {
570 let linked = url_style.with_link(target.to_string());
571 if bare {
572 text.append(target, Some(linked.into()));
573 } else {
574 self.append_inlines(text, content, Some(&linked));
575 }
576 return;
577 }
578 if bare {
579 text.append(target, Some(url_style.into()));
580 return;
581 }
582 let label_style = layer(style, self.style("markdown.link"));
583 self.append_inlines(text, content, Some(&label_style));
584 text.append(" (", style.cloned().map(Into::into));
585 text.append(target, Some(url_style.into()));
586 text.append(")", style.cloned().map(Into::into));
587 }
588
589 fn image(&self, target: &str, alt: &str, style: Option<&Style>) -> Text {
593 let title = if alt.trim().is_empty() {
594 let trimmed = target.trim_matches('/');
595 trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()
596 } else {
597 alt.to_string()
598 };
599 let mut title_style = style.cloned().unwrap_or_default();
600 if self.doc.hyperlinks && !target.is_empty() {
601 title_style = title_style.with_link(target.to_string());
602 }
603 let mut text = Text::new(IMAGE_MARKER);
604 text.append(&title, Some(title_style.into()));
605 text.append(" ", None);
606 text
607 }
608
609 fn list(&self, list: &List, width: usize, depth: usize) -> Vec<Vec<Segment>> {
610 let markers = list_markers(list);
611 let marker_width = markers.iter().map(|m| cell_len(m)).max().unwrap_or(0);
612 let marker_style = self.style(match list.kind {
613 ListKind::Bullet => "markdown.item.bullet",
614 ListKind::Ordered { .. } => "markdown.item.number",
615 ListKind::Callout => "document.callout",
616 });
617 let mut out = Vec::new();
618 for (item, marker) in list.items.iter().zip(markers) {
619 let marker = match list.kind {
622 ListKind::Bullet => format!("{marker:<marker_width$}"),
623 _ => format!("{marker:>marker_width$}"),
624 };
625 let mut prefix = vec![Segment::new(marker, Some(marker_style.clone()))];
626 let mut prefix_width = marker_width;
627 if let Some(checkbox) = item.checkbox {
628 let (glyph, key) = match checkbox {
629 Checkbox::Unchecked => ("[ ] ", "document.checkbox"),
630 Checkbox::Checked => ("[x] ", "document.checkbox.checked"),
631 Checkbox::Partial => ("[-] ", "document.checkbox.partial"),
632 };
633 prefix.push(Segment::new(glyph, Some(self.style(key))));
634 prefix_width += 4;
635 }
636 out.extend(self.item(item, prefix, prefix_width, width, depth));
637 }
638 out
639 }
640
641 fn item(
644 &self,
645 item: &ListItem,
646 prefix: Vec<Segment>,
647 prefix_width: usize,
648 width: usize,
649 depth: usize,
650 ) -> Vec<Vec<Segment>> {
651 let indent = depth < MAX_DEPTH && prefix_width + MIN_CONTENT <= width;
652 let content_width = if indent { width - prefix_width } else { width };
653 let mut lines: Vec<Vec<Segment>> = self
654 .blocks(&item.blocks, content_width, Container::Item, depth + 1)
655 .into_iter()
656 .skip_while(|line| line.is_empty())
657 .collect();
658 if lines.is_empty() {
659 lines.push(Vec::new());
660 }
661 if !indent {
662 let mut out = vec![prefix];
663 out.extend(lines);
664 return out;
665 }
666 pad_lines(&mut lines, content_width);
667 lines
668 .into_iter()
669 .enumerate()
670 .map(|(index, line)| {
671 let mut row = if index == 0 {
672 prefix.clone()
673 } else {
674 vec![Segment::new(" ".repeat(prefix_width), None)]
675 };
676 row.extend(line);
677 row
678 })
679 .collect()
680 }
681
682 fn definitions(
683 &self,
684 definitions: &[Definition],
685 width: usize,
686 depth: usize,
687 ) -> Vec<Vec<Segment>> {
688 let term_style = self.style("document.term");
689 let mut out = Vec::new();
690 for (index, definition) in definitions.iter().enumerate() {
691 if index > 0 {
692 out.push(Vec::new());
693 }
694 let term = self.inlines(&definition.term, Some(&term_style));
695 out.extend(self.text_lines(&term, width));
696 if !definition.blocks.is_empty() {
697 out.extend(self.indented(
698 &definition.blocks,
699 DEFINITION_INDENT,
700 width,
701 depth,
702 Container::Top,
703 ));
704 }
705 }
706 out
707 }
708
709 fn indented(
710 &self,
711 blocks: &[Block],
712 amount: usize,
713 width: usize,
714 depth: usize,
715 container: Container,
716 ) -> Vec<Vec<Segment>> {
717 let indent = depth < MAX_DEPTH && amount + MIN_CONTENT <= width;
718 if !indent {
719 return self.blocks(blocks, width, container, depth + 1);
720 }
721 let inner = width - amount;
722 let mut lines = self.blocks(blocks, inner, container, depth + 1);
723 pad_lines(&mut lines, inner);
724 lines
725 .into_iter()
726 .map(|line| {
727 let mut row = vec![Segment::new(" ".repeat(amount), None)];
728 row.extend(line);
729 row
730 })
731 .collect()
732 }
733
734 fn quote(
735 &self,
736 blocks: &[Block],
737 attribution: Option<&[Inline]>,
738 width: usize,
739 depth: usize,
740 ) -> Vec<Vec<Segment>> {
741 let style = self.style("markdown.block_quote");
742 let indent = depth + QUOTE_DEPTH <= MAX_DEPTH && 4 + MIN_CONTENT <= width;
744 let inner = if indent { width - 4 } else { width };
745 let mut lines: Vec<Vec<Segment>> = self
746 .blocks(blocks, inner, Container::Top, depth + QUOTE_DEPTH)
747 .into_iter()
748 .skip_while(|line| line.is_empty())
749 .collect();
750 if let Some(attribution) = attribution {
751 let attribution_style = self.style("document.attribution");
752 let mut text = Text::styled("\u{2014} ", attribution_style.clone());
753 text = text.append_text(&self.inlines(attribution, Some(&attribution_style)));
754 lines.extend(self.text_lines(&text, inner));
755 }
756 if !indent {
757 return lines;
758 }
759 pad_lines(&mut lines, inner);
760 lines
761 .into_iter()
762 .map(|line| {
763 let mut row = vec![Segment::new(QUOTE_PREFIX, Some(style.clone()))];
764 row.extend(Segment::apply_style(&line, &style));
765 row
766 })
767 .collect()
768 }
769
770 fn panel(
771 &self,
772 blocks: &[Block],
773 title: Option<Text>,
774 border: Style,
775 width: usize,
776 depth: usize,
777 ) -> Vec<Vec<Segment>> {
778 if depth + PANEL_DEPTH > MAX_DEPTH || width < 4 + MIN_CONTENT {
779 return self.blocks(blocks, width, Container::Boxed, depth + 1);
780 }
781 let body = Nested {
782 blocks: blocks.to_vec(),
783 options: self.doc.clone(),
784 depth: depth + PANEL_DEPTH,
785 };
786 let mut panel = Panel::new(Box::new(body))
787 .box_set(ROUNDED)
788 .border_style(border);
789 if let Some(title) = title {
790 panel = panel
791 .title_as_text(title)
792 .title_align(rich::HorizontalAlign::Left);
793 }
794 let inner = self.options.update_width(width);
795 Segment::split_lines(&panel.rich_render(self.console, &inner))
796 }
797
798 fn code(&self, code: &CodeBlock, width: usize) -> Vec<Vec<Segment>> {
799 let inner = self.options.update_width(width);
800 let language = code
801 .language
802 .as_deref()
803 .or(self.doc.default_language.as_deref())
804 .unwrap_or("");
805 let region = enter_region(self.console, || {
806 RegionInfo::new(RegionRole::Code).label(language)
807 });
808 let drawn = if language.is_empty() {
809 None
810 } else {
811 self.doc
812 .fences
813 .iter()
814 .find_map(|fence| fence.render_fence(language, &code.code, self.console, &inner))
815 };
816 let mut segments = match drawn {
817 Some(segments) => segments,
818 None => {
819 let source = if code.callouts.is_empty() {
821 code.code.clone()
822 } else {
823 super::scan::expand_tabs(&code.code)
824 };
825 let mut syntax = Syntax::new(source, lexer_name(language))
826 .word_wrap(true)
827 .padding(1);
828 if let Some(theme) = &self.doc.code_theme {
829 syntax = syntax.theme(theme.as_str());
830 }
831 let callout = self.style("document.callout");
832 for marker in &code.callouts {
833 let line = marker.line as i64 + 1;
834 syntax.stylize_range(
835 callout.clone(),
836 (line, marker.start as i64),
837 (line, marker.end as i64),
838 false,
839 );
840 }
841 syntax.rich_render(self.console, &inner)
842 }
843 };
844 if let Some(region) = region {
845 region.tag(&mut segments);
846 }
847 Segment::split_lines(&segments)
848 }
849
850 fn table(&self, table: &TableBlock, width: usize) -> Vec<Vec<Segment>> {
851 let columns = table
852 .header
853 .iter()
854 .chain(&table.rows)
855 .map(Vec::len)
856 .max()
857 .unwrap_or(0);
858 if columns == 0 {
859 return Vec::new();
860 }
861 let justify = |column: usize| match table.align.get(column) {
862 Some(Align::Center) => Justify::Center,
863 Some(Align::Right) => Justify::Right,
864 _ => Justify::Left,
865 };
866 let mut grid = Table::new()
868 .box_set(SIMPLE)
869 .pad_edge(false)
870 .collapse_padding(true)
871 .style(self.style("markdown.table.border"))
872 .show_header(table.header.is_some());
873 if let Some(title) = &table.title {
874 grid = grid.title_text(self.inlines(title, Some(&self.style("document.caption"))));
875 }
876 let header_style = self.style("markdown.table.header");
877 for column in 0..columns {
878 let header = table
879 .header
880 .as_ref()
881 .and_then(|header| header.get(column))
882 .map(|cell| self.inlines(cell, None))
883 .unwrap_or_else(|| Text::new(""));
884 grid.add_column_text(header, justify(column));
885 grid.column_header_style(header_style.clone());
886 }
887 for (index, row) in table.rows.iter().enumerate() {
888 if index > 0 && table.sections.contains(&index) {
889 grid.add_section();
890 }
891 let cells = (0..columns)
892 .map(|column| {
893 row.get(column)
894 .map(|cell| self.inlines(cell, None))
895 .unwrap_or_else(|| Text::new(""))
896 })
897 .collect();
898 grid.add_row_text(cells);
899 }
900 let inner = self.options.update_width(width);
901 Segment::split_lines(&grid.rich_render(self.console, &inner))
902 }
903
904 fn properties(
906 &self,
907 name: Option<&str>,
908 entries: &[(String, String)],
909 width: usize,
910 ) -> Vec<Vec<Segment>> {
911 let style = self.style("document.properties");
912 let key_style = style.combine(&self.style("document.properties.key"));
913 let mut text = Text::new("");
914 if let Some(name) = name {
915 text.append(name, Some(key_style.clone().into()));
916 if !entries.is_empty() {
917 text.append(" ", Some(style.clone().into()));
918 }
919 }
920 for (index, (key, value)) in entries.iter().enumerate() {
921 if index > 0 {
922 text.append(" \u{b7} ", Some(style.clone().into()));
923 }
924 if !key.is_empty() {
925 text.append(key, Some(key_style.clone().into()));
926 if !value.is_empty() {
927 text.append(": ", Some(style.clone().into()));
928 }
929 }
930 text.append(value, Some(style.clone().into()));
931 }
932 self.text_lines(&text, width)
933 }
934
935 fn banner(&self, left: &str, center: &str, right: &str, width: usize) -> Vec<Vec<Segment>> {
938 let style = self.style("document.banner");
939 let (l, c, r) = (cell_len(left), cell_len(center), cell_len(right));
940 let line = if l + c + r + 2 <= width {
941 let start = ((width - c) / 2).clamp(l + 1, width - r - 1 - c);
943 format!(
944 "{left}{}{center}{}{right}",
945 " ".repeat(start - l),
946 " ".repeat(width - r - start - c)
947 )
948 } else if l + r + 2 <= width {
949 format!("{left}{}{right}", " ".repeat(width - l - r))
950 } else {
951 left.to_string()
952 };
953 let text = Text::styled(line, style);
954 self.text_lines(&text, width)
955 }
956}
957
958fn list_markers(list: &List) -> Vec<String> {
960 let mut next = match list.kind {
961 ListKind::Ordered { start } => start,
962 _ => 1,
963 };
964 list.items
965 .iter()
966 .map(|item| match list.kind {
967 ListKind::Bullet => BULLET.to_string(),
968 ListKind::Ordered { .. } => {
969 let number = item.number.unwrap_or(next);
970 next = number.saturating_add(1);
971 format!(" {number} ")
972 }
973 ListKind::Callout => {
974 let number = item.number.unwrap_or(next);
975 next = number.saturating_add(1);
976 format!(" <{number}> ")
977 }
978 })
979 .collect()
980}
981
982fn lexer_name(language: &str) -> &str {
984 match language.to_ascii_lowercase().as_str() {
985 "emacs-lisp" | "elisp" | "lisp-interaction" => "lisp",
986 "shell" | "sh" | "zsh" | "console" | "shell-session" | "bash" => "bash",
987 "js" | "node" => "javascript",
988 "ts" => "typescript",
989 "py" | "python3" => "python",
990 "rb" => "ruby",
991 "c++" | "cxx" => "cpp",
992 "rs" => "rust",
993 "yml" => "yaml",
994 _ => language,
995 }
996}
997
998const MARKDOWN_STYLES: &[(&str, &str)] = &[
1001 ("markdown.em", "italic"),
1002 ("markdown.strong", "bold"),
1003 ("markdown.code", "bold cyan on black"),
1004 ("markdown.block_quote", "magenta"),
1005 ("markdown.item.bullet", "bold"),
1006 ("markdown.item.number", "cyan"),
1007 ("markdown.hr", "dim"),
1008 ("markdown.h1", "bold underline"),
1009 ("markdown.h2", "underline magenta"),
1010 ("markdown.h3", "bold magenta"),
1011 ("markdown.h4", "italic magenta"),
1012 ("markdown.h5", "italic"),
1013 ("markdown.h6", "dim"),
1014 ("markdown.link", "bright_blue"),
1015 ("markdown.link_url", "underline blue"),
1016 ("markdown.s", "strike"),
1017 ("markdown.table.border", "cyan"),
1018 ("markdown.table.header", "not bold cyan"),
1019];
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use crate::document::model::{AdmonitionKind, Keyword};
1025
1026 fn plain(document: Document, width: usize) -> String {
1027 let console = Console::builder().width(width).color_system(None).build();
1028 console.render_to_string(&DocumentView::new(document))
1029 }
1030
1031 fn para(text: &str) -> Block {
1032 Block::Paragraph(vec![Inline::text(text)])
1033 }
1034
1035 #[test]
1036 fn headings_paragraphs_and_spacing() {
1037 let document = Document {
1038 blocks: vec![
1039 Block::Heading(Heading {
1040 level: 1,
1041 content: vec![Inline::text("Title")],
1042 ..Heading::default()
1043 }),
1044 para("One."),
1045 Block::Heading(Heading {
1046 level: 2,
1047 content: vec![Inline::text("Part")],
1048 keyword: Some(Keyword {
1049 text: "TODO".into(),
1050 done: false,
1051 }),
1052 priority: Some("A".into()),
1053 tags: vec!["work".into()],
1054 }),
1055 ],
1056 };
1057 let out = plain(document, 30);
1058 let lines: Vec<&str> = out.lines().map(str::trim_end).collect();
1059 assert_eq!(
1060 lines,
1061 [
1062 " Title",
1063 "",
1064 "One.",
1065 "",
1066 "TODO [#A] Part :work:"
1067 ]
1068 );
1069 }
1070
1071 #[test]
1072 fn lists_nest_and_number() {
1073 let nested = Block::List(List {
1074 kind: ListKind::Ordered { start: 9 },
1075 items: vec![
1076 ListItem {
1077 blocks: vec![para("nine")],
1078 ..ListItem::default()
1079 },
1080 ListItem {
1081 checkbox: Some(Checkbox::Checked),
1082 blocks: vec![para("ten")],
1083 ..ListItem::default()
1084 },
1085 ],
1086 });
1087 let document = Document {
1088 blocks: vec![Block::List(List {
1089 kind: ListKind::Bullet,
1090 items: vec![ListItem {
1091 blocks: vec![para("a"), nested],
1092 ..ListItem::default()
1093 }],
1094 })],
1095 };
1096 let out = plain(document, 30);
1097 let lines: Vec<&str> = out.lines().map(str::trim_end).collect();
1098 assert_eq!(lines, [" • a", " 9 nine", " 10 [x] ten"]);
1101 }
1102
1103 #[test]
1104 fn links_write_their_target_without_hyperlinks() {
1105 let document = Document {
1106 blocks: vec![Block::Paragraph(vec![
1107 Inline::Link {
1108 target: "https://example.com".into(),
1109 content: vec![Inline::text("site")],
1110 },
1111 Inline::text(" and "),
1112 Inline::Link {
1113 target: "https://x.org".into(),
1114 content: vec![],
1115 },
1116 ])],
1117 };
1118 let console = Console::builder().width(60).color_system(None).build();
1119 let out = console.render_to_string(&DocumentView::new(document).hyperlinks(false));
1120 assert_eq!(
1121 out.trim_end(),
1122 "site (https://example.com) and https://x.org"
1123 );
1124 }
1125
1126 #[test]
1127 fn admonitions_are_titled_panels() {
1128 let document = Document {
1129 blocks: vec![Block::Admonition {
1130 kind: AdmonitionKind::Warning,
1131 title: None,
1132 blocks: vec![para("Careful.")],
1133 }],
1134 };
1135 let out = plain(document, 20);
1136 assert_eq!(
1137 out,
1138 "╭─ Warning ────────╮\n│ Careful. │\n╰──────────────────╯"
1139 );
1140 }
1141
1142 #[test]
1143 fn a_banner_spreads_over_the_row() {
1144 let document = Document {
1145 blocks: vec![Block::Banner {
1146 left: "LS(1)".into(),
1147 center: "User Commands".into(),
1148 right: "LS(1)".into(),
1149 }],
1150 };
1151 let out = plain(document, 40);
1152 assert_eq!(out.len(), 40, "{out:?}");
1153 assert!(out.starts_with("LS(1)") && out.ends_with("LS(1)"));
1154 assert!(out.contains("User Commands"));
1155 }
1156
1157 #[test]
1158 fn deep_nesting_stays_within_the_width() {
1159 let mut block = para("deep");
1160 for _ in 0..40 {
1161 block = Block::Quote {
1162 blocks: vec![block],
1163 attribution: None,
1164 };
1165 }
1166 let out = plain(
1167 Document {
1168 blocks: vec![block],
1169 },
1170 30,
1171 );
1172 assert!(out.contains("deep"), "{out}");
1173 assert!(out.lines().all(|line| rich::cells::cell_len(line) <= 30));
1174 }
1175}