summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2021-12-14 14:24:02 +0100
committerMartin Haug <mhaug@live.de>2022-01-27 22:04:45 +0100
commit4f66907d08a8ed18b41e70188b112d7c915aa0bc (patch)
treefee27c337c8d60754e1e459b48084a7ab2f9af00 /src
parent3739ab77207e0e54edb55a110a16a1eb925b84f4 (diff)
Add Code Block syntax highlighting
Diffstat (limited to 'src')
-rw-r--r--src/eval/mod.rs111
-rw-r--r--src/geom/paint.rs17
-rw-r--r--src/library/deco.rs37
-rw-r--r--src/syntax/highlight.rs69
4 files changed, 224 insertions, 10 deletions
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<Mutex<Theme>> = Lazy::new(|| {
+ Mutex::new(ThemeSet::load_defaults().themes.remove("InspiredGitHub").unwrap())
+});
+
+static SYNTAXES: Lazy<Mutex<SyntaxSet>> =
+ 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<Self::Output> {
- 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<Styled<Node>> = 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<Node> {
+ 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<RgbaColor> for Color {
- fn from(rgba: RgbaColor) -> Self {
- Self::Rgba(rgba)
+impl<T> From<T> for Color
+where
+ T: Into<RgbaColor>,
+{
+ fn from(rgba: T) -> Self {
+ Self::Rgba(rgba.into())
}
}
@@ -114,6 +119,12 @@ impl FromStr for RgbaColor {
}
}
+impl From<SynColor> 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<F>(
+ 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<F>(
+ node: RedRef,
+ text: &str,
+ scopes: Vec<Scope>,
+ 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)]