use super::{Formatter, HtmlElement};
use crate::decorations::{LineSelection, SteppedLineRange};
use crate::events::HighlightEvent;
use crate::languages::Language;
use crate::themes::Theme;
use derive_builder::Builder;
use std::{
io::{self, Write},
ops::RangeInclusive,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HighlightLines {
pub lines: Vec<RangeInclusive<usize>>,
pub style: Option<HighlightLinesStyle>,
pub class: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HighlightLinesStyle {
Theme,
Style(String),
}
impl Default for HighlightLines {
fn default() -> Self {
Self {
lines: Vec::new(),
style: Some(HighlightLinesStyle::Theme),
class: None,
}
}
}
#[derive(Builder, Clone, Debug)]
#[builder(default)]
pub struct HtmlInline {
#[builder(setter(custom))]
language: Language,
structure: crate::formatter::html::HtmlStructure,
theme: Option<Theme>,
pre_class: Option<String>,
pre_attrs: crate::formatter::html::HtmlAttrs,
code_attrs: crate::formatter::html::HtmlAttrs,
#[builder(setter(custom))]
italic: bool,
include_highlights: bool,
highlight_lines: Option<HighlightLines>,
#[builder(setter(skip), default)]
stepped_highlight_lines: Vec<SteppedLineRange>,
line_numbers: bool,
header: Option<HtmlElement>,
}
impl HtmlInlineBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn language(&mut self, language: Language) -> &mut Self {
self.language = Some(language);
self
}
#[doc(hidden)]
#[deprecated(
since = "4.2.0",
note = "customize theme styles with Theme::map_styles instead"
)]
pub fn italic(&mut self, italic: bool) -> &mut Self {
self.italic = Some(italic);
self
}
#[deprecated(since = "0.1.0", note = "use `.language(...)` instead")]
pub fn lang(&mut self, language: Language) -> &mut Self {
self.language(language)
}
}
impl HtmlInline {
pub fn new(
language: Language,
theme: Option<Theme>,
pre_class: Option<String>,
include_highlights: bool,
highlight_lines: Option<HighlightLines>,
line_numbers: bool,
header: Option<HtmlElement>,
) -> Self {
Self {
language,
structure: crate::formatter::html::HtmlStructure::Block,
theme,
pre_class,
pre_attrs: Vec::new(),
code_attrs: Vec::new(),
italic: true,
include_highlights,
highlight_lines,
stepped_highlight_lines: Vec::new(),
line_numbers,
header,
}
}
#[doc(hidden)]
pub fn set_stepped_highlight_lines(&mut self, lines: Vec<SteppedLineRange>) {
self.stepped_highlight_lines = lines;
}
fn line_selection(&self) -> LineSelection {
LineSelection::new(
self.highlight_lines
.as_ref()
.map_or(&[][..], |highlight| &highlight.lines),
&self.stepped_highlight_lines,
)
}
fn get_line_attrs(&self, is_highlighted: bool) -> (Option<String>, Option<String>) {
if !is_highlighted {
return (None, None);
}
let class_suffix = self
.highlight_lines
.as_ref()
.and_then(|hl| hl.class.as_ref())
.map(|c| format!(" {c}"));
let style = Some(format!(
"{}{}",
crate::formatter::html::LINE_LAYOUT,
self.get_highlight_style()
.map_or(String::new(), |style| format!(" {style}")),
));
(class_suffix, style)
}
fn get_highlight_style(&self) -> Option<String> {
let highlight_lines = self.highlight_lines.as_ref()?;
match &highlight_lines.style {
Some(HighlightLinesStyle::Theme) => {
let theme = self.theme.as_ref()?;
let highlighted_style = theme.get_style("highlighted")?;
Some(highlighted_style.css(" "))
}
Some(HighlightLinesStyle::Style(style_string)) => Some(style_string.clone()),
None => None,
}
}
fn span_attrs_from_index(&self, scope_index: usize, language: &str) -> String {
let scope = crate::highlights::HIGHLIGHT_NAMES
.get(scope_index)
.copied()
.unwrap_or("");
let lang = language.parse::<Language>().ok();
crate::formatter::html::span_inline_attrs(
lang,
scope,
self.theme.as_ref(),
self.include_highlights,
)
}
fn line_number_attrs(&self, highlighted: bool) -> String {
let scope = if highlighted {
"line_number.highlighted"
} else {
"line_number"
};
crate::formatter::html::unselectable_gutter(&crate::formatter::html::span_inline_attrs(
None,
scope,
self.theme.as_ref(),
false,
))
}
}
impl Default for HtmlInline {
fn default() -> Self {
Self {
language: Language::PlainText,
structure: crate::formatter::html::HtmlStructure::Block,
theme: None,
pre_class: None,
pre_attrs: Vec::new(),
code_attrs: Vec::new(),
italic: true,
include_highlights: false,
highlight_lines: None,
stepped_highlight_lines: Vec::new(),
line_numbers: false,
header: None,
}
}
}
impl<T> Formatter<T> for HtmlInline {
fn language(&self) -> Language {
self.language
}
fn render(
&self,
source: &str,
events: &[HighlightEvent<'_, T>],
output: &mut dyn Write,
) -> io::Result<()> {
self.write(source, events, output, None)
}
fn render_budgeted(
&self,
source: &str,
events: &[HighlightEvent<'_, T>],
output: &mut dyn Write,
exhausted: super::BudgetExhausted,
) -> io::Result<()> {
self.write(source, events, output, Some(exhausted))
}
}
impl HtmlInline {
fn write<T>(
&self,
source: &str,
events: &[HighlightEvent<'_, T>],
output: &mut dyn Write,
exhausted: Option<super::BudgetExhausted>,
) -> io::Result<()> {
if !self.italic {
let upright = Self {
theme: self.theme.clone().map(Theme::without_italics),
italic: true,
..self.clone()
};
return upright.write(source, events, output, exhausted);
}
if self.structure == crate::formatter::html::HtmlStructure::Inline {
return crate::formatter::html::write_html_spans(
output,
source,
events,
self.language,
&|scope_index, language| self.span_attrs_from_index(scope_index, language),
);
}
if let Some(ref header) = self.header {
write!(output, "{}", header.open_tag)?;
}
crate::formatter::html::write_pre_tag(
output,
self.pre_class.as_deref(),
self.theme.as_ref(),
&crate::formatter::html::budget_attrs(&self.pre_attrs, exhausted),
)?;
let code_attrs = if self.highlight_lines.is_some() {
crate::formatter::html::merge_attrs(
vec![("style".into(), crate::formatter::html::CODE_LAYOUT.into())],
&self.code_attrs,
)
} else {
self.code_attrs.clone()
};
crate::formatter::html::write_code_tag(output, self.language, &code_attrs)?;
let (class_suffix, style) = self.get_line_attrs(true);
let line_number_attrs = self.line_numbers.then(|| self.line_number_attrs(false));
let highlighted_line_number_attrs = self.line_numbers.then(|| self.line_number_attrs(true));
crate::formatter::html::write_html_lines(
output,
source,
events,
&crate::formatter::html::HtmlLines {
structure: crate::formatter::html::HtmlStructure::Block,
language: self.language,
selection: &self.line_selection(),
numbered: self.line_numbers,
line_number_attrs: line_number_attrs.as_deref(),
highlighted_line_number_attrs: highlighted_line_number_attrs.as_deref(),
highlighted_class: class_suffix.as_deref(),
highlighted_style: style.as_deref(),
empty_style: Some(crate::formatter::html::LINE_LAYOUT),
},
&|scope_index, language| self.span_attrs_from_index(scope_index, language),
)?;
crate::formatter::html::closing_tags(output)?;
if let Some(ref header) = self.header {
write!(output, "{}", header.close_tag)?;
}
Ok(())
}
}