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