HTML highlighting

This commit is contained in:
Laurenz 2023-01-29 23:23:03 +01:00
parent 196d9594fb
commit c987f07b76
4 changed files with 118 additions and 30 deletions

View File

@ -1,4 +1,4 @@
use crate::syntax::{ast, LinkedNode, SyntaxKind}; use crate::syntax::{ast, LinkedNode, SyntaxKind, SyntaxNode};
/// Syntax highlighting categories. /// Syntax highlighting categories.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -75,12 +75,38 @@ impl Category {
Self::Error => "invalid.typst", 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 /// Returns `None` if the node should not be highlighted.
/// highlighted.
pub fn highlight(node: &LinkedNode) -> Option<Category> { pub fn highlight(node: &LinkedNode) -> Option<Category> {
match node.kind() { match node.kind() {
SyntaxKind::Markup SyntaxKind::Markup
@ -285,6 +311,52 @@ fn is_ident(node: &LinkedNode) -> bool {
matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) 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("<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(category) = highlight(node) {
if category != Category::Error {
span = true;
html.push_str("<span class=\"");
html.push_str(category.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)] #[cfg(test)]
mod tests { mod tests {
use std::ops::Range; use std::ops::Range;

View File

@ -5,13 +5,13 @@ mod complete;
mod highlight; mod highlight;
mod tooltip; mod tooltip;
pub use analyze::*; pub use self::complete::*;
pub use complete::*; pub use self::highlight::*;
pub use highlight::*; pub use self::tooltip::*;
pub use tooltip::*;
use std::fmt::Write; use std::fmt::Write;
use self::analyze::*;
use crate::font::{FontInfo, FontStyle}; use crate::font::{FontInfo, FontStyle};
/// Extract the first sentence of plain text of a piece of documentation. /// 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. /// Create a short description of a font family.
pub fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> String { fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> String {
let mut infos: Vec<_> = variants.collect(); let mut infos: Vec<_> = variants.collect();
infos.sort_by_key(|info| info.variant); infos.sort_by_key(|info| info.variant);

View File

@ -35,8 +35,12 @@ impl SyntaxNode {
} }
/// Create a new error node. /// Create a new error node.
pub fn error(message: impl Into<EcoString>, pos: ErrorPos, len: usize) -> Self { pub fn error(
Self(Repr::Error(Arc::new(ErrorNode::new(message, pos, len)))) message: impl Into<EcoString>,
text: impl Into<EcoString>,
pos: ErrorPos,
) -> Self {
Self(Repr::Error(Arc::new(ErrorNode::new(message, text, pos))))
} }
/// The type of the node. /// The type of the node.
@ -53,7 +57,7 @@ impl SyntaxNode {
match &self.0 { match &self.0 {
Repr::Leaf(leaf) => leaf.len(), Repr::Leaf(leaf) => leaf.len(),
Repr::Inner(inner) => inner.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. /// 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 { pub fn text(&self) -> &EcoString {
static EMPTY: EcoString = EcoString::new(); static EMPTY: EcoString = EcoString::new();
match &self.0 { match &self.0 {
Repr::Leaf(leaf) => &leaf.text, Repr::Leaf(leaf) => &leaf.text,
Repr::Inner(_) | Repr::Error(_) => &EMPTY, Repr::Error(error) => &error.text,
Repr::Inner(_) => &EMPTY,
} }
} }
/// Extract the text from the node. /// 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 { pub fn into_text(self) -> EcoString {
match self.0 { match self.0 {
Repr::Leaf(leaf) => leaf.text, 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. /// Convert the child to an error.
pub(super) fn convert_to_error(&mut self, message: impl Into<EcoString>) { pub(super) fn convert_to_error(&mut self, message: impl Into<EcoString>) {
let len = self.len(); let text = std::mem::take(self).into_text();
*self = SyntaxNode::error(message, ErrorPos::Full, len); *self = SyntaxNode::error(message, text, ErrorPos::Full);
} }
/// Set a synthetic span for the node and all its descendants. /// Set a synthetic span for the node and all its descendants.
@ -204,7 +212,7 @@ impl SyntaxNode {
} }
/// Whether this is a leaf node. /// 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(_)) matches!(self.0, Repr::Leaf(_))
} }
@ -278,7 +286,7 @@ impl Debug for SyntaxNode {
impl Default for SyntaxNode { impl Default for SyntaxNode {
fn default() -> Self { fn default() -> Self {
Self::error("", ErrorPos::Full, 0) Self::error("", "", ErrorPos::Full)
} }
} }
@ -580,35 +588,44 @@ impl PartialEq for InnerNode {
struct ErrorNode { struct ErrorNode {
/// The error message. /// The error message.
message: EcoString, message: EcoString,
/// The source text of the node.
text: EcoString,
/// Where in the node an error should be annotated. /// Where in the node an error should be annotated.
pos: ErrorPos, pos: ErrorPos,
/// The byte length of the error in the source.
len: usize,
/// The node's span. /// The node's span.
span: Span, span: Span,
} }
impl ErrorNode { impl ErrorNode {
/// Create new error node. /// Create new error node.
fn new(message: impl Into<EcoString>, pos: ErrorPos, len: usize) -> Self { fn new(
message: impl Into<EcoString>,
text: impl Into<EcoString>,
pos: ErrorPos,
) -> Self {
Self { Self {
message: message.into(), message: message.into(),
text: text.into(),
pos, pos,
len,
span: Span::detached(), span: Span::detached(),
} }
} }
/// The byte length of the node in the source text.
fn len(&self) -> usize {
self.text.len()
}
} }
impl Debug for ErrorNode { impl Debug for ErrorNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { 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 { impl PartialEq for ErrorNode {
fn eq(&self, other: &Self) -> bool { 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
} }
} }

View File

@ -1198,12 +1198,11 @@ impl<'s> Parser<'s> {
} }
fn save(&mut self) { fn save(&mut self) {
let text = self.current_text();
if self.at(SyntaxKind::Error) { if self.at(SyntaxKind::Error) {
let (message, pos) = self.lexer.take_error().unwrap(); let (message, pos) = self.lexer.take_error().unwrap();
let len = self.current_end() - self.current_start; self.nodes.push(SyntaxNode::error(message, text, pos));
self.nodes.push(SyntaxNode::error(message, pos, len));
} else { } else {
let text = self.current_text();
self.nodes.push(SyntaxNode::leaf(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) .map_or(true, |child| child.kind() != SyntaxKind::Error)
{ {
let message = format_eco!("expected {}", thing); 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(); self.skip();
} }