term_transcript/svg/
data.rs

1//! Data provided to Handlebars templates.
2
3use std::{collections::HashMap, fmt};
4
5use anstyle::{Ansi256Color, Color, Effects, RgbColor, Style};
6use serde::Serialize;
7
8use crate::{
9    UserInput,
10    svg::{EmbeddedFont, TemplateOptions},
11};
12
13pub(super) mod serde_color {
14    use std::fmt;
15
16    use anstyle::RgbColor;
17    use serde::{Deserializer, Serialize, Serializer, de};
18    use styled_str::{parse_hex_color, rgb_color_to_hex};
19
20    #[allow(clippy::trivially_copy_pass_by_ref)] // required by serde
21    pub(crate) fn serialize<S: Serializer>(
22        color: &RgbColor,
23        serializer: S,
24    ) -> Result<S::Ok, S::Error> {
25        rgb_color_to_hex(*color).serialize(serializer)
26    }
27
28    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
29        deserializer: D,
30    ) -> Result<RgbColor, D::Error> {
31        #[derive(Debug)]
32        struct ColorVisitor;
33
34        impl de::Visitor<'_> for ColorVisitor {
35            type Value = RgbColor;
36
37            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38                formatter.write_str("hex color, such as #fed or #a757ff")
39            }
40
41            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
42                parse_hex_color(value.as_bytes()).map_err(E::custom)
43            }
44        }
45
46        deserializer.deserialize_str(ColorVisitor)
47    }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
51#[serde(untagged)]
52pub(super) enum SerdeColor {
53    Index(u8),
54    Rgb(#[serde(with = "serde_color")] RgbColor),
55}
56
57impl From<Color> for SerdeColor {
58    fn from(color: Color) -> Self {
59        match color {
60            Color::Ansi(color) => Self::Index(color as u8),
61            Color::Ansi256(Ansi256Color(idx)) => Self::Index(idx),
62            Color::Rgb(color) => Self::Rgb(color),
63        }
64    }
65}
66
67/// Serializable `anstyle::Style` representation.
68#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize)]
69#[allow(clippy::struct_excessive_bools)] // makes serialization simpler
70pub(super) struct SerdeStyle {
71    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
72    pub(super) bold: bool,
73    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
74    pub(super) italic: bool,
75    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
76    pub(super) underline: bool,
77    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
78    pub(super) dimmed: bool,
79    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
80    pub(super) strikethrough: bool,
81    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
82    pub(super) inverted: bool,
83    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
84    pub(super) blink: bool,
85    #[serde(skip_serializing_if = "SerdeStyle::is_false")]
86    pub(super) concealed: bool,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub(super) fg: Option<SerdeColor>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub(super) bg: Option<SerdeColor>,
91}
92
93impl SerdeStyle {
94    #[allow(clippy::trivially_copy_pass_by_ref)] // required by serde
95    fn is_false(&value: &bool) -> bool {
96        !value
97    }
98}
99
100impl From<Style> for SerdeStyle {
101    fn from(style: Style) -> Self {
102        let effects = style.get_effects();
103        Self {
104            bold: effects.contains(Effects::BOLD),
105            italic: effects.contains(Effects::ITALIC),
106            underline: effects.contains(Effects::UNDERLINE),
107            dimmed: effects.contains(Effects::DIMMED),
108            strikethrough: effects.contains(Effects::STRIKETHROUGH),
109            inverted: effects.contains(Effects::INVERT),
110            blink: effects.contains(Effects::BLINK),
111            concealed: effects.contains(Effects::HIDDEN),
112            fg: style.get_fg_color().map(SerdeColor::from),
113            bg: style.get_bg_color().map(SerdeColor::from),
114        }
115    }
116}
117
118/// Serializable version of `StyledSpan`. Also, inlines text for convenience instead of using lengths.
119#[derive(Debug, Serialize)]
120pub(super) struct SerdeStyledSpan<'a> {
121    #[serde(flatten)]
122    pub(super) style: SerdeStyle,
123    pub(super) text: &'a str,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
127#[serde(rename_all = "snake_case")]
128pub(super) enum LineBreak {
129    Hard,
130}
131
132#[derive(Debug, Default, Serialize)]
133pub(super) struct StyledLine<'a> {
134    pub(super) spans: Vec<SerdeStyledSpan<'a>>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub(super) br: Option<LineBreak>,
137}
138
139/// Root data structure sent to the Handlebars template.
140///
141/// # Examples
142///
143/// Here's example of JSON serialization of this type:
144///
145/// ```
146/// use styled_str::styled;
147/// # use term_transcript::{svg::{TemplateOptions, NamedPalette}, Transcript, UserInput};
148///
149/// let mut transcript = Transcript::new();
150/// let input = UserInput::command("rainbow");
151/// transcript.add_interaction(input, styled!("Hello, [[green]]world[[/]]!").into());
152/// let template_options = TemplateOptions {
153///     palette: NamedPalette::Dracula.into(),
154///     font_family: "Consolas, Menlo, monospace".to_owned(),
155///     ..TemplateOptions::default()
156/// }
157/// .validated()?;
158/// let data = template_options.render_data(&transcript).unwrap();
159///
160/// let expected_json = serde_json::json!({
161///     "creator": {
162///         "name": "term-transcript",
163///         "version": "0.5.0-beta.1",
164///         "repo": "https://github.com/slowli/term-transcript",
165///     },
166///     "width": 720,
167///     "line_height": null,
168///     "advance_width": null,
169///     "palette": {
170///         "colors": {
171///             "black": "#282936",
172///             "red": "#ea51b2",
173///             "green": "#ebff87",
174///             "yellow": "#00f769",
175///             "blue": "#62d6e8",
176///             "magenta": "#b45bcf",
177///             "cyan": "#a1efe4",
178///             "white": "#e9e9f4",
179///         },
180///         "intense_colors": {
181///             "black": "#626483",
182///             "red": "#b45bcf",
183///             "green": "#3a3c4e",
184///             "yellow": "#4d4f68",
185///             "blue": "#62d6e8",
186///             "magenta": "#f1f2f8",
187///             "cyan": "#00f769",
188///             "white": "#f7f7fb",
189///         },
190///     },
191///     "font_family": "Consolas, Menlo, monospace",
192///     "window": null,
193///     "wrap": {
194///         "hard_break_at": {
195///             "chars": 80,
196///             "mark": "ยป",
197///         },
198///     },
199///     "line_numbers": null,
200///     "dim_opacity": 0.7,
201///     "blink": {
202///         "opacity": 0.7,
203///         "interval": 1.0,
204///     },
205///     "has_failures": false,
206///     "interactions": [{
207///         "input": {
208///             "text": "rainbow",
209///             "prompt": "$",
210///             "hidden": false,
211///         },
212///         "output": [{
213///             "spans": [
214///                 { "text": "Hello, " },
215///                 { "text": "world", "fg": 2 },
216///                 { "text": "!" },
217///             ],
218///         }],
219///         "failure": false,
220///         "exit_status": null,
221///     }]
222/// });
223/// assert_eq!(serde_json::to_value(data).unwrap(), expected_json);
224/// # anyhow::Ok(())
225/// ```
226#[derive(Debug, Serialize)]
227#[non_exhaustive]
228pub struct HandlebarsData<'r> {
229    /// Information about the rendering software.
230    pub creator: CreatorData,
231    /// Template options used for rendering. These options are flattened into the parent
232    /// during serialization.
233    #[serde(flatten)]
234    pub options: &'r TemplateOptions,
235    /// Recorded terminal interactions.
236    pub interactions: Vec<SerializedInteraction<'r>>,
237    /// Has any of terminal interactions failed?
238    pub has_failures: bool,
239    /// A font (usually subset) to be embedded into the generated transcript.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub embedded_font: Option<EmbeddedFont>,
242}
243
244// 1. font-subset -> term-transcript -> font-subset-cli, ..
245// Problem: different workspaces / repos; meaning that font-subset-cli will depend on 2 `font-subset`s (????)
246// Patching the font-subset dep sort of works, unless term-transcript code needs to be modified
247//
248// 2. same, but font-subset is an optional dep in term-transcript, not used in font-subset-cli
249// (replaced with a local module)
250
251/// Information about software used for rendering (i.e., this crate).
252///
253/// It can make sense to include this info as a comment in the rendered template
254/// for debugging purposes.
255#[derive(Debug, Serialize)]
256#[non_exhaustive]
257pub struct CreatorData {
258    /// Name of software rendering the template.
259    pub name: &'static str,
260    /// Version of the rendering software.
261    pub version: &'static str,
262    /// Link to the git repository with the rendering software.
263    pub repo: &'static str,
264}
265
266impl Default for CreatorData {
267    fn default() -> Self {
268        Self {
269            name: env!("CARGO_PKG_NAME"),
270            version: env!("CARGO_PKG_VERSION"),
271            repo: env!("CARGO_PKG_REPOSITORY"),
272        }
273    }
274}
275
276/// Serializable version of [`Interaction`](crate::Interaction).
277#[derive(Serialize)]
278#[non_exhaustive]
279pub struct SerializedInteraction<'a> {
280    /// User's input.
281    pub input: &'a UserInput,
282    /// Terminal output in the [HTML format](#html-output).
283    pub(super) output: Vec<StyledLine<'a>>,
284    /// Exit status of the latest executed program, or `None` if it cannot be determined.
285    pub exit_status: Option<i32>,
286    /// Was execution unsuccessful judging by the [`ExitStatus`](crate::ExitStatus)?
287    pub failure: bool,
288}
289
290impl fmt::Debug for SerializedInteraction<'_> {
291    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
292        formatter
293            .debug_struct("SerializedInteraction")
294            .field("input", &self.input)
295            .field("output.line_count", &self.output.len())
296            .field("exit_status", &self.exit_status)
297            .finish_non_exhaustive()
298    }
299}
300
301#[derive(Debug, Serialize)]
302pub(super) struct CompleteHandlebarsData<'r> {
303    #[serde(flatten)]
304    pub inner: HandlebarsData<'r>,
305    #[serde(rename = "const")]
306    pub constants: &'r HashMap<&'static str, u32>,
307}