From 4f66907d08a8ed18b41e70188b112d7c915aa0bc Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 14 Dec 2021 14:24:02 +0100 Subject: Add Code Block syntax highlighting --- src/eval/mod.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++--- src/geom/paint.rs | 17 ++++++-- src/library/deco.rs | 37 +++++++++++++++- src/syntax/highlight.rs | 69 ++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/eval/mod.rs b/src/eval/mod.rs index c16c2208..2759e0d5 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -30,21 +30,36 @@ use std::collections::HashMap; use std::io; use std::mem; use std::path::PathBuf; +use std::sync::Mutex; + +use once_cell::sync::Lazy; +use syntect::easy::HighlightLines; +use syntect::highlighting::{FontStyle, Highlighter, Style as SynStyle, Theme, ThemeSet}; +use syntect::parsing::SyntaxSet; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; -use crate::geom::{Angle, Fractional, Length, Relative}; +use crate::geom::{Angle, Fractional, Length, Paint, Relative, RgbaColor}; use crate::image::ImageStore; use crate::layout::RootNode; -use crate::library::{self, TextNode}; +use crate::library::{self, Decoration, TextNode}; use crate::loading::Loader; +use crate::parse; use crate::source::{SourceId, SourceStore}; +use crate::syntax; use crate::syntax::ast::*; -use crate::syntax::{Span, Spanned}; +use crate::syntax::{RedNode, Span, Spanned}; use crate::util::{EcoString, RefMutExt}; use crate::Context; +static THEME: Lazy> = Lazy::new(|| { + Mutex::new(ThemeSet::load_defaults().themes.remove("InspiredGitHub").unwrap()) +}); + +static SYNTAXES: Lazy> = + Lazy::new(|| Mutex::new(SyntaxSet::load_defaults_newlines())); + /// An evaluated module, ready for importing or conversion to a root layout /// tree. #[derive(Debug, Default, Clone)] @@ -209,15 +224,99 @@ impl Eval for RawNode { type Output = Node; fn eval(&self, _: &mut EvalContext) -> TypResult { - let text = Node::Text(self.text.clone()).monospaced(); + let code = self.highlighted(); Ok(if self.block { - Node::Block(text.into_block()) + Node::Block(code.into_block()) } else { - text + code }) } } +impl RawNode { + /// Styled node for a code block, with optional syntax highlighting. + pub fn highlighted(&self) -> Node { + let mut sequence: Vec> = vec![]; + let syntaxes = SYNTAXES.lock().unwrap(); + + let syntax = if let Some(syntax) = self + .lang + .as_ref() + .and_then(|token| syntaxes.find_syntax_by_token(&token)) + { + Some(syntax) + } else if matches!( + self.lang.as_ref().map(|s| s.to_ascii_lowercase()).as_deref(), + Some("typ" | "typst") + ) { + None + } else { + return Node::Text(self.text.clone()).monospaced(); + }; + + let theme = THEME.lock().unwrap(); + let foreground = theme + .settings + .foreground + .map(RgbaColor::from) + .unwrap_or(RgbaColor::BLACK) + .into(); + + match syntax { + Some(syntax) => { + let mut highlighter = HighlightLines::new(syntax, &theme); + for (i, line) in self.text.lines().enumerate() { + if i != 0 { + sequence.push(Styled::bare(Node::Linebreak)); + } + + for (style, line) in highlighter.highlight(line, &syntaxes) { + sequence.push(Self::styled_line(style, line, foreground)); + } + } + } + None => { + let red_tree = + RedNode::from_root(parse::parse(&self.text), SourceId::from_raw(0)); + let highlighter = Highlighter::new(&theme); + + syntax::highlight_syntect( + red_tree.as_ref(), + &self.text, + &highlighter, + &mut |style, line| { + sequence.push(Self::styled_line(style, line, foreground)); + }, + ) + } + } + + Node::Sequence(sequence).monospaced() + } + + fn styled_line(style: SynStyle, line: &str, foreground: Paint) -> Styled { + let paint = style.foreground.into(); + let text_node = Node::Text(line.into()); + let mut style_map = StyleMap::new(); + + if paint != foreground { + style_map.set(TextNode::FILL, paint); + } + + if style.font_style.contains(FontStyle::BOLD) { + style_map.set(TextNode::STRONG, true); + } + if style.font_style.contains(FontStyle::ITALIC) { + style_map.set(TextNode::EMPH, true); + } + if style.font_style.contains(FontStyle::UNDERLINE) { + style_map.set(TextNode::LINES, vec![Decoration::underline()]); + } + + Styled::new(text_node, style_map) + } +} + impl Eval for MathNode { type Output = Node; diff --git a/src/geom/paint.rs b/src/geom/paint.rs index d906561c..f8638656 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -1,6 +1,8 @@ use std::fmt::Display; use std::str::FromStr; +use syntect::highlighting::Color as SynColor; + use super::*; /// How a fill or stroke should be painted. @@ -34,9 +36,12 @@ impl Debug for Color { } } -impl From for Color { - fn from(rgba: RgbaColor) -> Self { - Self::Rgba(rgba) +impl From for Color +where + T: Into, +{ + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) } } @@ -114,6 +119,12 @@ impl FromStr for RgbaColor { } } +impl From for RgbaColor { + fn from(color: SynColor) -> Self { + Self::new(color.r, color.b, color.g, color.a) + } +} + impl Debug for RgbaColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if f.alternate() { diff --git a/src/library/deco.rs b/src/library/deco.rs index 3e91d1de..5f27c8be 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -38,6 +38,41 @@ pub struct Decoration { pub extent: Linear, } +impl Decoration { + /// Create a new underline with default settings. + pub const fn underline() -> Self { + Self { + line: DecoLine::Underline, + stroke: None, + thickness: None, + offset: None, + extent: Linear::zero(), + } + } + + /// Create a new strikethrough with default settings. + pub const fn strikethrough() -> Self { + Self { + line: DecoLine::Underline, + stroke: None, + thickness: None, + offset: None, + extent: Linear::zero(), + } + } + + /// Create a new overline with default settings. + pub const fn overline() -> Self { + Self { + line: DecoLine::Overline, + stroke: None, + thickness: None, + offset: None, + extent: Linear::zero(), + } + } +} + /// The kind of decorative line. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum DecoLine { @@ -49,7 +84,7 @@ pub enum DecoLine { Overline, } -/// Differents kinds of decorative lines for text. +/// Different kinds of decorative lines for text. pub trait LineKind { const LINE: DecoLine; } diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 9f7365a8..001b28b3 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -1,5 +1,8 @@ use std::ops::Range; +use syntect::highlighting::{Highlighter, Style}; +use syntect::parsing::Scope; + use super::{NodeKind, RedRef}; /// Provide highlighting categories for the children of a node that fall into a @@ -19,6 +22,45 @@ where } } +/// Provide syntect highlighting styles for the children of a node. +pub fn highlight_syntect( + node: RedRef, + text: &str, + highlighter: &Highlighter, + f: &mut F, +) where + F: FnMut(Style, &str), +{ + highlight_syntect_impl(node, text, vec![], highlighter, f) +} + +/// Recursive implementation for returning syntect styles. +fn highlight_syntect_impl( + node: RedRef, + text: &str, + scopes: Vec, + highlighter: &Highlighter, + f: &mut F, +) where + F: FnMut(Style, &str), +{ + if node.children().size_hint().0 == 0 { + f( + highlighter.style_for_stack(&scopes), + &text[node.span().to_range()], + ); + return; + } + + for child in node.children() { + let mut scopes = scopes.clone(); + if let Some(category) = Category::determine(child, node) { + scopes.push(Scope::new(category.tm_scope()).unwrap()) + } + highlight_syntect_impl(child, text, scopes, highlighter, f); + } +} + /// The syntax highlighting category of a node. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Category { @@ -186,6 +228,33 @@ impl Category { NodeKind::IncludeExpr => None, } } + + /// Return the TextMate grammar scope for the given highlighting category. + pub const fn tm_scope(&self) -> &'static str { + match self { + Self::Bracket => "punctuation.definition.typst", + Self::Punctuation => "punctuation.typst", + Self::Comment => "comment.typst", + Self::Strong => "markup.bold.typst", + Self::Emph => "markup.italic.typst", + Self::Raw => "markup.raw.typst", + Self::Math => "string.other.math.typst", + Self::Heading => "markup.heading.typst", + Self::List => "markup.list.typst", + Self::Shortcut => "punctuation.shortcut.typst", + Self::Escape => "constant.character.escape.content.typst", + Self::Keyword => "keyword.typst", + Self::Operator => "keyword.operator.typst", + Self::None => "constant.language.none.typst", + Self::Auto => "constant.language.auto.typst", + Self::Bool => "constant.language.boolean.typst", + Self::Number => "constant.numeric.typst", + Self::String => "string.quoted.double.typst", + Self::Function => "entity.name.function.typst", + Self::Variable => "variable.parameter.typst", + Self::Invalid => "invalid.typst", + } + } } #[cfg(test)] -- cgit v1.2.3 From c183ed3c15110e11bffd40fad5c5fdfb4d1a5814 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 27 Jan 2022 23:07:10 +0100 Subject: Mutex comes from tex and we don't want any --- src/eval/mod.rs | 59 +++++++++++++++++++++++++------------------------ src/library/deco.rs | 29 +++--------------------- src/syntax/highlight.rs | 24 +++++++------------- 3 files changed, 41 insertions(+), 71 deletions(-) (limited to 'src') diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2759e0d5..58400f5a 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -30,7 +30,6 @@ use std::collections::HashMap; use std::io; use std::mem; use std::path::PathBuf; -use std::sync::Mutex; use once_cell::sync::Lazy; use syntect::easy::HighlightLines; @@ -43,7 +42,7 @@ use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; use crate::geom::{Angle, Fractional, Length, Paint, Relative, RgbaColor}; use crate::image::ImageStore; use crate::layout::RootNode; -use crate::library::{self, Decoration, TextNode}; +use crate::library::{self, DecoLine, TextNode}; use crate::loading::Loader; use crate::parse; use crate::source::{SourceId, SourceStore}; @@ -53,12 +52,10 @@ use crate::syntax::{RedNode, Span, Spanned}; use crate::util::{EcoString, RefMutExt}; use crate::Context; -static THEME: Lazy> = Lazy::new(|| { - Mutex::new(ThemeSet::load_defaults().themes.remove("InspiredGitHub").unwrap()) -}); +static THEME: Lazy = + Lazy::new(|| ThemeSet::load_defaults().themes.remove("InspiredGitHub").unwrap()); -static SYNTAXES: Lazy> = - Lazy::new(|| Mutex::new(SyntaxSet::load_defaults_newlines())); +static SYNTAXES: Lazy = Lazy::new(|| SyntaxSet::load_defaults_newlines()); /// An evaluated module, ready for importing or conversion to a root layout /// tree. @@ -237,12 +234,11 @@ impl RawNode { /// Styled node for a code block, with optional syntax highlighting. pub fn highlighted(&self) -> Node { let mut sequence: Vec> = vec![]; - let syntaxes = SYNTAXES.lock().unwrap(); let syntax = if let Some(syntax) = self .lang .as_ref() - .and_then(|token| syntaxes.find_syntax_by_token(&token)) + .and_then(|token| SYNTAXES.find_syntax_by_token(&token)) { Some(syntax) } else if matches!( @@ -254,8 +250,7 @@ impl RawNode { return Node::Text(self.text.clone()).monospaced(); }; - let theme = THEME.lock().unwrap(); - let foreground = theme + let foreground = THEME .settings .foreground .map(RgbaColor::from) @@ -264,28 +259,31 @@ impl RawNode { match syntax { Some(syntax) => { - let mut highlighter = HighlightLines::new(syntax, &theme); + let mut highlighter = HighlightLines::new(syntax, &THEME); for (i, line) in self.text.lines().enumerate() { if i != 0 { sequence.push(Styled::bare(Node::Linebreak)); } - for (style, line) in highlighter.highlight(line, &syntaxes) { - sequence.push(Self::styled_line(style, line, foreground)); + for (style, line) in highlighter.highlight(line, &SYNTAXES) { + sequence.push(Self::styled_piece(style, line, foreground)); } } } None => { - let red_tree = - RedNode::from_root(parse::parse(&self.text), SourceId::from_raw(0)); - let highlighter = Highlighter::new(&theme); + let green = parse::parse(&self.text); + let red = RedNode::from_root(green, SourceId::from_raw(0)); + let highlighter = Highlighter::new(&THEME); syntax::highlight_syntect( - red_tree.as_ref(), - &self.text, + red.as_ref(), &highlighter, - &mut |style, line| { - sequence.push(Self::styled_line(style, line, foreground)); + &mut |range, style| { + sequence.push(Self::styled_piece( + style, + &self.text[range], + foreground, + )); }, ) } @@ -294,26 +292,29 @@ impl RawNode { Node::Sequence(sequence).monospaced() } - fn styled_line(style: SynStyle, line: &str, foreground: Paint) -> Styled { + fn styled_piece(style: SynStyle, piece: &str, foreground: Paint) -> Styled { let paint = style.foreground.into(); - let text_node = Node::Text(line.into()); - let mut style_map = StyleMap::new(); + let node = Node::Text(piece.into()); + + let mut styles = StyleMap::new(); if paint != foreground { - style_map.set(TextNode::FILL, paint); + styles.set(TextNode::FILL, paint); } if style.font_style.contains(FontStyle::BOLD) { - style_map.set(TextNode::STRONG, true); + styles.set(TextNode::STRONG, true); } + if style.font_style.contains(FontStyle::ITALIC) { - style_map.set(TextNode::EMPH, true); + styles.set(TextNode::EMPH, true); } + if style.font_style.contains(FontStyle::UNDERLINE) { - style_map.set(TextNode::LINES, vec![Decoration::underline()]); + styles.set(TextNode::LINES, vec![DecoLine::Underline.into()]); } - Styled::new(text_node, style_map) + Styled::new(node, styles) } } diff --git a/src/library/deco.rs b/src/library/deco.rs index 5f27c8be..ccb657d1 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -38,33 +38,10 @@ pub struct Decoration { pub extent: Linear, } -impl Decoration { - /// Create a new underline with default settings. - pub const fn underline() -> Self { +impl From for Decoration { + fn from(line: DecoLine) -> Self { Self { - line: DecoLine::Underline, - stroke: None, - thickness: None, - offset: None, - extent: Linear::zero(), - } - } - - /// Create a new strikethrough with default settings. - pub const fn strikethrough() -> Self { - Self { - line: DecoLine::Underline, - stroke: None, - thickness: None, - offset: None, - extent: Linear::zero(), - } - } - - /// Create a new overline with default settings. - pub const fn overline() -> Self { - Self { - line: DecoLine::Overline, + line, stroke: None, thickness: None, offset: None, diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 001b28b3..0f1ee89d 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -23,32 +23,24 @@ where } /// Provide syntect highlighting styles for the children of a node. -pub fn highlight_syntect( - node: RedRef, - text: &str, - highlighter: &Highlighter, - f: &mut F, -) where - F: FnMut(Style, &str), +pub fn highlight_syntect(node: RedRef, highlighter: &Highlighter, f: &mut F) +where + F: FnMut(Range, Style), { - highlight_syntect_impl(node, text, vec![], highlighter, f) + highlight_syntect_impl(node, vec![], highlighter, f) } /// Recursive implementation for returning syntect styles. fn highlight_syntect_impl( node: RedRef, - text: &str, scopes: Vec, highlighter: &Highlighter, f: &mut F, ) where - F: FnMut(Style, &str), + F: FnMut(Range, Style), { if node.children().size_hint().0 == 0 { - f( - highlighter.style_for_stack(&scopes), - &text[node.span().to_range()], - ); + f(node.span().to_range(), highlighter.style_for_stack(&scopes)); return; } @@ -57,7 +49,7 @@ fn highlight_syntect_impl( if let Some(category) = Category::determine(child, node) { scopes.push(Scope::new(category.tm_scope()).unwrap()) } - highlight_syntect_impl(child, text, scopes, highlighter, f); + highlight_syntect_impl(child, scopes, highlighter, f); } } @@ -230,7 +222,7 @@ impl Category { } /// Return the TextMate grammar scope for the given highlighting category. - pub const fn tm_scope(&self) -> &'static str { + pub fn tm_scope(&self) -> &'static str { match self { Self::Bracket => "punctuation.definition.typst", Self::Punctuation => "punctuation.typst", -- cgit v1.2.3