Skip to main content

rich/
protocol.rs

1//! Rendering & extension protocols.
2//!
3//! Port of upstream `rich/protocol.py` + `rich/abc.py` + the highlighter
4//! interface. **These traits are the sanctioned extension points of the port.**
5//! Extensions in `rich-ext` (and, later, third-party plugins) implement them;
6//! the faithful core only ever ships upstream's built-in implementations. See
7//! docs/PLUGINS.md.
8
9use crate::console::{Console, ConsoleOptions};
10use crate::measure::Measurement;
11use crate::segment::Segment;
12use crate::text::Text;
13
14/// Anything that can be rendered to a stream of [`Segment`]s within a width.
15///
16/// The Rust equivalent of upstream's `__rich_console__(console, options)`
17/// protocol. Implement it to make a custom type printable by [`Console`]. The
18/// `options` carry the available width (and, later, height/justify) the
19/// renderable must fit into. Newlines between lines are emitted as ordinary
20/// segments containing `\n`.
21pub trait Renderable {
22    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment>;
23
24    /// The `(minimum, maximum)` cell width this renderable wants. Port of
25    /// `__rich_measure__`. The default is what upstream's `Measurement.get`
26    /// returns for a renderable without one: anything from nothing to the
27    /// whole width, `(0, max_width)`.
28    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
29        Measurement::new(0, options.max_width)
30    }
31
32    /// Whether a top-level `Console::print` shrinks this renderable to its
33    /// measured width. Upstream renders every top-level renderable at the full
34    /// console width, so the default is `false`; an extension may opt in.
35    fn fit_to_measurement(&self) -> bool {
36        false
37    }
38
39    /// The `Text` a top-level print renders in place of this renderable.
40    ///
41    /// Upstream's `Console._collect_renderables` rebuilds printed `str`/`Text`
42    /// values through `Text(sep, end=end).join(...)`, whose `blank_copy` takes
43    /// `justify`, `overflow` and `no_wrap` from the separator. Only `Text`
44    /// overrides this.
45    #[doc(hidden)]
46    fn printed_text(&self) -> Option<Text> {
47        None
48    }
49
50    /// The vertical alignment a `Table` cell holding this renderable uses in
51    /// place of its column's. Upstream reads `getattr(renderable, "vertical",
52    /// None)`; [`Align`](crate::align::Align) sets it.
53    fn vertical(&self) -> Option<crate::align::VerticalAlign> {
54        None
55    }
56
57    /// Whether a top-level print ends this renderable's last line. This
58    /// port's renderables *separate* lines, where upstream's end each line
59    /// with `"\n"`, so a print adds the final line break. A renderable
60    /// whose upstream output stops without one (`Screen`, `LiveRender`, a
61    /// `Rule` whose `end` has no newline) returns `false`, and the next
62    /// output continues its last line. `options` are the print's options.
63    fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
64        true
65    }
66}
67
68/// Optional line-streaming extension point for renderables.
69///
70/// Mirrors the incremental consumption of upstream's rendering generators.
71/// Consumers can write each visual line immediately instead of collecting the
72/// complete segment stream. Implementations may still retain source data for
73/// measurement. This trait keeps streaming hooks out of inherent core APIs.
74pub trait LineRenderable: Renderable {
75    /// Emit styled visual lines without trailing newlines, stopping immediately
76    /// on the callback's first error. An empty segment represents a blank line;
77    /// calling the callback zero times represents no output.
78    fn try_for_each_line<E>(
79        &self,
80        console: &Console,
81        options: &ConsoleOptions,
82        emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
83    ) -> Result<(), E>;
84}
85
86/// Transfer already-owned table rows without cloning every cell string.
87///
88/// This extension point changes ownership only. Column definitions, measurement
89/// and rendering follow the table's existing rules, including missing/extra cells.
90/// Producers that parse into owned strings can release their row collection as
91/// they populate a table instead of retaining a second complete copy.
92pub trait OwnedTableRows {
93    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self;
94}
95
96/// A transformer that adds style spans to [`Text`] (e.g. syntax/number/URL
97/// highlighting). The Rust equivalent of upstream's `Highlighter` ABC.
98///
99/// This is the primary *plugin* seam for the first slice: `rich-ext` registers
100/// [`Highlighter`]s onto a [`Console`] without the core knowing they exist.
101pub trait Highlighter {
102    /// Inspect `text` and apply any style spans in place.
103    fn highlight(&self, text: &mut Text);
104
105    /// A highlighted copy of `text`, leaving `text` itself alone. Port of
106    /// `Highlighter.__call__` (a `str` is `Text::new(…)` first).
107    fn highlighted(&self, text: &Text) -> Text {
108        let mut copy = text.clone();
109        self.highlight(&mut copy);
110        copy
111    }
112}
113
114/// One styled byte range of a highlighted line.
115#[derive(Clone, Debug, PartialEq)]
116pub struct HighlightSpan {
117    /// Byte range within the line (the line's text, without its `\n`).
118    pub range: std::ops::Range<usize>,
119    pub style: crate::style::Style,
120}
121
122/// The spans of one source line, plus the style of the line break after it.
123#[derive(Clone, Debug, Default, PartialEq)]
124pub struct HighlightedLine {
125    /// Sorted, non-overlapping spans. Bytes no span covers take
126    /// [`HighlightedCode::default_style`].
127    pub spans: Vec<HighlightSpan>,
128    /// The style an engine gives the `\n` ending this line (for example, inside a
129    /// multi-line string). `None` when the engine does not style line breaks.
130    pub newline_style: Option<crate::style::Style>,
131}
132
133/// What a [`CodeHighlighter`] returns: one [`HighlightedLine`] per element of
134/// `code.split('\n')`, so a trailing newline yields a final empty line.
135#[derive(Clone, Debug, Default, PartialEq)]
136pub struct HighlightedCode {
137    pub lines: Vec<HighlightedLine>,
138    /// The theme's background, if it has one. `Syntax` paints its block with it.
139    pub background: Option<crate::color::Color>,
140    /// The style for text no span covers.
141    pub default_style: crate::style::Style,
142}
143
144/// Why a [`CodeHighlighter`] could not highlight. An unknown *language* is not an
145/// error: highlighters fall back to plain text.
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub enum HighlightError {
148    /// The theme name is not one this highlighter provides.
149    UnknownTheme(String),
150    /// The engine failed on this input.
151    Engine(String),
152}
153
154impl std::fmt::Display for HighlightError {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            HighlightError::UnknownTheme(name) => write!(f, "unknown syntax theme {name:?}"),
158            HighlightError::Engine(message) => write!(f, "syntax highlighting failed: {message}"),
159        }
160    }
161}
162
163impl std::error::Error for HighlightError {}
164
165/// A syntax-highlighting engine behind [`Syntax`](crate::syntax::Syntax) and
166/// Markdown code blocks.
167///
168/// Upstream highlights with Pygments; the port's default is
169/// [`SyntectHighlighter`](crate::syntax::SyntectHighlighter), and anything that
170/// implements this trait can replace it (see [`Syntax::highlighter`](crate::syntax::Syntax::highlighter)).
171///
172/// # Contract
173///
174/// - `lines` has one entry per element of `code.split('\n')`.
175/// - Spans are sorted, do not overlap, stay inside their line and start and end
176///   on UTF-8 character boundaries.
177/// - An unknown language highlights as plain text rather than failing.
178///
179/// Core validates what an implementation returns: out-of-range, overlapping or
180/// misaligned spans are dropped, missing lines render unstyled, and span styles
181/// lose any hyperlink. The rendered characters always come from the source, so
182/// a highlighter cannot add text or terminal control sequences.
183///
184/// ```
185/// use rich::protocol::{CodeHighlighter, HighlightError, HighlightSpan, HighlightedCode, HighlightedLine};
186/// use rich::Style;
187///
188/// /// Makes every line bold.
189/// struct Bold;
190///
191/// impl CodeHighlighter for Bold {
192///     fn highlight(&self, code: &str, _language: Option<&str>, _theme: &str)
193///         -> Result<HighlightedCode, HighlightError>
194///     {
195///         let bold = Style::parse("bold").unwrap();
196///         let lines = code
197///             .split('\n')
198///             .map(|line| HighlightedLine {
199///                 spans: vec![HighlightSpan { range: 0..line.len(), style: bold.clone() }]
200///                     .into_iter()
201///                     .filter(|span| !span.range.is_empty())
202///                     .collect(),
203///                 newline_style: None,
204///             })
205///             .collect();
206///         Ok(HighlightedCode { lines, ..Default::default() })
207///     }
208///     fn default_theme(&self) -> &str { "bold" }
209///     fn themes(&self) -> Vec<String> { vec!["bold".into()] }
210///     fn languages(&self) -> Vec<String> { Vec::new() }
211/// }
212///
213/// # #[cfg(feature = "code")] {
214/// use rich::{Console, Syntax};
215/// use std::sync::Arc;
216///
217/// let console = Console::builder().width(20).force_terminal(true).build();
218/// let out = console.render_to_string(&Syntax::new("x = 1", "python").highlighter(Arc::new(Bold)));
219/// assert!(out.contains("\x1b[1mx = 1"));
220/// # }
221/// ```
222pub trait CodeHighlighter: Send + Sync {
223    /// Highlight `code`. `language` is a name or file extension (`"rust"`,
224    /// `"rs"`); `None` means plain text. `theme` is one of [`themes`](Self::themes).
225    fn highlight(
226        &self,
227        code: &str,
228        language: Option<&str>,
229        theme: &str,
230    ) -> Result<HighlightedCode, HighlightError>;
231
232    /// The theme used when none is chosen.
233    fn default_theme(&self) -> &str;
234
235    /// Every theme name `highlight` accepts.
236    fn themes(&self) -> Vec<String>;
237
238    /// Language names this highlighter knows, for help text and completion.
239    fn languages(&self) -> Vec<String>;
240
241    /// The language for a file path, if the highlighter recognises it.
242    fn language_for_path(&self, _path: &std::path::Path) -> Option<String> {
243        None
244    }
245
246    /// `theme`'s style for a Pygments token type — `"Text"` or `"Comment"` —
247    /// as upstream's `SyntaxTheme.get_style_for_token`. `Syntax` colours its
248    /// line numbers and indent guides with it; `None` (the default) means the
249    /// theme sets nothing for the token.
250    fn token_style(&self, _theme: &str, _token: &str) -> Option<crate::style::Style> {
251        None
252    }
253}
254
255/// Renders fenced Markdown code blocks of particular languages (for example
256/// ```` ```mermaid ````) in place of the usual highlighted code.
257///
258/// Upstream always renders a fence through `Syntax`, and so does
259/// [`Markdown`](crate::markdown::Markdown) unless a renderer is added with
260/// [`Markdown::fence_renderer`](crate::markdown::Markdown::fence_renderer).
261/// Markdown asks each renderer in turn; the first to return `Some` wins, and if
262/// none does the block is highlighted as code as before.
263///
264/// The fence body comes from the document, so treat it as untrusted: whatever
265/// text of it an implementation echoes must not carry terminal control
266/// sequences.
267pub trait FenceRenderer: Send + Sync {
268    /// Render the body of a fence whose info string starts with `language`, or
269    /// return `None` to decline. The returned segments fit `options.max_width`
270    /// and separate lines with `\n` segments, like [`Renderable::rich_render`].
271    fn render_fence(
272        &self,
273        language: &str,
274        code: &str,
275        console: &Console,
276        options: &ConsoleOptions,
277    ) -> Option<Vec<Segment>>;
278}
279
280/// A console-wide default [`CodeHighlighter`] and the theme to use with it.
281/// See [`ConsoleCodeHighlighting`].
282#[derive(Clone)]
283pub struct CodeHighlighting {
284    pub highlighter: std::sync::Arc<dyn CodeHighlighter>,
285    /// One of `highlighter`'s themes; `None` is its default theme.
286    pub theme: Option<String>,
287}
288
289impl std::fmt::Debug for CodeHighlighting {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        f.debug_struct("CodeHighlighting")
292            .field("default_theme", &self.highlighter.default_theme())
293            .field("theme", &self.theme)
294            .finish()
295    }
296}
297
298/// Attach/query a console's default code highlighter.
299///
300/// A [`Syntax`](crate::syntax::Syntax) without a highlighter of its own, and
301/// so Markdown code blocks, highlights with the console's when it renders;
302/// the console's theme applies when the `Syntax` names none. Without one,
303/// the default `SyntectHighlighter` is used, as before: upstream has no such
304/// setting. In a build with core's `code` feature but not `syntax` there is
305/// no default engine, and this is how code gets highlighted at all; without
306/// one, code renders plain.
307pub trait ConsoleCodeHighlighting {
308    fn set_code_highlighting(&mut self, value: Option<CodeHighlighting>);
309    fn code_highlighting(&self) -> Option<&CodeHighlighting>;
310}
311
312/// Evidence for an optional output protocol; inference is not confirmation.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum Support {
315    Unsupported,
316    Inferred,
317    Confirmed,
318}
319
320/// Immutable destination capabilities supplied by an extension. No detection or I/O.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct TargetCapabilities {
323    pub width: usize,
324    pub height: usize,
325    pub color_system: Option<crate::color::ColorSystem>,
326    pub interactive: bool,
327    pub unicode: bool,
328    pub hyperlinks: bool,
329    pub sixel: Support,
330}
331
332/// Optional context shared by nested renderables without changing their protocol.
333pub trait RenderEnvironment: Send + Sync {
334    fn capabilities(&self) -> TargetCapabilities;
335}
336
337/// Attach/query a per-console immutable extension environment.
338pub trait ConsoleEnvironment {
339    fn set_render_environment(&mut self, value: Option<std::sync::Arc<dyn RenderEnvironment>>);
340    fn render_environment(&self) -> Option<&dyn RenderEnvironment>;
341}
342
343/// What a semantic region is: the role a renderable reports for the cells it
344/// drew. See [`RegionSink`].
345#[derive(Clone, Debug, PartialEq, Eq)]
346#[non_exhaustive]
347pub enum RegionRole {
348    /// A [`Panel`](crate::panel::Panel): its border, padding and content.
349    Panel,
350    /// A [`Table`](crate::table::Table), title and caption included.
351    Table,
352    /// A header cell of column `column` (from 0).
353    TableHeader { column: usize },
354    /// A body cell: row `row` of the table's rows, column `column` (from 0).
355    TableCell { row: usize, column: usize },
356    /// A footer cell of column `column` (from 0).
357    TableFooter { column: usize },
358    /// A [`Rule`](crate::rule::Rule).
359    Rule,
360    /// A Markdown heading, `level` 1 to 6.
361    Heading { level: u8 },
362    /// A Markdown code block.
363    Code,
364    /// A hyperlink. Core reports links through [`Style::link`](crate::style::Style::link),
365    /// not through a sink; consumers derive link regions from styles.
366    Link,
367    /// A role an extension names.
368    Other(String),
369}
370
371/// A region a renderable reports: its role, and an optional label (a panel's
372/// title, a heading's text) and link target.
373#[derive(Clone, Debug, PartialEq, Eq)]
374pub struct RegionInfo {
375    pub role: RegionRole,
376    pub label: Option<String>,
377    pub link: Option<String>,
378}
379
380impl RegionInfo {
381    pub fn new(role: RegionRole) -> Self {
382        RegionInfo {
383            role,
384            label: None,
385            link: None,
386        }
387    }
388
389    /// With `label`, trimmed, unless that leaves it empty.
390    pub fn label(mut self, label: impl AsRef<str>) -> Self {
391        let label = label.as_ref().trim();
392        self.label = (!label.is_empty()).then(|| label.to_string());
393        self
394    }
395
396    pub fn link(mut self, link: impl Into<String>) -> Self {
397        self.link = Some(link.into());
398        self
399    }
400}
401
402/// A region's identity, chosen by the [`RegionSink`] that recorded it.
403#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
404pub struct RegionId(pub u64);
405
406/// The [`Meta`](crate::style::Meta) key under which a region's id rides on the
407/// styles of the segments it drew. It never renders.
408pub const REGION_META_KEY: &str = "rich.region";
409
410/// Observes semantic regions (#226): which cells a panel, table, table cell,
411/// rule, Markdown heading or code block drew. Not part of upstream.
412///
413/// Install one with [`ConsoleRegions::set_region_sink`]. Without one (the
414/// default) no renderable asks for a region, nothing is tagged, and output is
415/// exactly as before. With one, a reporting renderable calls
416/// [`enter`](Self::enter) before it renders its children and
417/// [`exit`](Self::exit) after, so the sink sees nesting in call order. It
418/// then tags every segment it drew that no child tagged, by setting
419/// [`REGION_META_KEY`] in the segment's style metadata. Metadata never
420/// renders, so the terminal bytes do not change; it does make styles compare
421/// unequal, so a consumer that merges equal styles (HTML export) should read
422/// the tags and strip them first, as `rich_ext::frame` does.
423pub trait RegionSink: Send + Sync {
424    /// A region starts. Return an id unique within this sink.
425    fn enter(&self, region: RegionInfo) -> RegionId;
426    /// The region `id` has finished rendering.
427    fn exit(&self, id: RegionId);
428}
429
430/// Attach/query a console's [`RegionSink`].
431pub trait ConsoleRegions {
432    fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn RegionSink>>);
433    fn region_sink(&self) -> Option<&dyn RegionSink>;
434}
435
436/// An entered region, exited when dropped, so an early return still exits.
437pub struct RegionGuard<'a> {
438    sink: &'a dyn RegionSink,
439    id: RegionId,
440}
441
442impl RegionGuard<'_> {
443    pub fn id(&self) -> RegionId {
444        self.id
445    }
446
447    /// Tag the segments of `segments` that carry no region yet with this one.
448    pub fn tag(&self, segments: &mut [Segment]) {
449        tag_region(segments, self.id);
450    }
451}
452
453impl Drop for RegionGuard<'_> {
454    fn drop(&mut self) {
455        self.sink.exit(self.id);
456    }
457}
458
459/// Enter a region when `console` has a sink. With none, `None`, and `info` is
460/// never called.
461pub fn enter_region<'a>(
462    console: &'a Console,
463    info: impl FnOnce() -> RegionInfo,
464) -> Option<RegionGuard<'a>> {
465    let sink = console.region_sink()?;
466    let id = sink.enter(info());
467    Some(RegionGuard { sink, id })
468}
469
470/// `render()` as a region. Without a sink, exactly `render()`.
471pub fn report_region(
472    console: &Console,
473    info: impl FnOnce() -> RegionInfo,
474    render: impl FnOnce() -> Vec<Segment>,
475) -> Vec<Segment> {
476    match enter_region(console, info) {
477        None => render(),
478        Some(guard) => {
479            let mut segments = render();
480            guard.tag(&mut segments);
481            segments
482        }
483    }
484}
485
486/// Set [`REGION_META_KEY`] to `id` on every non-control segment that has no
487/// region yet. An unstyled segment gets a style holding only the tag.
488pub fn tag_region(segments: &mut [Segment], id: RegionId) {
489    use crate::style::{Meta, MetaValue};
490    for segment in segments.iter_mut().filter(|segment| !segment.control) {
491        let style = segment.style.take().unwrap_or_default();
492        if region_of(&style).is_some() {
493            segment.style = Some(style);
494            continue;
495        }
496        let mut meta = style.meta_ref().cloned().unwrap_or_else(Meta::new);
497        meta.insert(REGION_META_KEY, MetaValue::Int(id.0 as i64));
498        segment.style = Some(style.with_meta(meta));
499    }
500}
501
502/// The region `style` was tagged with, if any.
503pub fn region_of(style: &crate::style::Style) -> Option<RegionId> {
504    match style.meta_ref()?.get(REGION_META_KEY)? {
505        crate::style::MetaValue::Int(id) => Some(RegionId(*id as u64)),
506        _ => None,
507    }
508}