lumis-core 6.0.0

Lumis shared core
Documentation
//! HTML formatter with linked CSS classes.
//!
//! This module provides the [`HtmlLinked`] formatter that generates HTML output with
//! CSS classes 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 derive_builder::Builder;
use std::{
    io::{self, Write},
    ops::RangeInclusive,
};

/// Configuration for highlighting specific lines in HTML linked output.
#[derive(Clone, Debug)]
pub struct HighlightLines {
    /// List of line ranges to highlight (1-based, inclusive).
    pub lines: Vec<RangeInclusive<usize>>,
    /// The CSS class name to add to highlighted line elements.
    pub class: String,
}

impl Default for HighlightLines {
    fn default() -> Self {
        Self {
            lines: Vec::new(),
            class: "l-highlighted".to_string(),
        }
    }
}

/// HTML formatter with CSS classes.
///
/// Generates HTML with CSS classes instead of inline styles.
/// Works with pre-computed highlight events from any source.
#[derive(Builder, Clone, Debug)]
#[builder(default)]
pub struct HtmlLinked {
    #[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,
    pre_class: Option<String>,
    pre_attrs: crate::formatter::html::HtmlAttrs,
    code_attrs: crate::formatter::html::HtmlAttrs,
    highlight_lines: Option<HighlightLines>,
    #[builder(setter(skip), default)]
    stepped_highlight_lines: Vec<SteppedLineRange>,
    line_numbers: bool,
    header: Option<HtmlElement>,
}

impl HtmlLinkedBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn language(&mut self, language: Language) -> &mut Self {
        self.language = Some(language);
        self
    }

    #[deprecated(since = "0.1.0", note = "use `.language(...)` instead")]
    pub fn lang(&mut self, language: Language) -> &mut Self {
        self.language(language)
    }
}

impl HtmlLinked {
    pub fn new(
        language: Language,
        pre_class: Option<String>,
        highlight_lines: Option<HighlightLines>,
        line_numbers: bool,
        header: Option<HtmlElement>,
    ) -> Self {
        Self {
            language,
            structure: crate::formatter::html::HtmlStructure::Block,
            pre_class,
            pre_attrs: Vec::new(),
            code_attrs: Vec::new(),
            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_class_suffix(&self, is_highlighted: bool) -> Option<String> {
        if !is_highlighted {
            return None;
        }

        self.highlight_lines
            .as_ref()
            .map(|highlight| format!(" {}", highlight.class))
    }

    fn span_attrs_from_index(scope_index: usize) -> String {
        let scope = crate::highlights::HIGHLIGHT_NAMES
            .get(scope_index)
            .copied()
            .unwrap_or("");
        crate::formatter::html::span_linked_attrs(scope)
    }
}

impl Default for HtmlLinked {
    fn default() -> Self {
        Self {
            language: Language::PlainText,
            structure: crate::formatter::html::HtmlStructure::Block,
            pre_class: None,
            pre_attrs: Vec::new(),
            code_attrs: Vec::new(),
            highlight_lines: None,
            stepped_highlight_lines: Vec::new(),
            line_numbers: false,
            header: None,
        }
    }
}

impl<T> Formatter<T> for HtmlLinked {
    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 HtmlLinked {
    fn write<T>(
        &self,
        source: &str,
        events: &[HighlightEvent<'_, T>],
        output: &mut dyn Write,
        exhausted: Option<super::BudgetExhausted>,
    ) -> io::Result<()> {
        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),
            );
        }

        if let Some(ref header) = self.header {
            write!(output, "{}", header.open_tag)?;
        }

        crate::formatter::html::write_pre_tag(
            output,
            self.pre_class.as_deref(),
            None,
            &crate::formatter::html::budget_attrs(&self.pre_attrs, exhausted),
        )?;
        crate::formatter::html::write_code_tag(output, self.language, &self.code_attrs)?;

        let class_suffix = self.get_line_class_suffix(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: None,
                highlighted_line_number_attrs: None,
                highlighted_class: class_suffix.as_deref(),
                highlighted_style: None,
                empty_style: None,
            },
            &|scope_index, _language| Self::span_attrs_from_index(scope_index),
        )?;

        crate::formatter::html::closing_tags(output)?;

        if let Some(ref header) = self.header {
            write!(output, "{}", header.close_tag)?;
        }

        Ok(())
    }
}