use std::{
collections::{BTreeSet, HashMap},
fmt,
io::Write,
iter,
};
use handlebars::{Handlebars, RenderError, RenderErrorReason, Template as HandlebarsTemplate};
#[cfg(feature = "font-subset")]
pub use self::subset::{FontFace, FontSubsetter, SubsettingError};
use self::{data::CompleteHandlebarsData, helpers::register_helpers};
pub use self::{
data::{CreatorData, HandlebarsData, SerializedInteraction},
font::{EmbeddedFont, EmbeddedFontFace, FontEmbedder, FontMetrics},
options::{
BlinkOptions, ContinuedLineNumbers, LineNumberingOptions, LineNumbers, ScrollOptions,
TemplateOptions, ValidTemplateOptions, WindowOptions, WrapOptions,
},
palette::{NamedPalette, NamedPaletteParseError, Palette, TermColors},
};
use crate::{
TermError, Transcript,
svg::{data::StyledLine, processing::split_into_lines},
};
mod data;
mod font;
mod helpers;
mod options;
mod palette;
mod processing;
#[cfg(feature = "font-subset")]
mod subset;
#[cfg(test)]
mod tests;
const COMMON_HELPERS: &str = include_str!("common.handlebars");
const DEFAULT_TEMPLATE: &str = include_str!("default.svg.handlebars");
const PURE_TEMPLATE: &str = include_str!("pure.svg.handlebars");
const MAIN_TEMPLATE_NAME: &str = "main";
impl TemplateOptions {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "debug", skip(transcript), err)
)]
fn render_data<'s>(
&'s self,
transcript: &'s Transcript,
) -> Result<HandlebarsData<'s>, TermError> {
let rendered_outputs = self.render_outputs(transcript);
let mut has_failures = false;
let mut used_chars = BTreeSet::new();
for interaction in transcript.interactions() {
let output = interaction.output();
used_chars.extend(output.text().chars());
let input = interaction.input();
if !input.is_hidden() {
let prompt = input.prompt();
let input_chars = iter::once(input.as_ref())
.chain(prompt)
.flat_map(str::chars);
used_chars.extend(input_chars);
}
}
if let Some(line_numbers) = &self.line_numbers {
used_chars.extend('0'..='9');
let additional_chars = match &line_numbers.continued {
ContinuedLineNumbers::Mark(mark) => mark.as_ref(),
ContinuedLineNumbers::Inherit => "",
};
used_chars.extend(additional_chars.chars());
}
if let Some(wrap) = &self.wrap {
let additional_chars = match wrap {
WrapOptions::HardBreakAt { mark, .. } => mark.as_ref(),
};
used_chars.extend(additional_chars.chars());
}
let embedded_font = self
.font_embedder
.as_deref()
.map(|embedder| embedder.embed_font(used_chars))
.transpose()
.map_err(TermError::FontEmbedding)?;
let interactions: Vec<_> = transcript
.interactions()
.iter()
.zip(rendered_outputs)
.map(|(interaction, output)| {
let failure = interaction
.exit_status()
.is_some_and(|status| !status.is_success());
has_failures = has_failures || failure;
SerializedInteraction {
input: interaction.input(),
output,
exit_status: interaction.exit_status().map(|status| status.0),
failure,
}
})
.collect();
Ok(HandlebarsData {
creator: CreatorData::default(),
interactions,
options: self,
has_failures,
embedded_font,
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))]
fn render_outputs<'a>(&self, transcript: &'a Transcript) -> Vec<Vec<StyledLine<'a>>> {
let max_width = self.wrap.as_ref().map(|wrap_options| match wrap_options {
WrapOptions::HardBreakAt { chars, .. } => chars.get(),
});
transcript
.interactions()
.iter()
.map(|interaction| split_into_lines(interaction.output().as_str(), max_width))
.collect()
}
}
pub struct Template {
options: TemplateOptions,
handlebars: Handlebars<'static>,
constants: HashMap<&'static str, u32>,
}
impl fmt::Debug for Template {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Template")
.field("options", &self.options)
.field("constants", &self.constants)
.finish_non_exhaustive()
}
}
impl Default for Template {
fn default() -> Self {
Self::new(ValidTemplateOptions::default())
}
}
impl Template {
const STD_CONSTANTS: &'static [(&'static str, u32)] = &[
("BLOCK_MARGIN", 6),
("USER_INPUT_PADDING", 2),
("WINDOW_PADDING", 10),
("FONT_SIZE", 14),
("WINDOW_FRAME_HEIGHT", 22),
("LN_WIDTH", 22),
("LN_PADDING", 7),
("SCROLLBAR_RIGHT_OFFSET", 7),
];
#[allow(clippy::missing_panics_doc)] pub fn new(options: ValidTemplateOptions) -> Self {
let template = HandlebarsTemplate::compile(DEFAULT_TEMPLATE)
.expect("Default template should be valid");
Self {
constants: Self::STD_CONSTANTS.iter().copied().collect(),
..Self::custom(template, options)
}
}
#[allow(clippy::missing_panics_doc)] pub fn pure_svg(options: ValidTemplateOptions) -> Self {
let template =
HandlebarsTemplate::compile(PURE_TEMPLATE).expect("Pure template should be valid");
Self {
constants: Self::STD_CONSTANTS.iter().copied().collect(),
..Self::custom(template, options)
}
}
#[allow(clippy::missing_panics_doc)] pub fn custom(template: HandlebarsTemplate, options: ValidTemplateOptions) -> Self {
let mut handlebars = Handlebars::new();
handlebars.set_strict_mode(true);
register_helpers(&mut handlebars);
handlebars.register_template(MAIN_TEMPLATE_NAME, template);
let helpers = HandlebarsTemplate::compile(COMMON_HELPERS).unwrap();
handlebars.register_template("_common", helpers);
Self {
options: options.into_inner(),
handlebars,
constants: HashMap::new(),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, err))]
pub fn render<W: Write>(
&self,
transcript: &Transcript,
destination: W,
) -> Result<(), RenderError> {
let data = self
.options
.render_data(transcript)
.map_err(|err| RenderErrorReason::NestedError(Box::new(err)))?;
let data = CompleteHandlebarsData {
inner: data,
constants: &self.constants,
};
#[cfg(feature = "tracing")]
tracing::debug!(?data, "using Handlebars data");
#[cfg(feature = "tracing")]
let _entered = tracing::debug_span!("render_to_write").entered();
self.handlebars
.render_to_write(MAIN_TEMPLATE_NAME, &data, destination)
}
}