mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
HTML highlighting
This commit is contained in:
parent
196d9594fb
commit
c987f07b76
@ -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("<"),
|
||||||
|
'>' => 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("</span>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user