From c987f07b76b18d8762a0ef48740ecc71722540f0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 29 Jan 2023 23:23:03 +0100 Subject: [PATCH] HTML highlighting --- src/ide/highlight.rs | 80 +++++++++++++++++++++++++++++++++++++++++--- src/ide/mod.rs | 10 +++--- src/syntax/node.rs | 51 ++++++++++++++++++---------- src/syntax/parser.rs | 7 ++-- 4 files changed, 118 insertions(+), 30 deletions(-) diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index ede13d7f6..2e418e224 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -1,4 +1,4 @@ -use crate::syntax::{ast, LinkedNode, SyntaxKind}; +use crate::syntax::{ast, LinkedNode, SyntaxKind, SyntaxNode}; /// Syntax highlighting categories. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -75,12 +75,38 @@ impl Category { Self::Error => "invalid.typst", } } + + /// The recommended CSS class for the highlighting category. + 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", + } + } } -/// Highlight a linked syntax node. +/// Determine the highlight category of a linked syntax node. /// -/// Produces a highlighting category or `None` if the node should not be -/// highlighted. +/// Returns `None` if the node should not be highlighted. pub fn highlight(node: &LinkedNode) -> Option { match node.kind() { SyntaxKind::Markup @@ -285,6 +311,52 @@ 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](Category::css_class). +pub fn highlight_html(root: &SyntaxNode) -> String { + let mut buf = String::from(""); + let node = LinkedNode::new(root); + highlight_html_impl(&mut buf, &node); + buf.push_str(""); + buf +} + +/// Highlight one source node, emitting HTML. +fn highlight_html_impl(html: &mut String, node: &LinkedNode) { + let mut span = false; + if let Some(category) = highlight(node) { + if category != Category::Error { + span = true; + html.push_str(""); + } + } + + let text = node.text(); + if !text.is_empty() { + for c in text.chars() { + match c { + '<' => html.push_str("<"), + '>' => html.push_str(">"), + '&' => html.push_str("&"), + '\'' => html.push_str("'"), + '"' => html.push_str("""), + _ => html.push(c), + } + } + } else { + for child in node.children() { + highlight_html_impl(html, &child); + } + } + + if span { + html.push_str(""); + } +} + #[cfg(test)] mod tests { use std::ops::Range; diff --git a/src/ide/mod.rs b/src/ide/mod.rs index ac69b38a8..4999da523 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -5,13 +5,13 @@ mod complete; mod highlight; mod tooltip; -pub use analyze::*; -pub use complete::*; -pub use highlight::*; -pub use tooltip::*; +pub use self::complete::*; +pub use self::highlight::*; +pub use self::tooltip::*; use std::fmt::Write; +use self::analyze::*; use crate::font::{FontInfo, FontStyle}; /// Extract the first sentence of plain text of a piece of documentation. @@ -60,7 +60,7 @@ fn plain_docs_sentence(docs: &str) -> String { } /// Create a short description of a font family. -pub fn summarize_font_family<'a>(variants: impl Iterator) -> String { +fn summarize_font_family<'a>(variants: impl Iterator) -> String { let mut infos: Vec<_> = variants.collect(); infos.sort_by_key(|info| info.variant); diff --git a/src/syntax/node.rs b/src/syntax/node.rs index 049275ed0..1fdb0a838 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -35,8 +35,12 @@ impl SyntaxNode { } /// Create a new error node. - pub fn error(message: impl Into, pos: ErrorPos, len: usize) -> Self { - Self(Repr::Error(Arc::new(ErrorNode::new(message, pos, len)))) + pub fn error( + message: impl Into, + text: impl Into, + pos: ErrorPos, + ) -> Self { + Self(Repr::Error(Arc::new(ErrorNode::new(message, text, pos)))) } /// The type of the node. @@ -53,7 +57,7 @@ impl SyntaxNode { match &self.0 { Repr::Leaf(leaf) => leaf.len(), Repr::Inner(inner) => inner.len, - Repr::Error(error) => error.len, + Repr::Error(error) => error.len(), } } @@ -68,22 +72,26 @@ impl SyntaxNode { /// The text of the node if it is a leaf node. /// - /// Returns an empty string if this is an inner or error node. + /// Returns the empty string if this is an inner node. pub fn text(&self) -> &EcoString { static EMPTY: EcoString = EcoString::new(); match &self.0 { Repr::Leaf(leaf) => &leaf.text, - Repr::Inner(_) | Repr::Error(_) => &EMPTY, + Repr::Error(error) => &error.text, + Repr::Inner(_) => &EMPTY, } } /// Extract the text from the node. /// - /// Returns an empty string if this is an inner or error node. + /// Builds the string if this is an inner node. pub fn into_text(self) -> EcoString { match self.0 { Repr::Leaf(leaf) => leaf.text, - Repr::Inner(_) | Repr::Error(_) => EcoString::new(), + Repr::Error(error) => error.text.clone(), + Repr::Inner(node) => { + node.children.iter().cloned().map(Self::into_text).collect() + } } } @@ -162,8 +170,8 @@ impl SyntaxNode { /// Convert the child to an error. pub(super) fn convert_to_error(&mut self, message: impl Into) { - let len = self.len(); - *self = SyntaxNode::error(message, ErrorPos::Full, len); + let text = std::mem::take(self).into_text(); + *self = SyntaxNode::error(message, text, ErrorPos::Full); } /// Set a synthetic span for the node and all its descendants. @@ -204,7 +212,7 @@ impl SyntaxNode { } /// Whether this is a leaf node. - pub(super) fn is_leaf(&self) -> bool { + pub(crate) fn is_leaf(&self) -> bool { matches!(self.0, Repr::Leaf(_)) } @@ -278,7 +286,7 @@ impl Debug for SyntaxNode { impl Default for SyntaxNode { fn default() -> Self { - Self::error("", ErrorPos::Full, 0) + Self::error("", "", ErrorPos::Full) } } @@ -580,35 +588,44 @@ impl PartialEq for InnerNode { struct ErrorNode { /// The error message. message: EcoString, + /// The source text of the node. + text: EcoString, /// Where in the node an error should be annotated. pos: ErrorPos, - /// The byte length of the error in the source. - len: usize, /// The node's span. span: Span, } impl ErrorNode { /// Create new error node. - fn new(message: impl Into, pos: ErrorPos, len: usize) -> Self { + fn new( + message: impl Into, + text: impl Into, + pos: ErrorPos, + ) -> Self { Self { message: message.into(), + text: text.into(), pos, - len, span: Span::detached(), } } + + /// The byte length of the node in the source text. + fn len(&self) -> usize { + self.text.len() + } } impl Debug for ErrorNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Error: {} ({})", self.len, self.message) + write!(f, "Error: {:?} ({})", self.text, self.message) } } impl PartialEq for ErrorNode { fn eq(&self, other: &Self) -> bool { - self.message == other.message && self.pos == other.pos && self.len == other.len + self.message == other.message && self.text == other.text && self.pos == other.pos } } diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index ad81cfa34..b51de59e9 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -1198,12 +1198,11 @@ impl<'s> Parser<'s> { } fn save(&mut self) { + let text = self.current_text(); if self.at(SyntaxKind::Error) { let (message, pos) = self.lexer.take_error().unwrap(); - let len = self.current_end() - self.current_start; - self.nodes.push(SyntaxNode::error(message, pos, len)); + self.nodes.push(SyntaxNode::error(message, text, pos)); } else { - let text = self.current_text(); self.nodes.push(SyntaxNode::leaf(self.current, text)); } @@ -1243,7 +1242,7 @@ impl<'s> Parser<'s> { .map_or(true, |child| child.kind() != SyntaxKind::Error) { let message = format_eco!("expected {}", thing); - self.nodes.push(SyntaxNode::error(message, ErrorPos::Full, 0)); + self.nodes.push(SyntaxNode::error(message, "", ErrorPos::Full)); } self.skip(); }