Skip to main content

rich_ext/document/
render.rs

1//! Drawing the document model with core's renderables, in the look of core's
2//! Markdown: the same `markdown.*` theme styles, list markers, quote bar,
3//! code panels, table boxes and blank-line spacing, so an Org file, an
4//! AsciiDoc file and a man page read like a Markdown file does.
5
6use 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
23/// The placeholder core's Markdown puts in front of an image.
24const IMAGE_MARKER: &str = "\u{1f306} ";
25/// Core Markdown's bullet.
26const BULLET: &str = " \u{2022} ";
27/// Core Markdown's block-quote bar.
28const QUOTE_PREFIX: &str = "\u{258c} ";
29/// How far a definition's body is indented under its term.
30const DEFINITION_INDENT: usize = 4;
31/// Containers nest at most this deep on screen; deeper content keeps the
32/// last indentation, so a hostile document cannot squeeze text to nothing.
33///
34/// Every container re-processes each line of what it holds, so the cost of
35/// a nest grows with its depth squared. Depth is therefore weighted by what
36/// a level costs: a list item or indent counts 1, a quote (which restyles
37/// its content) 2, and a panel 4, so at most six panels nest.
38const MAX_DEPTH: usize = 24;
39const QUOTE_DEPTH: usize = 2;
40const PANEL_DEPTH: usize = 4;
41/// Nor does nesting indent content narrower than this many cells.
42const MIN_CONTENT: usize = 12;
43
44/// What a document's code, links and hyperlinks look like when drawn.
45///
46/// The defaults match core's `Markdown`: hyperlinks on (a link's text is an
47/// OSC 8 link), code in `Syntax`'s default theme (or the console's code
48/// highlighting), and no fence renderers.
49#[derive(Clone)]
50#[must_use = "options do nothing until they are passed to what takes them"]
51pub struct RenderOptions {
52    /// `true`: a link's text carries an OSC 8 hyperlink. `false`: the target
53    /// is written out after the text, `text (https://…)`, as `rich -m` does
54    /// without `-y`.
55    pub hyperlinks: bool,
56    /// The `Syntax` theme for code blocks; `None` keeps the console's.
57    pub code_theme: Option<String>,
58    /// The language of code blocks that name none (a man page's examples,
59    /// an AsciiDoc listing). `None` draws them as plain text.
60    pub default_language: Option<String>,
61    /// Renderers asked first for code blocks with a language (Mermaid, DOT),
62    /// as `Markdown::fence_renderer` asks them for fences.
63    pub fences: Vec<Arc<dyn FenceRenderer>>,
64    /// The justification of paragraphs; `None` is left.
65    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    /// The defaults: hyperlinks on, the console's code theme.
94    pub fn new() -> Self {
95        RenderOptions::default()
96    }
97
98    /// Whether links carry OSC 8 hyperlinks (see [`hyperlinks`](Self::hyperlinks)).
99    pub fn hyperlinks(mut self, hyperlinks: bool) -> Self {
100        self.hyperlinks = hyperlinks;
101        self
102    }
103
104    /// The `Syntax` theme for code blocks.
105    pub fn code_theme(mut self, theme: impl Into<String>) -> Self {
106        self.code_theme = Some(theme.into());
107        self
108    }
109
110    /// The language of code blocks that name none.
111    pub fn default_language(mut self, language: impl Into<String>) -> Self {
112        self.default_language = Some(language.into());
113        self
114    }
115
116    /// Ask `renderer` first for code blocks with a language.
117    pub fn fence_renderer(mut self, renderer: Arc<dyn FenceRenderer>) -> Self {
118        self.fences.push(renderer);
119        self
120    }
121
122    /// Justify paragraphs.
123    pub fn justify(mut self, justify: Justify) -> Self {
124        self.justify = Some(justify);
125        self
126    }
127}
128
129/// A parsed document, ready to print. Build one from source with
130/// [`org`](Self::org), [`asciidoc`](Self::asciidoc) or [`man`](Self::man),
131/// or from a [`Document`] built by hand with [`new`](Self::new).
132///
133/// ```
134/// use rich::Console;
135/// use rich_ext::document::DocumentView;
136///
137/// let console = Console::builder().width(40).color_system(None).build();
138/// let view = DocumentView::org("* TODO Write the report :work:\nSome /italic/ text.");
139/// let out = console.render_to_string(&view);
140/// assert!(out.contains("TODO Write the report"));
141/// assert!(out.contains("Some italic text."));
142/// ```
143#[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    /// Draw `document` with the default [`RenderOptions`].
153    pub fn new(document: Document) -> Self {
154        DocumentView {
155            document,
156            diagnostics: Vec::new(),
157            options: RenderOptions::default(),
158        }
159    }
160
161    /// Draw what a parser returned, keeping its diagnostics.
162    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    /// Parse and draw an Org document.
171    pub fn org(source: &str) -> Self {
172        DocumentView::from_parsed(super::org::parse(source))
173    }
174
175    /// Parse and draw an AsciiDoc document.
176    pub fn asciidoc(source: &str) -> Self {
177        DocumentView::from_parsed(super::asciidoc::parse(source))
178    }
179
180    /// Parse and draw a man page (man or mdoc macros).
181    pub fn man(source: &str) -> Self {
182        DocumentView::from_parsed(super::man::parse(source))
183    }
184
185    /// Replace every render option.
186    pub fn options(mut self, options: RenderOptions) -> Self {
187        self.options = options;
188        self
189    }
190
191    /// Whether links carry OSC 8 hyperlinks (see [`RenderOptions::hyperlinks`]).
192    pub fn hyperlinks(mut self, hyperlinks: bool) -> Self {
193        self.options.hyperlinks = hyperlinks;
194        self
195    }
196
197    /// The `Syntax` theme for code blocks.
198    pub fn code_theme(mut self, theme: impl Into<String>) -> Self {
199        self.options.code_theme = Some(theme.into());
200        self
201    }
202
203    /// The language of code blocks that name none.
204    pub fn default_language(mut self, language: impl Into<String>) -> Self {
205        self.options.default_language = Some(language.into());
206        self
207    }
208
209    /// Ask `renderer` first for code blocks with a language.
210    pub fn fence_renderer(mut self, renderer: Arc<dyn FenceRenderer>) -> Self {
211        self.options.fences.push(renderer);
212        self
213    }
214
215    /// The parsed document.
216    pub fn document(&self) -> &Document {
217        &self.document
218    }
219
220    /// What the parser could not show as written.
221    pub fn diagnostics(&self) -> &[Diagnostic] {
222        &self.diagnostics
223    }
224
225    /// The render options.
226    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
243/// Blocks inside a panel, which needs an owned renderable for its body.
244struct 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
267/// Rows joined with line breaks; lines are separated, not terminated.
268fn 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
280/// Pad every row out to `width`, as core's Markdown pads list items and
281/// quotes, so nested content keeps its column.
282fn 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/// Where a run of blocks sits, which decides the blank lines between them.
292#[derive(Clone, Copy, PartialEq, Eq)]
293enum Container {
294    /// The document, a quote, an indent or a definition's body: a blank
295    /// line between blocks.
296    Top,
297    /// Inside a panel.
298    Boxed,
299    /// A list item: tight, except between two paragraphs.
300    Item,
301}
302
303struct Painter<'a> {
304    console: &'a Console,
305    options: &'a ConsoleOptions,
306    doc: &'a RenderOptions,
307}
308
309/// `outer` with `inner` layered over it.
310fn 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    /// A theme style: the console theme's, else this module's default, else
319    /// core Markdown's default, else `name` parsed as a style definition.
320    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    /// Whether `block` follows `previous` with no blank line between.
364    fn joins(&self, previous: &Block, block: &Block, container: Container) -> bool {
365        // A byline and a heading's drawers and planning lines stay with the
366        // heading, and a caption with what it names.
367        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                // The panel pads its title with a space each side.
423                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    /// Wrap `text` to `width`, as `Text.__rich_console__` does: its own
456    /// justify, overflow and no-wrap win, then the options'.
457    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    /// `inlines` as one `Text`, every run layered over `base`.
514    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    /// A link as core's Markdown draws one: with hyperlinks, the text in
562    /// `markdown.link_url` carrying the OSC 8 target; without, the text in
563    /// `markdown.link` and the target written out after it. A link whose
564    /// text is its target (a bare URL) shows the target once.
565    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    /// Core Markdown's image placeholder: the marker, then the alt text
590    /// (or the target's last path component), linked to the target when
591    /// hyperlinks are on.
592    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            // Bullets sit left in their column, numbers right, as columns
620            // of numbers do.
621            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    /// One list item: its blocks beside its marker, continuation rows
642    /// indented to the text.
643    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        // Core's Markdown draws quote content four cells narrower.
743        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                // Callout columns are counted in the tab-expanded code.
820                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        // As core's Markdown builds its tables.
867        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    /// Drawers, planning lines and keywords: one dim, wrapped line.
905    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    /// A man page's running header: left, centre and right on one row; the
936    /// centre part is dropped first, then the right, when the row is narrow.
937    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            // Centre the middle part on the row, as far as the sides allow.
942            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
958/// The marker text of each item of `list`, before alignment.
959fn 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
982/// The `Syntax` name for a language as Org and AsciiDoc spell it.
983fn 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
998/// Core Markdown's defaults for the `markdown.*` keys this module reads,
999/// for a console whose theme lacks them.
1000const 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        // Numbers right-align in their column, as core's Markdown aligns
1099        // them.
1100        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}