Skip to content

Commit

Permalink
chore: merge pull request #70 from Unimarkup/syntax-highlight
Browse files Browse the repository at this point in the history
feat: add syntax highlighting for verbatim blocks

Uses the syntect crate to highlight content in HTML. The crate also allows to create custom output formats.
In the future, the content could be highlighted using Unimarkup elements instead.
For this, text groups with attributes must be implemented first.
  • Loading branch information
mhatzl committed Apr 28, 2022
2 parents 3b2a5f6 + 6d026cd commit 1f2e748
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 14 deletions.
2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ shlex = "1.1.0"
clap = { version = "^3.1.0", features = ["derive", "cargo", "env"] }
sha3 = "0.10"
hex = "0.4"
syntect = "4.6"
lazy_static = "1.4.0"
38 changes: 24 additions & 14 deletions core/src/elements/verbatim_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::elements::log_id::EnclosedErrLogId;
use crate::elements::types::{UnimarkupBlocks, UnimarkupType};
use crate::frontend::error::{custom_pest_error, FrontendError};
use crate::frontend::parser::{Rule, UmParse};
use crate::highlight::{self, DEFAULT_THEME, PLAIN_SYNTAX};
use crate::log_id::{LogId, SetLog};
use crate::middleend::{AsIrLines, ContentIrLine};

Expand Down Expand Up @@ -190,18 +191,21 @@ impl Render for VerbatimBlock {
let attributes =
serde_json::from_str::<VerbatimAttributes>(&self.attributes).unwrap_or_default();

res.push_str("<pre><code");
res.push_str(" id='");
res.push_str(&self.id);

if let Some(language) = attributes.language {
res.push_str("' class='language-");
res.push_str(&language.trim().to_lowercase());
}
let language = match attributes.language {
Some(language) => language,
None => PLAIN_SYNTAX.to_string(),
};

res.push_str("'>");
res.push_str(&self.content);
res.push_str("</code></pre>");
res.push_str(&format!(
"<div id='{}' class='code-block language-{}' >",
&self.id, &language
));
res.push_str(&highlight::highlight_html_lines(
&self.content,
&language,
DEFAULT_THEME,
));
res.push_str("</div>");

Ok(res)
}
Expand Down Expand Up @@ -240,8 +244,10 @@ mod tests {
};

let expected_html = format!(
"<pre><code id='{}' class='language-{}'>{}</code></pre>",
id, lang, content
"<div id='{}' class='code-block language-{}' >{}</div>",
id,
lang,
&highlight::highlight_html_lines(&content, &lang, DEFAULT_THEME)
);

assert_eq!(expected_html, block.render_html().unwrap());
Expand All @@ -264,7 +270,11 @@ mod tests {
line_nr: 0,
};

let expected_html = format!("<pre><code id='{}'>{}</code></pre>", id, content);
let expected_html = format!(
"<div id='{}' class='code-block language-plain' >{}</div>",
id,
&highlight::highlight_html_lines(&content, PLAIN_SYNTAX, DEFAULT_THEME)
);

assert_eq!(expected_html, block.render_html().unwrap());
}
Expand Down
87 changes: 87 additions & 0 deletions core/src/highlight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Provides access to syntax and theme sets and syntax highlighting in general

use lazy_static::lazy_static;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};

/// The default theme that is used for highlighting
pub const DEFAULT_THEME: &str = "Solarized (dark)";

/// Constant to get syntax highlighting for a plain text
pub const PLAIN_SYNTAX: &str = "plain";

lazy_static! {
/// Static reference to the syntax set containing all supported syntaxes
pub static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines();
/// Static reference to the theme set containing all supported themes
pub static ref THEME_SET: ThemeSet = ThemeSet::load_defaults();
}

/// This function highlights given lines according to set language and theme,
/// returning a standalone HTML string with surrounding `pre` tags.
///
/// If the language is not found in the available syntax set, the first line is analysed.
/// If this also leads to no match, the content is highlighted as plain text.
///
/// If the theme is not supported, a fallback theme is used.
///
/// # Arguments
///
/// * `content` - Content that is being highlighted
/// * `language` - The language to use for highlighting
/// * `theme` - The theme to use for highlighting
///
/// Returns HTML with the highlighted content.
pub fn highlight_html_lines(content: &str, language: &str, theme: &str) -> String {
let syntax = get_syntax(content.lines().next().unwrap(), language);
syntect::html::highlighted_html_for_string(content, &SYNTAX_SET, syntax, get_theme(theme))
}

/// This function highlights a single line according to set language and theme to HTML.
///
/// If the language is not found in the available syntax set, the line is analysed.
/// If this also leads to no match, the content is highlighted as plain text.
///
/// If the theme is not supported, a fallback theme is used.
///
/// # Arguments
///
/// * `content` - Content that is being highlighted. Must NOT contain a newline!
/// * `language` - The language to use for highlighting
/// * `theme` - The theme to use for highlighting
///
/// Returns HTML with the highlighted content.
pub fn highlight_single_html_line(one_line: &str, language: &str, theme: &str) -> String {
let syntax = get_syntax(one_line, language);
let mut h = HighlightLines::new(syntax, get_theme(theme));
let regions = h.highlight(one_line, &SYNTAX_SET);
syntect::html::styled_line_to_highlighted_html(
&regions[..],
syntect::html::IncludeBackground::No,
)
}

/// Get syntax for given language or try to identify syntax by given line.
/// Falls back to plain text if neither matches a syntax.
fn get_syntax(first_line: &str, language: &str) -> &'static SyntaxReference {
if language.to_lowercase() == PLAIN_SYNTAX {
return SYNTAX_SET.find_syntax_plain_text();
}

match SYNTAX_SET.find_syntax_by_name(language) {
Some(syntax) => syntax,
None => match SYNTAX_SET.find_syntax_by_first_line(first_line) {
Some(syntax) => syntax,
None => SYNTAX_SET.find_syntax_plain_text(),
},
}
}

/// Get theme or fallback, if theme is not found
fn get_theme(theme: &str) -> &'static Theme {
match THEME_SET.themes.get(theme) {
Some(theme) => theme,
None => &THEME_SET.themes[DEFAULT_THEME],
}
}
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod config;
pub mod elements;
pub mod error;
pub mod frontend;
pub mod highlight;
pub mod log_id;
pub mod middleend;
pub mod security;
Expand Down

0 comments on commit 1f2e748

Please sign in to comment.