//! A typed layer over the untyped syntax tree. //! //! The AST is rooted in the [`Markup`] node. use std::num::NonZeroUsize; use std::ops::Deref; use unscanny::Scanner; use super::{ is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode, }; use crate::geom::{AbsUnit, AngleUnit}; use crate::util::EcoString; /// A typed AST node. pub trait AstNode: Sized { /// Convert a node into its typed variant. fn from_untyped(node: &SyntaxNode) -> Option; /// A reference to the underlying syntax node. fn as_untyped(&self) -> &SyntaxNode; /// The source code location. fn span(&self) -> Span { self.as_untyped().span() } } macro_rules! node { ($(#[$attr:meta])* $name:ident) => { #[derive(Debug, Default, Clone, PartialEq, Hash)] #[repr(transparent)] $(#[$attr])* pub struct $name(SyntaxNode); impl AstNode for $name { fn from_untyped(node: &SyntaxNode) -> Option { if matches!(node.kind(), SyntaxKind::$name) { Some(Self(node.clone())) } else { Option::None } } fn as_untyped(&self) -> &SyntaxNode { &self.0 } } }; } node! { /// The syntactical root capable of representing a full parsed document. Markup } impl Markup { /// The expressions. pub fn exprs(&self) -> impl DoubleEndedIterator + '_ { let mut was_stmt = false; self.0 .children() .filter(move |node| { // Ignore newline directly after statements without semicolons. let kind = node.kind(); let keep = !was_stmt || node.kind() != SyntaxKind::Space; was_stmt = kind.is_stmt(); keep }) .filter_map(Expr::cast_with_space) } } /// An expression in markup, math or code. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Expr { /// Plain text without markup. Text(Text), /// Whitespace in markup or math. Has at most one newline in markup, as more /// indicate a paragraph break. Space(Space), /// A forced line break: `\`. Linebreak(Linebreak), /// A paragraph break, indicated by one or multiple blank lines. Parbreak(Parbreak), /// An escape sequence: `\#`, `\u{1F5FA}`. Escape(Escape), /// A shorthand for a unicode codepoint. For example, `~` for non-breaking /// space or `-?` for a soft hyphen. Shorthand(Shorthand), /// A smart quote: `'` or `"`. SmartQuote(SmartQuote), /// Strong content: `*Strong*`. Strong(Strong), /// Emphasized content: `_Emphasized_`. Emph(Emph), /// Raw text with optional syntax highlighting: `` `...` ``. Raw(Raw), /// A hyperlink: `https://typst.org`. Link(Link), /// A label: ``. Label(Label), /// A reference: `@target`. Ref(Ref), /// A section heading: `= Introduction`. Heading(Heading), /// An item in a bullet list: `- ...`. List(ListItem), /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. Enum(EnumItem), /// An item in a term list: `/ Term: Details`. Term(TermItem), /// A math formula: `$x$`, `$ x^2 $`. Formula(Formula), /// A math formula: `$x$`, `$ x^2 $`. Math(Math), /// An identifier in a math formula: `pi`. MathIdent(MathIdent), /// An alignment point in a math formula: `&`. MathAlignPoint(MathAlignPoint), /// Matched delimiters surrounding math in a formula: `[x + y]`. MathDelimited(MathDelimited), /// A base with optional attachments in a formula: `a_1^2`. MathAttach(MathAttach), /// A fraction in a math formula: `x/2`. MathFrac(MathFrac), /// An identifier: `left`. Ident(Ident), /// The `none` literal. None(None), /// The `auto` literal. Auto(Auto), /// A boolean: `true`, `false`. Bool(Bool), /// An integer: `120`. Int(Int), /// A floating-point number: `1.2`, `10e-4`. Float(Float), /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. Numeric(Numeric), /// A quoted string: `"..."`. Str(Str), /// A code block: `{ let x = 1; x + 2 }`. Code(CodeBlock), /// A content block: `[*Hi* there!]`. Content(ContentBlock), /// A grouped expression: `(1 + 2)`. Parenthesized(Parenthesized), /// An array: `(1, "hi", 12cm)`. Array(Array), /// A dictionary: `(thickness: 3pt, pattern: dashed)`. Dict(Dict), /// A unary operation: `-x`. Unary(Unary), /// A binary operation: `a + b`. Binary(Binary), /// A field access: `properties.age`. FieldAccess(FieldAccess), /// An invocation of a function or method: `f(x, y)`. FuncCall(FuncCall), /// A closure: `(x, y) => z`. Closure(Closure), /// A let binding: `let x = 1`. Let(LetBinding), /// A set rule: `set text(...)`. Set(SetRule), /// A show rule: `show heading: it => [*{it.body}*]`. Show(ShowRule), /// An if-else conditional: `if x { y } else { z }`. Conditional(Conditional), /// A while loop: `while x { y }`. While(WhileLoop), /// A for loop: `for x in y { z }`. For(ForLoop), /// A module import: `import a, b, c from "utils.typ"`. Import(ModuleImport), /// A module include: `include "chapter1.typ"`. Include(ModuleInclude), /// A break from a loop: `break`. Break(LoopBreak), /// A continue in a loop: `continue`. Continue(LoopContinue), /// A return from a function: `return`, `return x + 1`. Return(FuncReturn), } impl Expr { fn cast_with_space(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Space => node.cast().map(Self::Space), _ => Self::from_untyped(node), } } } impl AstNode for Expr { fn from_untyped(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), SyntaxKind::Parbreak => node.cast().map(Self::Parbreak), SyntaxKind::Text => node.cast().map(Self::Text), SyntaxKind::Escape => node.cast().map(Self::Escape), SyntaxKind::Shorthand => node.cast().map(Self::Shorthand), SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote), SyntaxKind::Strong => node.cast().map(Self::Strong), SyntaxKind::Emph => node.cast().map(Self::Emph), SyntaxKind::Raw => node.cast().map(Self::Raw), SyntaxKind::Link => node.cast().map(Self::Link), SyntaxKind::Label => node.cast().map(Self::Label), SyntaxKind::Ref => node.cast().map(Self::Ref), SyntaxKind::Heading => node.cast().map(Self::Heading), SyntaxKind::ListItem => node.cast().map(Self::List), SyntaxKind::EnumItem => node.cast().map(Self::Enum), SyntaxKind::TermItem => node.cast().map(Self::Term), SyntaxKind::Formula => node.cast().map(Self::Formula), SyntaxKind::Math => node.cast().map(Self::Math), SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), SyntaxKind::Ident => node.cast().map(Self::Ident), SyntaxKind::None => node.cast().map(Self::None), SyntaxKind::Auto => node.cast().map(Self::Auto), SyntaxKind::Bool => node.cast().map(Self::Bool), SyntaxKind::Int => node.cast().map(Self::Int), SyntaxKind::Float => node.cast().map(Self::Float), SyntaxKind::Numeric => node.cast().map(Self::Numeric), SyntaxKind::Str => node.cast().map(Self::Str), SyntaxKind::CodeBlock => node.cast().map(Self::Code), SyntaxKind::ContentBlock => node.cast().map(Self::Content), SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), SyntaxKind::Array => node.cast().map(Self::Array), SyntaxKind::Dict => node.cast().map(Self::Dict), SyntaxKind::Unary => node.cast().map(Self::Unary), SyntaxKind::Binary => node.cast().map(Self::Binary), SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess), SyntaxKind::FuncCall => node.cast().map(Self::FuncCall), SyntaxKind::Closure => node.cast().map(Self::Closure), SyntaxKind::LetBinding => node.cast().map(Self::Let), SyntaxKind::SetRule => node.cast().map(Self::Set), SyntaxKind::ShowRule => node.cast().map(Self::Show), SyntaxKind::Conditional => node.cast().map(Self::Conditional), SyntaxKind::WhileLoop => node.cast().map(Self::While), SyntaxKind::ForLoop => node.cast().map(Self::For), SyntaxKind::ModuleImport => node.cast().map(Self::Import), SyntaxKind::ModuleInclude => node.cast().map(Self::Include), SyntaxKind::LoopBreak => node.cast().map(Self::Break), SyntaxKind::LoopContinue => node.cast().map(Self::Continue), SyntaxKind::FuncReturn => node.cast().map(Self::Return), _ => Option::None, } } fn as_untyped(&self) -> &SyntaxNode { match self { Self::Text(v) => v.as_untyped(), Self::Space(v) => v.as_untyped(), Self::Linebreak(v) => v.as_untyped(), Self::Parbreak(v) => v.as_untyped(), Self::Escape(v) => v.as_untyped(), Self::Shorthand(v) => v.as_untyped(), Self::SmartQuote(v) => v.as_untyped(), Self::Strong(v) => v.as_untyped(), Self::Emph(v) => v.as_untyped(), Self::Raw(v) => v.as_untyped(), Self::Link(v) => v.as_untyped(), Self::Label(v) => v.as_untyped(), Self::Ref(v) => v.as_untyped(), Self::Heading(v) => v.as_untyped(), Self::List(v) => v.as_untyped(), Self::Enum(v) => v.as_untyped(), Self::Term(v) => v.as_untyped(), Self::Formula(v) => v.as_untyped(), Self::Math(v) => v.as_untyped(), Self::MathIdent(v) => v.as_untyped(), Self::MathAlignPoint(v) => v.as_untyped(), Self::MathDelimited(v) => v.as_untyped(), Self::MathAttach(v) => v.as_untyped(), Self::MathFrac(v) => v.as_untyped(), Self::Ident(v) => v.as_untyped(), Self::None(v) => v.as_untyped(), Self::Auto(v) => v.as_untyped(), Self::Bool(v) => v.as_untyped(), Self::Int(v) => v.as_untyped(), Self::Float(v) => v.as_untyped(), Self::Numeric(v) => v.as_untyped(), Self::Str(v) => v.as_untyped(), Self::Code(v) => v.as_untyped(), Self::Content(v) => v.as_untyped(), Self::Array(v) => v.as_untyped(), Self::Dict(v) => v.as_untyped(), Self::Parenthesized(v) => v.as_untyped(), Self::Unary(v) => v.as_untyped(), Self::Binary(v) => v.as_untyped(), Self::FieldAccess(v) => v.as_untyped(), Self::FuncCall(v) => v.as_untyped(), Self::Closure(v) => v.as_untyped(), Self::Let(v) => v.as_untyped(), Self::Set(v) => v.as_untyped(), Self::Show(v) => v.as_untyped(), Self::Conditional(v) => v.as_untyped(), Self::While(v) => v.as_untyped(), Self::For(v) => v.as_untyped(), Self::Import(v) => v.as_untyped(), Self::Include(v) => v.as_untyped(), Self::Break(v) => v.as_untyped(), Self::Continue(v) => v.as_untyped(), Self::Return(v) => v.as_untyped(), } } } impl Expr { /// Can this expression be embedded into markup with a hashtag? pub fn hashtag(&self) -> bool { match self { Self::Ident(_) => true, Self::None(_) => true, Self::Auto(_) => true, Self::Bool(_) => true, Self::Int(_) => true, Self::Float(_) => true, Self::Numeric(_) => true, Self::Str(_) => true, Self::Code(_) => true, Self::Content(_) => true, Self::Array(_) => true, Self::Dict(_) => true, Self::Parenthesized(_) => true, Self::FieldAccess(_) => true, Self::FuncCall(_) => true, Self::Let(_) => true, Self::Set(_) => true, Self::Show(_) => true, Self::Conditional(_) => true, Self::While(_) => true, Self::For(_) => true, Self::Import(_) => true, Self::Include(_) => true, Self::Break(_) => true, Self::Continue(_) => true, Self::Return(_) => true, _ => false, } } /// Is this a literal? pub fn is_literal(&self) -> bool { match self { Self::None(_) => true, Self::Auto(_) => true, Self::Bool(_) => true, Self::Int(_) => true, Self::Float(_) => true, Self::Numeric(_) => true, Self::Str(_) => true, _ => false, } } } impl Default for Expr { fn default() -> Self { Expr::Space(Space::default()) } } node! { /// Plain text without markup. Text } impl Text { /// Get the text. pub fn get(&self) -> &EcoString { self.0.text() } } node! { /// Whitespace in markup or math. Has at most one newline in markup, as more /// indicate a paragraph break. Space } node! { /// A forced line break: `\`. Linebreak } node! { /// A paragraph break, indicated by one or multiple blank lines. Parbreak } node! { /// An escape sequence: `\#`, `\u{1F5FA}`. Escape } impl Escape { /// Get the escaped character. pub fn get(&self) -> char { let mut s = Scanner::new(self.0.text()); s.expect('\\'); if s.eat_if("u{") { let hex = s.eat_while(char::is_ascii_hexdigit); u32::from_str_radix(hex, 16) .ok() .and_then(std::char::from_u32) .unwrap_or_default() } else { s.eat().unwrap_or_default() } } } node! { /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking /// space or `-?` for a soft hyphen. Shorthand } impl Shorthand { /// A list of all shorthands. pub const LIST: &[(&'static str, char)] = &[ // Text only. ("~", '\u{00A0}'), ("--", '\u{2013}'), ("---", '\u{2014}'), ("-?", '\u{00AD}'), // Math only. ("-", '\u{2212}'), ("'", '′'), ("*", '∗'), ("!=", '≠'), ("<<", '≪'), ("<<<", '⋘'), (">>", '≫'), (">>>", '⋙'), ("<=", '≤'), (">=", '≥'), ("<-", '←'), ("->", '→'), ("=>", '⇒'), ("|->", '↦'), ("|=>", '⤇'), ("<->", '↔'), ("<=>", '⇔'), (":=", '≔'), ("[|", '⟦'), ("|]", '⟧'), ("||", '‖'), // Both. ("...", '…'), ]; /// Get the shorthanded character. pub fn get(&self) -> char { let text = self.0.text().as_str(); Self::LIST .iter() .find(|&&(s, _)| s == text) .map_or_else(char::default, |&(_, c)| c) } } node! { /// A smart quote: `'` or `"`. SmartQuote } impl SmartQuote { /// Whether this is a double quote. pub fn double(&self) -> bool { self.0.text() == "\"" } } node! { /// Strong content: `*Strong*`. Strong } impl Strong { /// The contents of the strong node. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } } node! { /// Emphasized content: `_Emphasized_`. Emph } impl Emph { /// The contents of the emphasis node. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } } node! { /// Raw text with optional syntax highlighting: `` `...` ``. Raw } impl Raw { /// The trimmed raw text. pub fn text(&self) -> EcoString { let mut text = self.0.text().as_str(); let blocky = text.starts_with("```"); text = text.trim_matches('`'); // Trim tag, one space at the start, and one space at the end if the // last non-whitespace char is a backtick. if blocky { let mut s = Scanner::new(text); if s.eat_if(is_id_start) { s.eat_while(is_id_continue); } text = s.after(); text = text.strip_prefix(' ').unwrap_or(text); if text.trim_end().ends_with('`') { text = text.strip_suffix(' ').unwrap_or(text); } } // Split into lines. let mut lines = split_newlines(text); if blocky { let dedent = lines .iter() .skip(1) .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) .min() .unwrap_or(0); // Dedent based on column, but not for the first line. for line in lines.iter_mut().skip(1) { let offset = line.chars().take(dedent).map(char::len_utf8).sum(); *line = &line[offset..]; } let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace); // Trims a sequence of whitespace followed by a newline at the start. if lines.first().map_or(false, is_whitespace) { lines.remove(0); } // Trims a newline followed by a sequence of whitespace at the end. if lines.last().map_or(false, is_whitespace) { lines.pop(); } } lines.join("\n").into() } /// An optional identifier specifying the language to syntax-highlight in. pub fn lang(&self) -> Option<&str> { let inner = self.0.text().trim_start_matches('`'); let mut s = Scanner::new(inner); s.eat_if(is_id_start).then(|| { s.eat_while(is_id_continue); s.before() }) } /// Whether the raw text should be displayed in a separate block. pub fn block(&self) -> bool { let text = self.0.text(); text.starts_with("```") && text.chars().any(is_newline) } } node! { /// A hyperlink: `https://typst.org`. Link } impl Link { /// Get the URL. pub fn get(&self) -> &EcoString { self.0.text() } } node! { /// A label: ``. Label } impl Label { /// Get the label's text. pub fn get(&self) -> &str { self.0.text().trim_start_matches('<').trim_end_matches('>') } } node! { /// A reference: `@target`. Ref } impl Ref { /// Get the target. pub fn get(&self) -> &str { self.0.text().trim_start_matches('@') } } node! { /// A section heading: `= Introduction`. Heading } impl Heading { /// The contents of the heading. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } /// The section depth (numer of equals signs). pub fn level(&self) -> NonZeroUsize { self.0 .children() .find(|node| node.kind() == SyntaxKind::HeadingMarker) .and_then(|node| node.len().try_into().ok()) .unwrap_or(NonZeroUsize::new(1).unwrap()) } } node! { /// An item in a bullet list: `- ...`. ListItem } impl ListItem { /// The contents of the list item. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } } node! { /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. EnumItem } impl EnumItem { /// The explicit numbering, if any: `23.`. pub fn number(&self) -> Option { self.0.children().find_map(|node| match node.kind() { SyntaxKind::EnumMarker => node.text().trim_end_matches('.').parse().ok(), _ => Option::None, }) } /// The contents of the list item. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } } node! { /// An item in a term list: `/ Term: Details`. TermItem } impl TermItem { /// The term described by the item. pub fn term(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } /// The description of the term. pub fn description(&self) -> Markup { self.0.cast_last_match().unwrap_or_default() } } node! { /// A math formula: `$x$`, `$ x^2 $`. Formula } impl Formula { /// The contained math. pub fn body(&self) -> Math { self.0.cast_first_match().unwrap_or_default() } /// Whether the formula should be displayed as a separate block. pub fn block(&self) -> bool { let is_space = |node: Option<&SyntaxNode>| { node.map(SyntaxNode::kind) == Some(SyntaxKind::Space) }; is_space(self.0.children().nth(1)) && is_space(self.0.children().nth_back(1)) } } node! { /// Math markup. Math } impl Math { /// The expressions the mathematical content consists of. pub fn exprs(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(Expr::cast_with_space) } } node! { /// An identifier in a math formula: `pi`. MathIdent } impl MathIdent { /// Get the identifier. pub fn get(&self) -> &EcoString { self.0.text() } /// Take out the contained identifier. pub fn take(self) -> EcoString { self.0.into_text() } /// Get the identifier as a string slice. pub fn as_str(&self) -> &str { self.get() } } impl Deref for MathIdent { type Target = str; fn deref(&self) -> &Self::Target { self.as_str() } } node! { /// An alignment point in a formula: `&`. MathAlignPoint } node! { /// Matched delimiters surrounding math in a formula: `[x + y]`. MathDelimited } impl MathDelimited { /// The opening delimiter. pub fn open(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The contents, including the delimiters. pub fn body(&self) -> Math { self.0.cast_first_match().unwrap_or_default() } /// The closing delimiter. pub fn close(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A base with optional attachments in a formula: `a_1^2`. MathAttach } impl MathAttach { /// The base, to which things are attached. pub fn base(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The bottom attachment. pub fn bottom(&self) -> Option { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore)) .find_map(SyntaxNode::cast) } /// The top attachment. pub fn top(&self) -> Option { self.0 .children() .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat)) .find_map(SyntaxNode::cast) } } node! { /// A fraction in a formula: `x/2` MathFrac } impl MathFrac { /// The numerator. pub fn num(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The denominator. pub fn denom(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// An identifier: `it`. Ident } impl Ident { /// Get the identifier. pub fn get(&self) -> &EcoString { self.0.text() } /// Take out the contained identifier. pub fn take(self) -> EcoString { self.0.into_text() } /// Get the identifier as a string slice. pub fn as_str(&self) -> &str { self.get() } } impl Deref for Ident { type Target = str; fn deref(&self) -> &Self::Target { self.as_str() } } node! { /// The `none` literal. None } node! { /// The `auto` literal. Auto } node! { /// A boolean: `true`, `false`. Bool } impl Bool { /// Get the boolean value. pub fn get(&self) -> bool { self.0.text() == "true" } } node! { /// An integer: `120`. Int } impl Int { /// Get the integer value. pub fn get(&self) -> i64 { self.0.text().parse().unwrap_or_default() } } node! { /// A floating-point number: `1.2`, `10e-4`. Float } impl Float { /// Get the floating-point value. pub fn get(&self) -> f64 { self.0.text().parse().unwrap_or_default() } } node! { /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. Numeric } impl Numeric { /// Get the numeric value and unit. pub fn get(&self) -> (f64, Unit) { let text = self.0.text(); let count = text .chars() .rev() .take_while(|c| matches!(c, 'a'..='z' | '%')) .count(); let split = text.len() - count; let value = text[..split].parse().unwrap_or_default(); let unit = match &text[split..] { "pt" => Unit::Length(AbsUnit::Pt), "mm" => Unit::Length(AbsUnit::Mm), "cm" => Unit::Length(AbsUnit::Cm), "in" => Unit::Length(AbsUnit::In), "deg" => Unit::Angle(AngleUnit::Deg), "rad" => Unit::Angle(AngleUnit::Rad), "em" => Unit::Em, "fr" => Unit::Fr, "%" => Unit::Percent, _ => Unit::Percent, }; (value, unit) } } /// Unit of a numeric value. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Unit { /// An absolute length unit. Length(AbsUnit), /// An angular unit. Angle(AngleUnit), /// Font-relative: `1em` is the same as the font size. Em, /// Fractions: `fr`. Fr, /// Percentage: `%`. Percent, } node! { /// A quoted string: `"..."`. Str } impl Str { /// Get the string value with resolved escape sequences. pub fn get(&self) -> EcoString { let text = self.0.text(); let unquoted = &text[1..text.len() - 1]; if !unquoted.contains('\\') { return unquoted.into(); } let mut out = EcoString::with_capacity(unquoted.len()); let mut s = Scanner::new(unquoted); while let Some(c) = s.eat() { if c != '\\' { out.push(c); continue; } let start = s.locate(-1); match s.eat() { Some('\\') => out.push('\\'), Some('"') => out.push('"'), Some('n') => out.push('\n'), Some('r') => out.push('\r'), Some('t') => out.push('\t'), Some('u') if s.eat_if('{') => { let sequence = s.eat_while(char::is_ascii_hexdigit); s.eat_if('}'); match u32::from_str_radix(sequence, 16) .ok() .and_then(std::char::from_u32) { Some(c) => out.push(c), Option::None => out.push_str(s.from(start)), } } _ => out.push_str(s.from(start)), } } out } } node! { /// A code block: `{ let x = 1; x + 2 }`. CodeBlock } impl CodeBlock { /// The contained code. pub fn body(&self) -> Code { self.0.cast_first_match().unwrap_or_default() } } node! { /// Code. Code } impl Code { /// The list of expressions contained in the code. pub fn exprs(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } node! { /// A content block: `[*Hi* there!]`. ContentBlock } impl ContentBlock { /// The contained markup. pub fn body(&self) -> Markup { self.0.cast_first_match().unwrap_or_default() } } node! { /// A grouped expression: `(1 + 2)`. Parenthesized } impl Parenthesized { /// The wrapped expression. pub fn expr(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } } node! { /// An array: `(1, "hi", 12cm)`. Array } impl Array { /// The array's items. pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } /// An item in an array. #[derive(Debug, Clone, PartialEq, Hash)] pub enum ArrayItem { /// A bare expression: `12`. Pos(Expr), /// A spreaded expression: `..things`. Spread(Expr), } impl AstNode for ArrayItem { fn from_untyped(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), _ => node.cast().map(Self::Pos), } } fn as_untyped(&self) -> &SyntaxNode { match self { Self::Pos(v) => v.as_untyped(), Self::Spread(v) => v.as_untyped(), } } } node! { /// A dictionary: `(thickness: 3pt, pattern: dashed)`. Dict } impl Dict { /// The dictionary's items. pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } /// An item in an dictionary expresssion. #[derive(Debug, Clone, PartialEq, Hash)] pub enum DictItem { /// A named pair: `thickness: 3pt`. Named(Named), /// A keyed pair: `"spacy key": true`. Keyed(Keyed), /// A spreaded expression: `..things`. Spread(Expr), } impl AstNode for DictItem { fn from_untyped(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Named => node.cast().map(Self::Named), SyntaxKind::Keyed => node.cast().map(Self::Keyed), SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), _ => Option::None, } } fn as_untyped(&self) -> &SyntaxNode { match self { Self::Named(v) => v.as_untyped(), Self::Keyed(v) => v.as_untyped(), Self::Spread(v) => v.as_untyped(), } } } node! { /// A named pair: `thickness: 3pt`. Named } impl Named { /// The name: `thickness`. pub fn name(&self) -> Ident { self.0.cast_first_match().unwrap_or_default() } /// The right-hand side of the pair: `3pt`. pub fn expr(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A keyed pair: `"spacy key": true`. Keyed } impl Keyed { /// The key: `"spacy key"`. pub fn key(&self) -> Str { self.0 .children() .find_map(|node| node.cast::()) .unwrap_or_default() } /// The right-hand side of the pair: `true`. pub fn expr(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A unary operation: `-x`. Unary } impl Unary { /// The operator: `-`. pub fn op(&self) -> UnOp { self.0 .children() .find_map(|node| UnOp::from_kind(node.kind())) .unwrap_or(UnOp::Pos) } /// The expression to operate on: `x`. pub fn expr(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } /// A unary operator. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum UnOp { /// The plus operator: `+`. Pos, /// The negation operator: `-`. Neg, /// The boolean `not`. Not, } impl UnOp { /// Try to convert the token into a unary operation. pub fn from_kind(token: SyntaxKind) -> Option { Some(match token { SyntaxKind::Plus => Self::Pos, SyntaxKind::Minus => Self::Neg, SyntaxKind::Not => Self::Not, _ => return Option::None, }) } /// The precedence of this operator. pub fn precedence(self) -> usize { match self { Self::Pos | Self::Neg => 7, Self::Not => 4, } } /// The string representation of this operation. pub fn as_str(self) -> &'static str { match self { Self::Pos => "+", Self::Neg => "-", Self::Not => "not", } } } node! { /// A binary operation: `a + b`. Binary } impl Binary { /// The binary operator: `+`. pub fn op(&self) -> BinOp { let mut not = false; self.0 .children() .find_map(|node| match node.kind() { SyntaxKind::Not => { not = true; Option::None } SyntaxKind::In if not => Some(BinOp::NotIn), _ => BinOp::from_kind(node.kind()), }) .unwrap_or(BinOp::Add) } /// The left-hand side of the operation: `a`. pub fn lhs(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The right-hand side of the operation: `b`. pub fn rhs(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } /// A binary operator. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum BinOp { /// The addition operator: `+`. Add, /// The subtraction operator: `-`. Sub, /// The multiplication operator: `*`. Mul, /// The division operator: `/`. Div, /// The short-circuiting boolean `and`. And, /// The short-circuiting boolean `or`. Or, /// The equality operator: `==`. Eq, /// The inequality operator: `!=`. Neq, /// The less-than operator: `<`. Lt, /// The less-than or equal operator: `<=`. Leq, /// The greater-than operator: `>`. Gt, /// The greater-than or equal operator: `>=`. Geq, /// The assignment operator: `=`. Assign, /// The containment operator: `in`. In, /// The inversed containment operator: `not in`. NotIn, /// The add-assign operator: `+=`. AddAssign, /// The subtract-assign oeprator: `-=`. SubAssign, /// The multiply-assign operator: `*=`. MulAssign, /// The divide-assign operator: `/=`. DivAssign, } impl BinOp { /// Try to convert the token into a binary operation. pub fn from_kind(token: SyntaxKind) -> Option { Some(match token { SyntaxKind::Plus => Self::Add, SyntaxKind::Minus => Self::Sub, SyntaxKind::Star => Self::Mul, SyntaxKind::Slash => Self::Div, SyntaxKind::And => Self::And, SyntaxKind::Or => Self::Or, SyntaxKind::EqEq => Self::Eq, SyntaxKind::ExclEq => Self::Neq, SyntaxKind::Lt => Self::Lt, SyntaxKind::LtEq => Self::Leq, SyntaxKind::Gt => Self::Gt, SyntaxKind::GtEq => Self::Geq, SyntaxKind::Eq => Self::Assign, SyntaxKind::In => Self::In, SyntaxKind::PlusEq => Self::AddAssign, SyntaxKind::HyphEq => Self::SubAssign, SyntaxKind::StarEq => Self::MulAssign, SyntaxKind::SlashEq => Self::DivAssign, _ => return Option::None, }) } /// The precedence of this operator. pub fn precedence(self) -> usize { match self { Self::Mul => 6, Self::Div => 6, Self::Add => 5, Self::Sub => 5, Self::Eq => 4, Self::Neq => 4, Self::Lt => 4, Self::Leq => 4, Self::Gt => 4, Self::Geq => 4, Self::In => 4, Self::NotIn => 4, Self::And => 3, Self::Or => 2, Self::Assign => 1, Self::AddAssign => 1, Self::SubAssign => 1, Self::MulAssign => 1, Self::DivAssign => 1, } } /// The associativity of this operator. pub fn assoc(self) -> Assoc { match self { Self::Add => Assoc::Left, Self::Sub => Assoc::Left, Self::Mul => Assoc::Left, Self::Div => Assoc::Left, Self::And => Assoc::Left, Self::Or => Assoc::Left, Self::Eq => Assoc::Left, Self::Neq => Assoc::Left, Self::Lt => Assoc::Left, Self::Leq => Assoc::Left, Self::Gt => Assoc::Left, Self::Geq => Assoc::Left, Self::In => Assoc::Left, Self::NotIn => Assoc::Left, Self::Assign => Assoc::Right, Self::AddAssign => Assoc::Right, Self::SubAssign => Assoc::Right, Self::MulAssign => Assoc::Right, Self::DivAssign => Assoc::Right, } } /// The string representation of this operation. pub fn as_str(self) -> &'static str { match self { Self::Add => "+", Self::Sub => "-", Self::Mul => "*", Self::Div => "/", Self::And => "and", Self::Or => "or", Self::Eq => "==", Self::Neq => "!=", Self::Lt => "<", Self::Leq => "<=", Self::Gt => ">", Self::Geq => ">=", Self::In => "in", Self::NotIn => "not in", Self::Assign => "=", Self::AddAssign => "+=", Self::SubAssign => "-=", Self::MulAssign => "*=", Self::DivAssign => "/=", } } } /// The associativity of a binary operator. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Assoc { /// Left-associative: `a + b + c` is equivalent to `(a + b) + c`. Left, /// Right-associative: `a = b = c` is equivalent to `a = (b = c)`. Right, } node! { /// A field access: `properties.age`. FieldAccess } impl FieldAccess { /// The expression to access the field on. pub fn target(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The name of the field. pub fn field(&self) -> Ident { self.0.cast_last_match().unwrap_or_default() } } node! { /// An invocation of a function or method: `f(x, y)`. FuncCall } impl FuncCall { /// The function to call. pub fn callee(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The arguments to the function. pub fn args(&self) -> Args { self.0.cast_last_match().unwrap_or_default() } } node! { /// A function call's argument list: `(12pt, y)`. Args } impl Args { /// The positional and named arguments. pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } /// An argument to a function call. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Arg { /// A positional argument: `12`. Pos(Expr), /// A named argument: `draw: false`. Named(Named), /// A spreaded argument: `..things`. Spread(Expr), } impl AstNode for Arg { fn from_untyped(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Named => node.cast().map(Self::Named), SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), _ => node.cast().map(Self::Pos), } } fn as_untyped(&self) -> &SyntaxNode { match self { Self::Pos(v) => v.as_untyped(), Self::Named(v) => v.as_untyped(), Self::Spread(v) => v.as_untyped(), } } } node! { /// A closure: `(x, y) => z`. Closure } impl Closure { /// The name of the closure. /// /// This only exists if you use the function syntax sugar: `let f(x) = y`. pub fn name(&self) -> Option { self.0.children().next()?.cast() } /// The parameter bindings. pub fn params(&self) -> impl DoubleEndedIterator + '_ { self.0 .children() .find(|x| x.kind() == SyntaxKind::Params) .map_or([].iter(), |params| params.children()) .filter_map(SyntaxNode::cast) } /// The body of the closure. pub fn body(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } /// A parameter to a closure. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Param { /// A positional parameter: `x`. Pos(Ident), /// A named parameter with a default value: `draw: false`. Named(Named), /// An argument sink: `..args`. Sink(Ident), } impl AstNode for Param { fn from_untyped(node: &SyntaxNode) -> Option { match node.kind() { SyntaxKind::Ident => node.cast().map(Self::Pos), SyntaxKind::Named => node.cast().map(Self::Named), SyntaxKind::Spread => node.cast_first_match().map(Self::Sink), _ => Option::None, } } fn as_untyped(&self) -> &SyntaxNode { match self { Self::Pos(v) => v.as_untyped(), Self::Named(v) => v.as_untyped(), Self::Sink(v) => v.as_untyped(), } } } node! { /// A let binding: `let x = 1`. LetBinding } impl LetBinding { /// The binding to assign to. pub fn binding(&self) -> Ident { match self.0.cast_first_match() { Some(Expr::Ident(binding)) => binding, Some(Expr::Closure(closure)) => closure.name().unwrap_or_default(), _ => Ident::default(), } } /// The expression the binding is initialized with. pub fn init(&self) -> Option { if self.0.cast_first_match::().is_some() { // This is a normal binding like `let x = 1`. self.0.children().filter_map(SyntaxNode::cast).nth(1) } else { // This is a closure binding like `let f(x) = 1`. self.0.cast_first_match() } } } node! { /// A set rule: `set text(...)`. SetRule } impl SetRule { /// The function to set style properties for. pub fn target(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The style properties to set. pub fn args(&self) -> Args { self.0.cast_last_match().unwrap_or_default() } /// A condition under which the set rule applies. pub fn condition(&self) -> Option { self.0 .children() .skip_while(|child| child.kind() != SyntaxKind::If) .find_map(SyntaxNode::cast) } } node! { /// A show rule: `show heading: it => [*{it.body}*]`. ShowRule } impl ShowRule { /// Defines which nodes the show rule applies to. pub fn selector(&self) -> Option { self.0 .children() .rev() .skip_while(|child| child.kind() != SyntaxKind::Colon) .find_map(SyntaxNode::cast) } /// The transformation recipe. pub fn transform(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// An if-else conditional: `if x { y } else { z }`. Conditional } impl Conditional { /// The condition which selects the body to evaluate. pub fn condition(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The expression to evaluate if the condition is true. pub fn if_body(&self) -> Expr { self.0 .children() .filter_map(SyntaxNode::cast) .nth(1) .unwrap_or_default() } /// The expression to evaluate if the condition is false. pub fn else_body(&self) -> Option { self.0.children().filter_map(SyntaxNode::cast).nth(2) } } node! { /// A while loop: `while x { y }`. WhileLoop } impl WhileLoop { /// The condition which selects whether to evaluate the body. pub fn condition(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The expression to evaluate while the condition is true. pub fn body(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A for loop: `for x in y { z }`. ForLoop } impl ForLoop { /// The pattern to assign to. pub fn pattern(&self) -> ForPattern { self.0.cast_first_match().unwrap_or_default() } /// The expression to iterate over. pub fn iter(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The expression to evaluate for each iteration. pub fn body(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A for loop's destructuring pattern: `x` or `x, y`. ForPattern } impl ForPattern { /// The key part of the pattern: index for arrays, name for dictionaries. pub fn key(&self) -> Option { let mut children = self.0.children().filter_map(SyntaxNode::cast); let key = children.next(); if children.next().is_some() { key } else { Option::None } } /// The value part of the pattern. pub fn value(&self) -> Ident { self.0.cast_last_match().unwrap_or_default() } } node! { /// A module import: `import "utils.typ": a, b, c`. ModuleImport } impl ModuleImport { /// The module or path from which the items should be imported. pub fn source(&self) -> Expr { self.0.cast_first_match().unwrap_or_default() } /// The items to be imported. pub fn imports(&self) -> Option { self.0.children().find_map(|node| match node.kind() { SyntaxKind::Star => Some(Imports::Wildcard), SyntaxKind::ImportItems => { let items = node.children().filter_map(SyntaxNode::cast).collect(); Some(Imports::Items(items)) } _ => Option::None, }) } } /// The items that ought to be imported from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Imports { /// All items in the scope of the file should be imported. Wildcard, /// The specified items from the file should be imported. Items(Vec), } node! { /// A module include: `include "chapter1.typ"`. ModuleInclude } impl ModuleInclude { /// The module or path from which the content should be included. pub fn source(&self) -> Expr { self.0.cast_last_match().unwrap_or_default() } } node! { /// A break from a loop: `break`. LoopBreak } node! { /// A continue in a loop: `continue`. LoopContinue } node! { /// A return from a function: `return`, `return x + 1`. FuncReturn } impl FuncReturn { /// The expression to return. pub fn body(&self) -> Option { self.0.cast_last_match() } }