summaryrefslogtreecommitdiff
path: root/crates/typst-syntax/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-09-26 17:12:18 +0200
committerLaurenz <laurmaedje@gmail.com>2023-09-26 17:12:18 +0200
commitd7928a8ea347df89f501e5b3ecbb52402cae8edb (patch)
tree0c63d98a96e1e3762d0fec94c7cda3d16817da64 /crates/typst-syntax/src
parent0d39fa021f2aa84c3302242082ea3bf23cc7f99d (diff)
Extract IDE crate
Diffstat (limited to 'crates/typst-syntax/src')
-rw-r--r--crates/typst-syntax/src/highlight.rs457
-rw-r--r--crates/typst-syntax/src/lib.rs2
2 files changed, 459 insertions, 0 deletions
diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs
new file mode 100644
index 00000000..e8ae613b
--- /dev/null
+++ b/crates/typst-syntax/src/highlight.rs
@@ -0,0 +1,457 @@
+use crate::{ast, LinkedNode, SyntaxKind, SyntaxNode};
+
+/// A syntax highlighting tag.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Tag {
+ /// A line or block comment.
+ Comment,
+ /// Punctuation in code.
+ Punctuation,
+ /// An escape sequence or shorthand.
+ Escape,
+ /// Strong markup.
+ Strong,
+ /// Emphasized markup.
+ Emph,
+ /// A hyperlink.
+ Link,
+ /// Raw text.
+ Raw,
+ /// A label.
+ Label,
+ /// A reference to a label.
+ Ref,
+ /// A section heading.
+ Heading,
+ /// A marker of a list, enumeration, or term list.
+ ListMarker,
+ /// A term in a term list.
+ ListTerm,
+ /// The delimiters of an equation.
+ MathDelimiter,
+ /// An operator with special meaning in an equation.
+ MathOperator,
+ /// A keyword.
+ Keyword,
+ /// An operator in code.
+ Operator,
+ /// A numeric literal.
+ Number,
+ /// A string literal.
+ String,
+ /// A function or method name.
+ Function,
+ /// An interpolated variable in markup or math.
+ Interpolated,
+ /// A syntax error.
+ Error,
+}
+
+impl Tag {
+ /// The list of all tags.
+ pub const LIST: &'static [Tag] = &[
+ Self::Comment,
+ Self::Punctuation,
+ Self::Escape,
+ Self::Strong,
+ Self::Emph,
+ Self::Link,
+ Self::Raw,
+ Self::MathDelimiter,
+ Self::MathOperator,
+ Self::Heading,
+ Self::ListMarker,
+ Self::ListTerm,
+ Self::Label,
+ Self::Ref,
+ Self::Keyword,
+ Self::Operator,
+ Self::Number,
+ Self::String,
+ Self::Function,
+ Self::Interpolated,
+ Self::Error,
+ ];
+
+ /// Return the recommended TextMate grammar scope for the given highlighting
+ /// tag.
+ pub fn tm_scope(&self) -> &'static str {
+ match self {
+ Self::Comment => "comment.typst",
+ Self::Punctuation => "punctuation.typst",
+ Self::Escape => "constant.character.escape.typst",
+ Self::Strong => "markup.bold.typst",
+ Self::Emph => "markup.italic.typst",
+ Self::Link => "markup.underline.link.typst",
+ Self::Raw => "markup.raw.typst",
+ Self::MathDelimiter => "punctuation.definition.math.typst",
+ Self::MathOperator => "keyword.operator.math.typst",
+ Self::Heading => "markup.heading.typst",
+ Self::ListMarker => "punctuation.definition.list.typst",
+ Self::ListTerm => "markup.list.term.typst",
+ Self::Label => "entity.name.label.typst",
+ Self::Ref => "markup.other.reference.typst",
+ Self::Keyword => "keyword.typst",
+ Self::Operator => "keyword.operator.typst",
+ Self::Number => "constant.numeric.typst",
+ Self::String => "string.quoted.double.typst",
+ Self::Function => "entity.name.function.typst",
+ Self::Interpolated => "meta.interpolation.typst",
+ Self::Error => "invalid.typst",
+ }
+ }
+
+ /// The recommended CSS class for the highlighting tag.
+ pub fn css_class(self) -> &'static str {
+ match self {
+ Self::Comment => "typ-comment",
+ Self::Punctuation => "typ-punct",
+ Self::Escape => "typ-escape",
+ Self::Strong => "typ-strong",
+ Self::Emph => "typ-emph",
+ Self::Link => "typ-link",
+ Self::Raw => "typ-raw",
+ Self::Label => "typ-label",
+ Self::Ref => "typ-ref",
+ Self::Heading => "typ-heading",
+ Self::ListMarker => "typ-marker",
+ Self::ListTerm => "typ-term",
+ Self::MathDelimiter => "typ-math-delim",
+ Self::MathOperator => "typ-math-op",
+ Self::Keyword => "typ-key",
+ Self::Operator => "typ-op",
+ Self::Number => "typ-num",
+ Self::String => "typ-str",
+ Self::Function => "typ-func",
+ Self::Interpolated => "typ-pol",
+ Self::Error => "typ-error",
+ }
+ }
+}
+
+/// Determine the highlight tag of a linked syntax node.
+///
+/// Returns `None` if the node should not be highlighted.
+pub fn highlight(node: &LinkedNode) -> Option<Tag> {
+ match node.kind() {
+ SyntaxKind::Markup
+ if node.parent_kind() == Some(SyntaxKind::TermItem)
+ && node.next_sibling_kind() == Some(SyntaxKind::Colon) =>
+ {
+ Some(Tag::ListTerm)
+ }
+ SyntaxKind::Markup => None,
+ SyntaxKind::Text => None,
+ SyntaxKind::Space => None,
+ SyntaxKind::Linebreak => Some(Tag::Escape),
+ SyntaxKind::Parbreak => None,
+ SyntaxKind::Escape => Some(Tag::Escape),
+ SyntaxKind::Shorthand => Some(Tag::Escape),
+ SyntaxKind::SmartQuote => None,
+ SyntaxKind::Strong => Some(Tag::Strong),
+ SyntaxKind::Emph => Some(Tag::Emph),
+ SyntaxKind::Raw => Some(Tag::Raw),
+ SyntaxKind::Link => Some(Tag::Link),
+ SyntaxKind::Label => Some(Tag::Label),
+ SyntaxKind::Ref => Some(Tag::Ref),
+ SyntaxKind::RefMarker => None,
+ SyntaxKind::Heading => Some(Tag::Heading),
+ SyntaxKind::HeadingMarker => None,
+ SyntaxKind::ListItem => None,
+ SyntaxKind::ListMarker => Some(Tag::ListMarker),
+ SyntaxKind::EnumItem => None,
+ SyntaxKind::EnumMarker => Some(Tag::ListMarker),
+ SyntaxKind::TermItem => None,
+ SyntaxKind::TermMarker => Some(Tag::ListMarker),
+ SyntaxKind::Equation => None,
+
+ SyntaxKind::Math => None,
+ SyntaxKind::MathIdent => highlight_ident(node),
+ SyntaxKind::MathAlignPoint => Some(Tag::MathOperator),
+ SyntaxKind::MathDelimited => None,
+ SyntaxKind::MathAttach => None,
+ SyntaxKind::MathFrac => None,
+ SyntaxKind::MathRoot => None,
+ SyntaxKind::MathPrimes => None,
+
+ SyntaxKind::Hashtag => highlight_hashtag(node),
+ SyntaxKind::LeftBrace => Some(Tag::Punctuation),
+ SyntaxKind::RightBrace => Some(Tag::Punctuation),
+ SyntaxKind::LeftBracket => Some(Tag::Punctuation),
+ SyntaxKind::RightBracket => Some(Tag::Punctuation),
+ SyntaxKind::LeftParen => Some(Tag::Punctuation),
+ SyntaxKind::RightParen => Some(Tag::Punctuation),
+ SyntaxKind::Comma => Some(Tag::Punctuation),
+ SyntaxKind::Semicolon => Some(Tag::Punctuation),
+ SyntaxKind::Colon => Some(Tag::Punctuation),
+ SyntaxKind::Star => match node.parent_kind() {
+ Some(SyntaxKind::Strong) => None,
+ _ => Some(Tag::Operator),
+ },
+ SyntaxKind::Underscore => match node.parent_kind() {
+ Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator),
+ _ => None,
+ },
+ SyntaxKind::Dollar => Some(Tag::MathDelimiter),
+ SyntaxKind::Plus => Some(Tag::Operator),
+ SyntaxKind::Minus => Some(Tag::Operator),
+ SyntaxKind::Slash => Some(match node.parent_kind() {
+ Some(SyntaxKind::MathFrac) => Tag::MathOperator,
+ _ => Tag::Operator,
+ }),
+ SyntaxKind::Hat => Some(Tag::MathOperator),
+ SyntaxKind::Prime => Some(Tag::MathOperator),
+ SyntaxKind::Dot => Some(Tag::Punctuation),
+ SyntaxKind::Eq => match node.parent_kind() {
+ Some(SyntaxKind::Heading) => None,
+ _ => Some(Tag::Operator),
+ },
+ SyntaxKind::EqEq => Some(Tag::Operator),
+ SyntaxKind::ExclEq => Some(Tag::Operator),
+ SyntaxKind::Lt => Some(Tag::Operator),
+ SyntaxKind::LtEq => Some(Tag::Operator),
+ SyntaxKind::Gt => Some(Tag::Operator),
+ SyntaxKind::GtEq => Some(Tag::Operator),
+ SyntaxKind::PlusEq => Some(Tag::Operator),
+ SyntaxKind::HyphEq => Some(Tag::Operator),
+ SyntaxKind::StarEq => Some(Tag::Operator),
+ SyntaxKind::SlashEq => Some(Tag::Operator),
+ SyntaxKind::Dots => Some(Tag::Operator),
+ SyntaxKind::Arrow => Some(Tag::Operator),
+ SyntaxKind::Root => Some(Tag::MathOperator),
+
+ SyntaxKind::Not => Some(Tag::Keyword),
+ SyntaxKind::And => Some(Tag::Keyword),
+ SyntaxKind::Or => Some(Tag::Keyword),
+ SyntaxKind::None => Some(Tag::Keyword),
+ SyntaxKind::Auto => Some(Tag::Keyword),
+ SyntaxKind::Let => Some(Tag::Keyword),
+ SyntaxKind::Set => Some(Tag::Keyword),
+ SyntaxKind::Show => Some(Tag::Keyword),
+ SyntaxKind::If => Some(Tag::Keyword),
+ SyntaxKind::Else => Some(Tag::Keyword),
+ SyntaxKind::For => Some(Tag::Keyword),
+ SyntaxKind::In => Some(Tag::Keyword),
+ SyntaxKind::While => Some(Tag::Keyword),
+ SyntaxKind::Break => Some(Tag::Keyword),
+ SyntaxKind::Continue => Some(Tag::Keyword),
+ SyntaxKind::Return => Some(Tag::Keyword),
+ SyntaxKind::Import => Some(Tag::Keyword),
+ SyntaxKind::Include => Some(Tag::Keyword),
+ SyntaxKind::As => Some(Tag::Keyword),
+
+ SyntaxKind::Code => None,
+ SyntaxKind::Ident => highlight_ident(node),
+ SyntaxKind::Bool => Some(Tag::Keyword),
+ SyntaxKind::Int => Some(Tag::Number),
+ SyntaxKind::Float => Some(Tag::Number),
+ SyntaxKind::Numeric => Some(Tag::Number),
+ SyntaxKind::Str => Some(Tag::String),
+ SyntaxKind::CodeBlock => None,
+ SyntaxKind::ContentBlock => None,
+ SyntaxKind::Parenthesized => None,
+ SyntaxKind::Array => None,
+ SyntaxKind::Dict => None,
+ SyntaxKind::Named => None,
+ SyntaxKind::Keyed => None,
+ SyntaxKind::Unary => None,
+ SyntaxKind::Binary => None,
+ SyntaxKind::FieldAccess => None,
+ SyntaxKind::FuncCall => None,
+ SyntaxKind::Args => None,
+ SyntaxKind::Spread => None,
+ SyntaxKind::Closure => None,
+ SyntaxKind::Params => None,
+ SyntaxKind::LetBinding => None,
+ SyntaxKind::SetRule => None,
+ SyntaxKind::ShowRule => None,
+ SyntaxKind::Conditional => None,
+ SyntaxKind::WhileLoop => None,
+ SyntaxKind::ForLoop => None,
+ SyntaxKind::ModuleImport => None,
+ SyntaxKind::ImportItems => None,
+ SyntaxKind::RenamedImportItem => None,
+ SyntaxKind::ModuleInclude => None,
+ SyntaxKind::LoopBreak => None,
+ SyntaxKind::LoopContinue => None,
+ SyntaxKind::FuncReturn => None,
+ SyntaxKind::Destructuring => None,
+ SyntaxKind::DestructAssignment => None,
+
+ SyntaxKind::LineComment => Some(Tag::Comment),
+ SyntaxKind::BlockComment => Some(Tag::Comment),
+ SyntaxKind::Error => Some(Tag::Error),
+ SyntaxKind::Eof => None,
+ }
+}
+
+/// Highlight an identifier based on context.
+fn highlight_ident(node: &LinkedNode) -> Option<Tag> {
+ // Are we directly before an argument list?
+ let next_leaf = node.next_leaf();
+ if let Some(next) = &next_leaf {
+ if node.range().end == next.offset()
+ && ((next.kind() == SyntaxKind::LeftParen
+ && matches!(
+ next.parent_kind(),
+ Some(SyntaxKind::Args | SyntaxKind::Params)
+ ))
+ || (next.kind() == SyntaxKind::LeftBracket
+ && next.parent_kind() == Some(SyntaxKind::ContentBlock)))
+ {
+ return Some(Tag::Function);
+ }
+ }
+
+ // Are we in math?
+ if node.kind() == SyntaxKind::MathIdent {
+ return Some(Tag::Interpolated);
+ }
+
+ // Find the first non-field access ancestor.
+ let mut ancestor = node;
+ while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) {
+ ancestor = ancestor.parent()?;
+ }
+
+ // Are we directly before or behind a show rule colon?
+ if ancestor.parent_kind() == Some(SyntaxKind::ShowRule)
+ && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)
+ || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon))
+ {
+ return Some(Tag::Function);
+ }
+
+ // Are we (or an ancestor field access) directly after a hashtag.
+ if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hashtag) {
+ return Some(Tag::Interpolated);
+ }
+
+ // Are we behind a dot, that is behind another identifier?
+ let prev = node.prev_leaf()?;
+ if prev.kind() == SyntaxKind::Dot {
+ let prev_prev = prev.prev_leaf()?;
+ if is_ident(&prev_prev) {
+ return highlight_ident(&prev_prev);
+ }
+ }
+
+ None
+}
+
+/// Highlight a hashtag based on context.
+fn highlight_hashtag(node: &LinkedNode) -> Option<Tag> {
+ let next = node.next_sibling()?;
+ let expr = next.cast::<ast::Expr>()?;
+ if !expr.hashtag() {
+ return None;
+ }
+ highlight(&next.leftmost_leaf()?)
+}
+
+/// Whether the node is one of the two identifier nodes.
+fn is_ident(node: &LinkedNode) -> bool {
+ matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
+}
+
+/// Highlight a node to an HTML `code` element.
+///
+/// This uses these [CSS classes for categories](Tag::css_class).
+pub fn highlight_html(root: &SyntaxNode) -> String {
+ let mut buf = String::from("<code>");
+ let node = LinkedNode::new(root);
+ highlight_html_impl(&mut buf, &node);
+ buf.push_str("</code>");
+ buf
+}
+
+/// Highlight one source node, emitting HTML.
+fn highlight_html_impl(html: &mut String, node: &LinkedNode) {
+ let mut span = false;
+ if let Some(tag) = highlight(node) {
+ if tag != Tag::Error {
+ span = true;
+ html.push_str("<span class=\"");
+ html.push_str(tag.css_class());
+ html.push_str("\">");
+ }
+ }
+
+ let text = node.text();
+ if !text.is_empty() {
+ for c in text.chars() {
+ match c {
+ '<' => html.push_str("&lt;"),
+ '>' => html.push_str("&gt;"),
+ '&' => html.push_str("&amp;"),
+ '\'' => html.push_str("&#39;"),
+ '"' => html.push_str("&quot;"),
+ _ => html.push(c),
+ }
+ }
+ } else {
+ for child in node.children() {
+ highlight_html_impl(html, &child);
+ }
+ }
+
+ if span {
+ html.push_str("</span>");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ops::Range;
+
+ use super::*;
+
+ #[test]
+ fn test_highlighting() {
+ use Tag::*;
+
+ #[track_caller]
+ fn test(text: &str, goal: &[(Range<usize>, Tag)]) {
+ let mut vec = vec![];
+ let root = crate::parse(text);
+ highlight_tree(&mut vec, &LinkedNode::new(&root));
+ assert_eq!(vec, goal);
+ }
+
+ fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) {
+ if let Some(tag) = highlight(node) {
+ tags.push((node.range(), tag));
+ }
+
+ for child in node.children() {
+ highlight_tree(tags, &child);
+ }
+ }
+
+ test("= *AB*", &[(0..6, Heading), (2..6, Strong)]);
+
+ test(
+ "#f(x + 1)",
+ &[
+ (0..1, Function),
+ (1..2, Function),
+ (2..3, Punctuation),
+ (5..6, Operator),
+ (7..8, Number),
+ (8..9, Punctuation),
+ ],
+ );
+
+ test(
+ "#let f(x) = x",
+ &[
+ (0..1, Keyword),
+ (1..4, Keyword),
+ (5..6, Function),
+ (6..7, Punctuation),
+ (8..9, Punctuation),
+ (10..11, Operator),
+ ],
+ );
+ }
+}
diff --git a/crates/typst-syntax/src/lib.rs b/crates/typst-syntax/src/lib.rs
index dec9a751..4ee37096 100644
--- a/crates/typst-syntax/src/lib.rs
+++ b/crates/typst-syntax/src/lib.rs
@@ -3,6 +3,7 @@
pub mod ast;
mod file;
+mod highlight;
mod kind;
mod lexer;
mod node;
@@ -12,6 +13,7 @@ mod source;
mod span;
pub use self::file::{FileId, PackageSpec, PackageVersion, VirtualPath};
+pub use self::highlight::{highlight, highlight_html, Tag};
pub use self::kind::SyntaxKind;
pub use self::lexer::{is_id_continue, is_id_start, is_ident, is_newline};
pub use self::node::{LinkedChildren, LinkedNode, SyntaxError, SyntaxNode};