summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock196
-rw-r--r--Cargo.toml3
-rw-r--r--src/eval/mod.rs112
-rw-r--r--src/geom/paint.rs17
-rw-r--r--src/library/deco.rs14
-rw-r--r--src/syntax/highlight.rs61
-rw-r--r--tests/ref/markup/raw.pngbin10127 -> 22260 bytes
7 files changed, 392 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 706cd481..74035c53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15,6 +15,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -45,6 +54,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -212,6 +245,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]]
+name = "fancy-regex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
+dependencies = [
+ "bit-set",
+ "regex",
+]
+
+[[package]]
name = "filedescriptor"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -241,6 +284,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -261,6 +310,12 @@ dependencies = [
]
[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
name = "iai"
version = "0.1.1"
source = "git+https://github.com/reknih/iai#3f0f92736408ebce6545808b98e0cb2aea89b7dd"
@@ -285,6 +340,16 @@ dependencies = [
]
[[package]]
+name = "indexmap"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -300,6 +365,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
name = "jpeg-decoder"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -315,12 +386,33 @@ dependencies = [
]
[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
name = "libc"
version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9"
[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
+[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -336,6 +428,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
name = "memmap2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -405,6 +503,15 @@ dependencies = [
]
[[package]]
+name = "num_threads"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -417,7 +524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36d760a6f2ac90811cba1006a298e8a7e5ce2c922bb5dc7f7000911a4a6b60f4"
dependencies = [
"bitflags",
- "itoa",
+ "itoa 0.4.8",
"ryu",
]
@@ -436,6 +543,20 @@ dependencies = [
]
[[package]]
+name = "plist"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
+dependencies = [
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "serde",
+ "time",
+ "xml-rs",
+]
+
+[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -550,6 +671,23 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
name = "resvg"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -615,6 +753,12 @@ dependencies = [
]
[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -644,6 +788,17 @@ dependencies = [
]
[[package]]
+name = "serde_json"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
+dependencies = [
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "simplecss"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -697,6 +852,27 @@ dependencies = [
]
[[package]]
+name = "syntect"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031"
+dependencies = [
+ "bincode",
+ "bitflags",
+ "fancy-regex",
+ "flate2",
+ "fnv",
+ "lazy_static",
+ "lazycell",
+ "plist",
+ "regex-syntax",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "walkdir",
+]
+
+[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -726,6 +902,17 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
+dependencies = [
+ "itoa 1.0.1",
+ "libc",
+ "num_threads",
+]
+
+[[package]]
name = "tiny-skia"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -771,6 +958,7 @@ dependencies = [
"same-file",
"serde",
"svg2pdf",
+ "syntect",
"tiny-skia",
"ttf-parser",
"typst-macros",
@@ -914,6 +1102,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a"
[[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+
+[[package]]
name = "xmlparser"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index bfeeb060..2da2b02f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,9 @@ xi-unicode = "0.3"
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
usvg = { version = "0.20", default-features = false }
+# External implementation of user-facing features
+syntect = { version = "4.6", default-features = false, features = ["dump-load", "parsing", "regex-fancy", "assets"] }
+
# PDF export
miniz_oxide = "0.4"
pdf-writer = "0.4"
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index c16c2208..58400f5a 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -31,20 +31,32 @@ use std::io;
use std::mem;
use std::path::PathBuf;
+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, DecoLine, 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<Theme> =
+ Lazy::new(|| ThemeSet::load_defaults().themes.remove("InspiredGitHub").unwrap());
+
+static SYNTAXES: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines());
+
/// An evaluated module, ready for importing or conversion to a root layout
/// tree.
#[derive(Debug, Default, Clone)]
@@ -209,15 +221,103 @@ 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 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 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_piece(style, line, foreground));
+ }
+ }
+ }
+ None => {
+ 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.as_ref(),
+ &highlighter,
+ &mut |range, style| {
+ sequence.push(Self::styled_piece(
+ style,
+ &self.text[range],
+ foreground,
+ ));
+ },
+ )
+ }
+ }
+
+ Node::Sequence(sequence).monospaced()
+ }
+
+ fn styled_piece(style: SynStyle, piece: &str, foreground: Paint) -> Styled<Node> {
+ let paint = style.foreground.into();
+ let node = Node::Text(piece.into());
+
+ let mut styles = StyleMap::new();
+
+ if paint != foreground {
+ styles.set(TextNode::FILL, paint);
+ }
+
+ if style.font_style.contains(FontStyle::BOLD) {
+ styles.set(TextNode::STRONG, true);
+ }
+
+ if style.font_style.contains(FontStyle::ITALIC) {
+ styles.set(TextNode::EMPH, true);
+ }
+
+ if style.font_style.contains(FontStyle::UNDERLINE) {
+ styles.set(TextNode::LINES, vec![DecoLine::Underline.into()]);
+ }
+
+ Styled::new(node, styles)
+ }
+}
+
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..ccb657d1 100644
--- a/src/library/deco.rs
+++ b/src/library/deco.rs
@@ -38,6 +38,18 @@ pub struct Decoration {
pub extent: Linear,
}
+impl From<DecoLine> for Decoration {
+ fn from(line: DecoLine) -> Self {
+ Self {
+ line,
+ 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 +61,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..0f1ee89d 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,37 @@ where
}
}
+/// Provide syntect highlighting styles for the children of a node.
+pub fn highlight_syntect<F>(node: RedRef, highlighter: &Highlighter, f: &mut F)
+where
+ F: FnMut(Range<usize>, Style),
+{
+ highlight_syntect_impl(node, vec![], highlighter, f)
+}
+
+/// Recursive implementation for returning syntect styles.
+fn highlight_syntect_impl<F>(
+ node: RedRef,
+ scopes: Vec<Scope>,
+ highlighter: &Highlighter,
+ f: &mut F,
+) where
+ F: FnMut(Range<usize>, Style),
+{
+ if node.children().size_hint().0 == 0 {
+ f(node.span().to_range(), highlighter.style_for_stack(&scopes));
+ 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, scopes, highlighter, f);
+ }
+}
+
/// The syntax highlighting category of a node.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Category {
@@ -186,6 +220,33 @@ impl Category {
NodeKind::IncludeExpr => None,
}
}
+
+ /// Return the TextMate grammar scope for the given highlighting category.
+ pub 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)]
diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png
index 4effb303..ec39d3df 100644
--- a/tests/ref/markup/raw.png
+++ b/tests/ref/markup/raw.png
Binary files differ