lumis-core 6.0.0

Lumis shared core
Documentation
//! HTML formatter with inline CSS styles.
//!
//! This module provides the [`HtmlInline`] formatter that generates HTML output with
//! inline CSS styles for syntax highlighting, working from pre-computed highlight events.

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,
};

/// Configuration for highlighting specific lines in HTML inline output.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HighlightLines {
    /// List of line ranges to highlight (1-based, inclusive).
    pub lines: Vec<RangeInclusive<usize>>,
    /// The styling method to use for highlighted lines.
    pub style: Option<HighlightLinesStyle>,
    /// Optional CSS class to add to highlighted lines.
    pub class: Option<String>,
}

/// Defines how highlighted lines should be styled in HTML inline output.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HighlightLinesStyle {
    /// Use the theme's 'highlighted' style if available.
    Theme,
    /// Use a custom CSS style string.
    Style(String),
}

impl Default for HighlightLines {
    fn default() -> Self {
        Self {
            lines: Vec::new(),
            style: Some(HighlightLinesStyle::Theme),
            class: None,
        }
    }
}

/// HTML formatter with inline CSS styles.
///
/// Generates self-contained HTML with styles embedded directly in elements.
/// Works with pre-computed highlight events from any source.
#[derive(Builder, Clone, Debug)]
#[builder(default)]
pub struct HtmlInline {
    #[builder(setter(custom))]
    language: Language,
    /// `Block` writes a `<pre><code>` block; `Inline` writes only the token
    /// spans, for markup the page already owns. See [`HtmlStructure`].
    ///
    /// [`HtmlStructure`]: crate::formatter::html::HtmlStructure
    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 {
    /// Creates a block formatter with explicit options.
    ///
    /// The theme's styles are rendered as they are. Use
    /// [`Theme::map_styles`] to customize individual scopes.
    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,
        }
    }

    /// Supply compact stepped ranges from a language binding.
    #[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(())
    }
}