mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
2400 lines
68 KiB
Rust
2400 lines
68 KiB
Rust
/*!
|
||
# Abstract Syntax Tree Interface
|
||
|
||
Typst's Abstract Syntax Tree (AST) is a lazy, typed view over the untyped
|
||
Concrete Syntax Tree (CST) and is rooted in the [`Markup`] node.
|
||
|
||
## The AST is a View
|
||
|
||
Most AST nodes are wrapper structs around [`SyntaxNode`] pointers. This summary
|
||
will use a running example of the [`Raw`] node type, which is declared (after
|
||
macro expansion) as: `struct Raw<'a>(&'a SyntaxNode);`.
|
||
|
||
[`SyntaxNode`]s are generated by the parser and constitute the Concrete Syntax
|
||
Tree (CST). The CST is _concrete_ because it has the property that an in-order
|
||
tree traversal will recreate the text of the source file exactly.
|
||
|
||
[`SyntaxNode`]s in the CST contain their [`SyntaxKind`], but don't themselves
|
||
provide access to the semantic meaning of their contents. That semantic meaning
|
||
is available through the Abstract Syntax Tree by iterating over CST nodes and
|
||
inspecting their contents. The format is prepared ahead-of-time by the parser so
|
||
that this module can unpack the abstract meaning from the CST's structure.
|
||
|
||
Raw nodes are parsed by recognizing paired backtick delimiters, which you will
|
||
find as CST nodes with the [`RawDelim`] kind. However, the AST doesn't include
|
||
these delimiters because it _abstracts_ over the backticks. Instead, the parent
|
||
raw node will only use its child [`RawDelim`] CST nodes to determine whether the
|
||
element is a block or inline.
|
||
|
||
## The AST is Typed
|
||
|
||
AST nodes all implement the [`AstNode`] trait, but nodes can also implement
|
||
their own unique methods. These unique methods are the "real" interface of the
|
||
AST, and provide access to the abstract, semantic, representation of each kind
|
||
of node. For example, the [`Raw`] node provides 3 methods that specify its
|
||
abstract representation: [`Raw::lines()`] returns the raw text as an iterator of
|
||
lines, [`Raw::lang()`] provides the optionally present [`RawLang`] language tag,
|
||
and [`Raw::block()`] gives a bool for whether the raw element is a block or
|
||
inline.
|
||
|
||
This semantic information is unavailable in the CST. Only by converting a CST
|
||
node to an AST struct will Rust let you call a method of that struct. This is a
|
||
safe interface because the only way to create an AST node outside this file is
|
||
to call [`AstNode::from_untyped`]. The `node!` macro implements `from_untyped`
|
||
by checking the node's kind before constructing it, returning `Some()` only if
|
||
the kind matches. So we know that it will have the expected children underneath,
|
||
otherwise the parser wouldn't have produced this node.
|
||
|
||
## The AST is rooted in the [`Markup`] node
|
||
|
||
The AST is rooted in the [`Markup`] node, which provides only one method:
|
||
[`Markup::exprs`]. This returns an iterator of the main [`Expr`] enum. [`Expr`]
|
||
is important because it contains the majority of expressions that Typst will
|
||
evaluate. Not just markup, but also math and code expressions. Not all
|
||
expression types are available from the parser at every step, but this does
|
||
decrease the amount of wrapper enums needed in the AST (and this file is long
|
||
enough already).
|
||
|
||
Expressions also branch off into the remaining tree. You can view enums in this
|
||
file as edges on a graph: areas where the tree has paths from one type to
|
||
another (accessed through methods), then structs are the nodes of the graph,
|
||
providing methods that return enums, etc. etc.
|
||
|
||
## The AST is Lazy
|
||
|
||
Being lazy means that the untyped CST nodes are converted to typed AST nodes
|
||
only as the tree is traversed. If we parse a file and a raw block is contained
|
||
in a branch of an if-statement that we don't take, then we won't pay the cost of
|
||
creating an iterator over the lines or checking whether it was a block or
|
||
inline (although it will still be parsed into nodes).
|
||
|
||
This is also a factor of the current "tree-interpreter" evaluation model. A
|
||
bytecode interpreter might instead eagerly convert the AST into bytecode, but it
|
||
would still traverse using this lazy interface. While the tree-interpreter
|
||
evaluation is straightforward and easy to add new features onto, it has to
|
||
re-traverse the AST every time a function is evaluated. A bytecode interpreter
|
||
using the lazy interface would only need to traverse each node once, improving
|
||
throughput at the cost of initial latency and development flexibility.
|
||
*/
|
||
|
||
use std::num::NonZeroUsize;
|
||
use std::ops::Deref;
|
||
use std::path::Path;
|
||
use std::str::FromStr;
|
||
|
||
use ecow::EcoString;
|
||
use unscanny::Scanner;
|
||
|
||
use crate::package::PackageSpec;
|
||
use crate::{is_ident, is_newline, Span, SyntaxKind, SyntaxNode};
|
||
|
||
/// A typed AST node.
|
||
pub trait AstNode<'a>: Sized {
|
||
/// Convert a node into its typed variant.
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self>;
|
||
|
||
/// A reference to the underlying syntax node.
|
||
fn to_untyped(self) -> &'a SyntaxNode;
|
||
|
||
/// The source code location.
|
||
fn span(self) -> Span {
|
||
self.to_untyped().span()
|
||
}
|
||
}
|
||
|
||
// A generic interface for converting untyped nodes into typed AST nodes.
|
||
impl SyntaxNode {
|
||
/// Whether the node can be cast to the given AST node.
|
||
pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool {
|
||
self.cast::<T>().is_some()
|
||
}
|
||
|
||
/// Try to convert the node to a typed AST node.
|
||
pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
|
||
T::from_untyped(self)
|
||
}
|
||
|
||
/// Find the first child that can cast to the AST type `T`.
|
||
fn try_cast_first<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
|
||
self.children().find_map(Self::cast)
|
||
}
|
||
|
||
/// Find the last child that can cast to the AST type `T`.
|
||
fn try_cast_last<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
|
||
self.children().rev().find_map(Self::cast)
|
||
}
|
||
|
||
/// Get the first child of AST type `T` or a placeholder if none.
|
||
fn cast_first<'a, T: AstNode<'a> + Default>(&'a self) -> T {
|
||
self.try_cast_first().unwrap_or_default()
|
||
}
|
||
|
||
/// Get the last child of AST type `T` or a placeholder if none.
|
||
fn cast_last<'a, T: AstNode<'a> + Default>(&'a self) -> T {
|
||
self.try_cast_last().unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
/// Implements [`AstNode`] for a struct whose name matches a [`SyntaxKind`]
|
||
/// variant.
|
||
///
|
||
/// The struct becomes a wrapper around a [`SyntaxNode`] pointer, and the
|
||
/// implementation of [`AstNode::from_untyped`] checks that the pointer's kind
|
||
/// matches when converting, returning `Some` or `None` respectively.
|
||
///
|
||
/// The generated struct is the basis for typed accessor methods for properties
|
||
/// of this AST node. For example, the [`Raw`] struct has methods for accessing
|
||
/// its content by lines, its optional language tag, and whether the raw element
|
||
/// is inline or a block. These methods are accessible only _after_ a
|
||
/// `SyntaxNode` is coerced to the `Raw` struct type (via `from_untyped`),
|
||
/// guaranteeing their implementations will work with the expected structure.
|
||
macro_rules! node {
|
||
($(#[$attr:meta])* struct $name:ident) => {
|
||
// Create the struct as a wrapper around a `SyntaxNode` reference.
|
||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||
#[repr(transparent)]
|
||
$(#[$attr])*
|
||
pub struct $name<'a>(&'a SyntaxNode);
|
||
|
||
impl<'a> AstNode<'a> for $name<'a> {
|
||
#[inline]
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
if node.kind() == SyntaxKind::$name {
|
||
Some(Self(node))
|
||
} else {
|
||
Option::None
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
self.0
|
||
}
|
||
}
|
||
|
||
impl Default for $name<'_> {
|
||
#[inline]
|
||
fn default() -> Self {
|
||
static PLACEHOLDER: SyntaxNode
|
||
= SyntaxNode::placeholder(SyntaxKind::$name);
|
||
Self(&PLACEHOLDER)
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
node! {
|
||
/// The syntactical root capable of representing a full parsed document.
|
||
struct Markup
|
||
}
|
||
|
||
impl<'a> Markup<'a> {
|
||
/// The expressions.
|
||
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
||
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, Copy, Clone, Hash)]
|
||
pub enum Expr<'a> {
|
||
/// Plain text without markup.
|
||
Text(Text<'a>),
|
||
/// Whitespace in markup or math. Has at most one newline in markup, as more
|
||
/// indicate a paragraph break.
|
||
Space(Space<'a>),
|
||
/// A forced line break: `\`.
|
||
Linebreak(Linebreak<'a>),
|
||
/// A paragraph break, indicated by one or multiple blank lines.
|
||
Parbreak(Parbreak<'a>),
|
||
/// An escape sequence: `\#`, `\u{1F5FA}`.
|
||
Escape(Escape<'a>),
|
||
/// A shorthand for a unicode codepoint. For example, `~` for non-breaking
|
||
/// space or `-?` for a soft hyphen.
|
||
Shorthand(Shorthand<'a>),
|
||
/// A smart quote: `'` or `"`.
|
||
SmartQuote(SmartQuote<'a>),
|
||
/// Strong content: `*Strong*`.
|
||
Strong(Strong<'a>),
|
||
/// Emphasized content: `_Emphasized_`.
|
||
Emph(Emph<'a>),
|
||
/// Raw text with optional syntax highlighting: `` `...` ``.
|
||
Raw(Raw<'a>),
|
||
/// A hyperlink: `https://typst.org`.
|
||
Link(Link<'a>),
|
||
/// A label: `<intro>`.
|
||
Label(Label<'a>),
|
||
/// A reference: `@target`, `@target[..]`.
|
||
Ref(Ref<'a>),
|
||
/// A section heading: `= Introduction`.
|
||
Heading(Heading<'a>),
|
||
/// An item in a bullet list: `- ...`.
|
||
ListItem(ListItem<'a>),
|
||
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
|
||
EnumItem(EnumItem<'a>),
|
||
/// An item in a term list: `/ Term: Details`.
|
||
TermItem(TermItem<'a>),
|
||
/// A mathematical equation: `$x$`, `$ x^2 $`.
|
||
Equation(Equation<'a>),
|
||
/// The contents of a mathematical equation: `x^2 + 1`.
|
||
Math(Math<'a>),
|
||
/// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`.
|
||
MathText(MathText<'a>),
|
||
/// An identifier in math: `pi`.
|
||
MathIdent(MathIdent<'a>),
|
||
/// A shorthand for a unicode codepoint in math: `a <= b`.
|
||
MathShorthand(MathShorthand<'a>),
|
||
/// An alignment point in math: `&`.
|
||
MathAlignPoint(MathAlignPoint<'a>),
|
||
/// Matched delimiters in math: `[x + y]`.
|
||
MathDelimited(MathDelimited<'a>),
|
||
/// A base with optional attachments in math: `a_1^2`.
|
||
MathAttach(MathAttach<'a>),
|
||
/// Grouped math primes
|
||
MathPrimes(MathPrimes<'a>),
|
||
/// A fraction in math: `x/2`.
|
||
MathFrac(MathFrac<'a>),
|
||
/// A root in math: `√x`, `∛x` or `∜x`.
|
||
MathRoot(MathRoot<'a>),
|
||
/// An identifier: `left`.
|
||
Ident(Ident<'a>),
|
||
/// The `none` literal.
|
||
None(None<'a>),
|
||
/// The `auto` literal.
|
||
Auto(Auto<'a>),
|
||
/// A boolean: `true`, `false`.
|
||
Bool(Bool<'a>),
|
||
/// An integer: `120`.
|
||
Int(Int<'a>),
|
||
/// A floating-point number: `1.2`, `10e-4`.
|
||
Float(Float<'a>),
|
||
/// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
|
||
Numeric(Numeric<'a>),
|
||
/// A quoted string: `"..."`.
|
||
Str(Str<'a>),
|
||
/// A code block: `{ let x = 1; x + 2 }`.
|
||
CodeBlock(CodeBlock<'a>),
|
||
/// A content block: `[*Hi* there!]`.
|
||
ContentBlock(ContentBlock<'a>),
|
||
/// A grouped expression: `(1 + 2)`.
|
||
Parenthesized(Parenthesized<'a>),
|
||
/// An array: `(1, "hi", 12cm)`.
|
||
Array(Array<'a>),
|
||
/// A dictionary: `(thickness: 3pt, dash: "solid")`.
|
||
Dict(Dict<'a>),
|
||
/// A unary operation: `-x`.
|
||
Unary(Unary<'a>),
|
||
/// A binary operation: `a + b`.
|
||
Binary(Binary<'a>),
|
||
/// A field access: `properties.age`.
|
||
FieldAccess(FieldAccess<'a>),
|
||
/// An invocation of a function or method: `f(x, y)`.
|
||
FuncCall(FuncCall<'a>),
|
||
/// A closure: `(x, y) => z`.
|
||
Closure(Closure<'a>),
|
||
/// A let binding: `let x = 1`.
|
||
LetBinding(LetBinding<'a>),
|
||
/// A destructuring assignment: `(x, y) = (1, 2)`.
|
||
DestructAssignment(DestructAssignment<'a>),
|
||
/// A set rule: `set text(...)`.
|
||
SetRule(SetRule<'a>),
|
||
/// A show rule: `show heading: it => emph(it.body)`.
|
||
ShowRule(ShowRule<'a>),
|
||
/// A contextual expression: `context text.lang`.
|
||
Contextual(Contextual<'a>),
|
||
/// An if-else conditional: `if x { y } else { z }`.
|
||
Conditional(Conditional<'a>),
|
||
/// A while loop: `while x { y }`.
|
||
WhileLoop(WhileLoop<'a>),
|
||
/// A for loop: `for x in y { z }`.
|
||
ForLoop(ForLoop<'a>),
|
||
/// A module import: `import "utils.typ": a, b, c`.
|
||
ModuleImport(ModuleImport<'a>),
|
||
/// A module include: `include "chapter1.typ"`.
|
||
ModuleInclude(ModuleInclude<'a>),
|
||
/// A break from a loop: `break`.
|
||
LoopBreak(LoopBreak<'a>),
|
||
/// A continue in a loop: `continue`.
|
||
LoopContinue(LoopContinue<'a>),
|
||
/// A return from a function: `return`, `return x + 1`.
|
||
FuncReturn(FuncReturn<'a>),
|
||
}
|
||
|
||
impl<'a> Expr<'a> {
|
||
fn cast_with_space(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Space => Some(Self::Space(Space(node))),
|
||
_ => Self::from_untyped(node),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for Expr<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Space => Option::None, // Skipped unless using `cast_with_space`.
|
||
SyntaxKind::Linebreak => Some(Self::Linebreak(Linebreak(node))),
|
||
SyntaxKind::Parbreak => Some(Self::Parbreak(Parbreak(node))),
|
||
SyntaxKind::Text => Some(Self::Text(Text(node))),
|
||
SyntaxKind::Escape => Some(Self::Escape(Escape(node))),
|
||
SyntaxKind::Shorthand => Some(Self::Shorthand(Shorthand(node))),
|
||
SyntaxKind::SmartQuote => Some(Self::SmartQuote(SmartQuote(node))),
|
||
SyntaxKind::Strong => Some(Self::Strong(Strong(node))),
|
||
SyntaxKind::Emph => Some(Self::Emph(Emph(node))),
|
||
SyntaxKind::Raw => Some(Self::Raw(Raw(node))),
|
||
SyntaxKind::Link => Some(Self::Link(Link(node))),
|
||
SyntaxKind::Label => Some(Self::Label(Label(node))),
|
||
SyntaxKind::Ref => Some(Self::Ref(Ref(node))),
|
||
SyntaxKind::Heading => Some(Self::Heading(Heading(node))),
|
||
SyntaxKind::ListItem => Some(Self::ListItem(ListItem(node))),
|
||
SyntaxKind::EnumItem => Some(Self::EnumItem(EnumItem(node))),
|
||
SyntaxKind::TermItem => Some(Self::TermItem(TermItem(node))),
|
||
SyntaxKind::Equation => Some(Self::Equation(Equation(node))),
|
||
SyntaxKind::Math => Some(Self::Math(Math(node))),
|
||
SyntaxKind::MathText => Some(Self::MathText(MathText(node))),
|
||
SyntaxKind::MathIdent => Some(Self::MathIdent(MathIdent(node))),
|
||
SyntaxKind::MathShorthand => Some(Self::MathShorthand(MathShorthand(node))),
|
||
SyntaxKind::MathAlignPoint => {
|
||
Some(Self::MathAlignPoint(MathAlignPoint(node)))
|
||
}
|
||
SyntaxKind::MathDelimited => Some(Self::MathDelimited(MathDelimited(node))),
|
||
SyntaxKind::MathAttach => Some(Self::MathAttach(MathAttach(node))),
|
||
SyntaxKind::MathPrimes => Some(Self::MathPrimes(MathPrimes(node))),
|
||
SyntaxKind::MathFrac => Some(Self::MathFrac(MathFrac(node))),
|
||
SyntaxKind::MathRoot => Some(Self::MathRoot(MathRoot(node))),
|
||
SyntaxKind::Ident => Some(Self::Ident(Ident(node))),
|
||
SyntaxKind::None => Some(Self::None(None(node))),
|
||
SyntaxKind::Auto => Some(Self::Auto(Auto(node))),
|
||
SyntaxKind::Bool => Some(Self::Bool(Bool(node))),
|
||
SyntaxKind::Int => Some(Self::Int(Int(node))),
|
||
SyntaxKind::Float => Some(Self::Float(Float(node))),
|
||
SyntaxKind::Numeric => Some(Self::Numeric(Numeric(node))),
|
||
SyntaxKind::Str => Some(Self::Str(Str(node))),
|
||
SyntaxKind::CodeBlock => Some(Self::CodeBlock(CodeBlock(node))),
|
||
SyntaxKind::ContentBlock => Some(Self::ContentBlock(ContentBlock(node))),
|
||
SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))),
|
||
SyntaxKind::Array => Some(Self::Array(Array(node))),
|
||
SyntaxKind::Dict => Some(Self::Dict(Dict(node))),
|
||
SyntaxKind::Unary => Some(Self::Unary(Unary(node))),
|
||
SyntaxKind::Binary => Some(Self::Binary(Binary(node))),
|
||
SyntaxKind::FieldAccess => Some(Self::FieldAccess(FieldAccess(node))),
|
||
SyntaxKind::FuncCall => Some(Self::FuncCall(FuncCall(node))),
|
||
SyntaxKind::Closure => Some(Self::Closure(Closure(node))),
|
||
SyntaxKind::LetBinding => Some(Self::LetBinding(LetBinding(node))),
|
||
SyntaxKind::DestructAssignment => {
|
||
Some(Self::DestructAssignment(DestructAssignment(node)))
|
||
}
|
||
SyntaxKind::SetRule => Some(Self::SetRule(SetRule(node))),
|
||
SyntaxKind::ShowRule => Some(Self::ShowRule(ShowRule(node))),
|
||
SyntaxKind::Contextual => Some(Self::Contextual(Contextual(node))),
|
||
SyntaxKind::Conditional => Some(Self::Conditional(Conditional(node))),
|
||
SyntaxKind::WhileLoop => Some(Self::WhileLoop(WhileLoop(node))),
|
||
SyntaxKind::ForLoop => Some(Self::ForLoop(ForLoop(node))),
|
||
SyntaxKind::ModuleImport => Some(Self::ModuleImport(ModuleImport(node))),
|
||
SyntaxKind::ModuleInclude => Some(Self::ModuleInclude(ModuleInclude(node))),
|
||
SyntaxKind::LoopBreak => Some(Self::LoopBreak(LoopBreak(node))),
|
||
SyntaxKind::LoopContinue => Some(Self::LoopContinue(LoopContinue(node))),
|
||
SyntaxKind::FuncReturn => Some(Self::FuncReturn(FuncReturn(node))),
|
||
_ => Option::None,
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Text(v) => v.to_untyped(),
|
||
Self::Space(v) => v.to_untyped(),
|
||
Self::Linebreak(v) => v.to_untyped(),
|
||
Self::Parbreak(v) => v.to_untyped(),
|
||
Self::Escape(v) => v.to_untyped(),
|
||
Self::Shorthand(v) => v.to_untyped(),
|
||
Self::SmartQuote(v) => v.to_untyped(),
|
||
Self::Strong(v) => v.to_untyped(),
|
||
Self::Emph(v) => v.to_untyped(),
|
||
Self::Raw(v) => v.to_untyped(),
|
||
Self::Link(v) => v.to_untyped(),
|
||
Self::Label(v) => v.to_untyped(),
|
||
Self::Ref(v) => v.to_untyped(),
|
||
Self::Heading(v) => v.to_untyped(),
|
||
Self::ListItem(v) => v.to_untyped(),
|
||
Self::EnumItem(v) => v.to_untyped(),
|
||
Self::TermItem(v) => v.to_untyped(),
|
||
Self::Equation(v) => v.to_untyped(),
|
||
Self::Math(v) => v.to_untyped(),
|
||
Self::MathText(v) => v.to_untyped(),
|
||
Self::MathIdent(v) => v.to_untyped(),
|
||
Self::MathShorthand(v) => v.to_untyped(),
|
||
Self::MathAlignPoint(v) => v.to_untyped(),
|
||
Self::MathDelimited(v) => v.to_untyped(),
|
||
Self::MathAttach(v) => v.to_untyped(),
|
||
Self::MathPrimes(v) => v.to_untyped(),
|
||
Self::MathFrac(v) => v.to_untyped(),
|
||
Self::MathRoot(v) => v.to_untyped(),
|
||
Self::Ident(v) => v.to_untyped(),
|
||
Self::None(v) => v.to_untyped(),
|
||
Self::Auto(v) => v.to_untyped(),
|
||
Self::Bool(v) => v.to_untyped(),
|
||
Self::Int(v) => v.to_untyped(),
|
||
Self::Float(v) => v.to_untyped(),
|
||
Self::Numeric(v) => v.to_untyped(),
|
||
Self::Str(v) => v.to_untyped(),
|
||
Self::CodeBlock(v) => v.to_untyped(),
|
||
Self::ContentBlock(v) => v.to_untyped(),
|
||
Self::Array(v) => v.to_untyped(),
|
||
Self::Dict(v) => v.to_untyped(),
|
||
Self::Parenthesized(v) => v.to_untyped(),
|
||
Self::Unary(v) => v.to_untyped(),
|
||
Self::Binary(v) => v.to_untyped(),
|
||
Self::FieldAccess(v) => v.to_untyped(),
|
||
Self::FuncCall(v) => v.to_untyped(),
|
||
Self::Closure(v) => v.to_untyped(),
|
||
Self::LetBinding(v) => v.to_untyped(),
|
||
Self::DestructAssignment(v) => v.to_untyped(),
|
||
Self::SetRule(v) => v.to_untyped(),
|
||
Self::ShowRule(v) => v.to_untyped(),
|
||
Self::Contextual(v) => v.to_untyped(),
|
||
Self::Conditional(v) => v.to_untyped(),
|
||
Self::WhileLoop(v) => v.to_untyped(),
|
||
Self::ForLoop(v) => v.to_untyped(),
|
||
Self::ModuleImport(v) => v.to_untyped(),
|
||
Self::ModuleInclude(v) => v.to_untyped(),
|
||
Self::LoopBreak(v) => v.to_untyped(),
|
||
Self::LoopContinue(v) => v.to_untyped(),
|
||
Self::FuncReturn(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Expr<'_> {
|
||
/// Can this expression be embedded into markup with a hash?
|
||
pub fn hash(self) -> bool {
|
||
matches!(
|
||
self,
|
||
Self::Ident(_)
|
||
| Self::None(_)
|
||
| Self::Auto(_)
|
||
| Self::Bool(_)
|
||
| Self::Int(_)
|
||
| Self::Float(_)
|
||
| Self::Numeric(_)
|
||
| Self::Str(_)
|
||
| Self::CodeBlock(_)
|
||
| Self::ContentBlock(_)
|
||
| Self::Array(_)
|
||
| Self::Dict(_)
|
||
| Self::Parenthesized(_)
|
||
| Self::FieldAccess(_)
|
||
| Self::FuncCall(_)
|
||
| Self::LetBinding(_)
|
||
| Self::SetRule(_)
|
||
| Self::ShowRule(_)
|
||
| Self::Contextual(_)
|
||
| Self::Conditional(_)
|
||
| Self::WhileLoop(_)
|
||
| Self::ForLoop(_)
|
||
| Self::ModuleImport(_)
|
||
| Self::ModuleInclude(_)
|
||
| Self::LoopBreak(_)
|
||
| Self::LoopContinue(_)
|
||
| Self::FuncReturn(_)
|
||
)
|
||
}
|
||
|
||
/// Is this a literal?
|
||
pub fn is_literal(self) -> bool {
|
||
matches!(
|
||
self,
|
||
Self::None(_)
|
||
| Self::Auto(_)
|
||
| Self::Bool(_)
|
||
| Self::Int(_)
|
||
| Self::Float(_)
|
||
| Self::Numeric(_)
|
||
| Self::Str(_)
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for Expr<'_> {
|
||
fn default() -> Self {
|
||
Expr::None(None::default())
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Plain text without markup.
|
||
struct Text
|
||
}
|
||
|
||
impl<'a> Text<'a> {
|
||
/// Get the text.
|
||
pub fn get(self) -> &'a EcoString {
|
||
self.0.text()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Whitespace in markup or math. Has at most one newline in markup, as more
|
||
/// indicate a paragraph break.
|
||
struct Space
|
||
}
|
||
|
||
node! {
|
||
/// A forced line break: `\`.
|
||
struct Linebreak
|
||
}
|
||
|
||
node! {
|
||
/// A paragraph break, indicated by one or multiple blank lines.
|
||
struct Parbreak
|
||
}
|
||
|
||
node! {
|
||
/// An escape sequence: `\#`, `\u{1F5FA}`.
|
||
struct 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.
|
||
struct Shorthand
|
||
}
|
||
|
||
impl Shorthand<'_> {
|
||
/// A list of all shorthands in markup mode.
|
||
pub const LIST: &'static [(&'static str, char)] = &[
|
||
("...", '…'),
|
||
("~", '\u{00A0}'),
|
||
("-", '\u{2212}'), // Only before a digit
|
||
("--", '\u{2013}'),
|
||
("---", '\u{2014}'),
|
||
("-?", '\u{00AD}'),
|
||
];
|
||
|
||
/// Get the shorthanded character.
|
||
pub fn get(self) -> char {
|
||
let text = self.0.text();
|
||
Self::LIST
|
||
.iter()
|
||
.find(|&&(s, _)| s == text)
|
||
.map_or_else(char::default, |&(_, c)| c)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A smart quote: `'` or `"`.
|
||
struct SmartQuote
|
||
}
|
||
|
||
impl SmartQuote<'_> {
|
||
/// Whether this is a double quote.
|
||
pub fn double(self) -> bool {
|
||
self.0.text() == "\""
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Strong content: `*Strong*`.
|
||
struct Strong
|
||
}
|
||
|
||
impl<'a> Strong<'a> {
|
||
/// The contents of the strong node.
|
||
pub fn body(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Emphasized content: `_Emphasized_`.
|
||
struct Emph
|
||
}
|
||
|
||
impl<'a> Emph<'a> {
|
||
/// The contents of the emphasis node.
|
||
pub fn body(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Raw text with optional syntax highlighting: `` `...` ``.
|
||
struct Raw
|
||
}
|
||
|
||
impl<'a> Raw<'a> {
|
||
/// The lines in the raw block.
|
||
pub fn lines(self) -> impl DoubleEndedIterator<Item = Text<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// An optional identifier specifying the language to syntax-highlight in.
|
||
pub fn lang(self) -> Option<RawLang<'a>> {
|
||
// Only blocky literals are supposed to contain a language.
|
||
let delim: RawDelim = self.0.try_cast_first()?;
|
||
if delim.0.len() < 3 {
|
||
return Option::None;
|
||
}
|
||
|
||
self.0.try_cast_first()
|
||
}
|
||
|
||
/// Whether the raw text should be displayed in a separate block.
|
||
pub fn block(self) -> bool {
|
||
self.0
|
||
.try_cast_first()
|
||
.is_some_and(|delim: RawDelim| delim.0.len() >= 3)
|
||
&& self.0.children().any(|e| {
|
||
e.kind() == SyntaxKind::RawTrimmed && e.text().chars().any(is_newline)
|
||
})
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A language tag at the start of raw element: ``typ ``.
|
||
struct RawLang
|
||
}
|
||
|
||
impl<'a> RawLang<'a> {
|
||
/// Get the language tag.
|
||
pub fn get(self) -> &'a EcoString {
|
||
self.0.text()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A raw delimiter in single or 3+ backticks: `` ` ``.
|
||
struct RawDelim
|
||
}
|
||
|
||
node! {
|
||
/// A hyperlink: `https://typst.org`.
|
||
struct Link
|
||
}
|
||
|
||
impl<'a> Link<'a> {
|
||
/// Get the URL.
|
||
pub fn get(self) -> &'a EcoString {
|
||
self.0.text()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A label: `<intro>`.
|
||
struct Label
|
||
}
|
||
|
||
impl<'a> Label<'a> {
|
||
/// Get the label's text.
|
||
pub fn get(self) -> &'a str {
|
||
self.0.text().trim_start_matches('<').trim_end_matches('>')
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A reference: `@target`, `@target[..]`.
|
||
struct Ref
|
||
}
|
||
|
||
impl<'a> Ref<'a> {
|
||
/// Get the target.
|
||
pub fn target(self) -> &'a str {
|
||
self.0
|
||
.children()
|
||
.find(|node| node.kind() == SyntaxKind::RefMarker)
|
||
.map(|node| node.text().trim_start_matches('@'))
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// Get the supplement.
|
||
pub fn supplement(self) -> Option<ContentBlock<'a>> {
|
||
self.0.try_cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A section heading: `= Introduction`.
|
||
struct Heading
|
||
}
|
||
|
||
impl<'a> Heading<'a> {
|
||
/// The contents of the heading.
|
||
pub fn body(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The section depth (number of equals signs).
|
||
pub fn depth(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: `- ...`.
|
||
struct ListItem
|
||
}
|
||
|
||
impl<'a> ListItem<'a> {
|
||
/// The contents of the list item.
|
||
pub fn body(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
|
||
struct EnumItem
|
||
}
|
||
|
||
impl<'a> EnumItem<'a> {
|
||
/// The explicit numbering, if any: `23.`.
|
||
pub fn number(self) -> Option<u64> {
|
||
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<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An item in a term list: `/ Term: Details`.
|
||
struct TermItem
|
||
}
|
||
|
||
impl<'a> TermItem<'a> {
|
||
/// The term described by the item.
|
||
pub fn term(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The description of the term.
|
||
pub fn description(self) -> Markup<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A mathematical equation: `$x$`, `$ x^2 $`.
|
||
struct Equation
|
||
}
|
||
|
||
impl<'a> Equation<'a> {
|
||
/// The contained math.
|
||
pub fn body(self) -> Math<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// Whether the equation 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! {
|
||
/// The contents of a mathematical equation: `x^2 + 1`.
|
||
struct Math
|
||
}
|
||
|
||
impl<'a> Math<'a> {
|
||
/// The expressions the mathematical content consists of.
|
||
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
||
self.0.children().filter_map(Expr::cast_with_space)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`.
|
||
struct MathText
|
||
}
|
||
|
||
/// The underlying text kind.
|
||
pub enum MathTextKind<'a> {
|
||
Character(char),
|
||
Number(&'a EcoString),
|
||
}
|
||
|
||
impl<'a> MathText<'a> {
|
||
/// Return the underlying text.
|
||
pub fn get(self) -> MathTextKind<'a> {
|
||
let text = self.0.text();
|
||
let mut chars = text.chars();
|
||
let c = chars.next().unwrap();
|
||
if c.is_numeric() {
|
||
// Numbers are potentially grouped as multiple characters. This is
|
||
// done in `Lexer::math_text()`.
|
||
MathTextKind::Number(text)
|
||
} else {
|
||
assert!(chars.next().is_none());
|
||
MathTextKind::Character(c)
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An identifier in math: `pi`.
|
||
struct MathIdent
|
||
}
|
||
|
||
impl<'a> MathIdent<'a> {
|
||
/// Get the identifier.
|
||
pub fn get(self) -> &'a EcoString {
|
||
self.0.text()
|
||
}
|
||
|
||
/// Get the identifier as a string slice.
|
||
pub fn as_str(self) -> &'a str {
|
||
self.get()
|
||
}
|
||
}
|
||
|
||
impl Deref for MathIdent<'_> {
|
||
type Target = str;
|
||
|
||
/// Dereference to a string. Note that this shortens the lifetime, so you
|
||
/// may need to use [`get()`](Self::get) instead in some situations.
|
||
fn deref(&self) -> &Self::Target {
|
||
self.as_str()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A shorthand for a unicode codepoint in math: `a <= b`.
|
||
struct MathShorthand
|
||
}
|
||
|
||
impl MathShorthand<'_> {
|
||
/// A list of all shorthands in math mode.
|
||
pub const LIST: &'static [(&'static str, char)] = &[
|
||
("...", '…'),
|
||
("-", '−'),
|
||
("*", '∗'),
|
||
("~", '∼'),
|
||
("!=", '≠'),
|
||
(":=", '≔'),
|
||
("::=", '⩴'),
|
||
("=:", '≕'),
|
||
("<<", '≪'),
|
||
("<<<", '⋘'),
|
||
(">>", '≫'),
|
||
(">>>", '⋙'),
|
||
("<=", '≤'),
|
||
(">=", '≥'),
|
||
("->", '→'),
|
||
("-->", '⟶'),
|
||
("|->", '↦'),
|
||
(">->", '↣'),
|
||
("->>", '↠'),
|
||
("<-", '←'),
|
||
("<--", '⟵'),
|
||
("<-<", '↢'),
|
||
("<<-", '↞'),
|
||
("<->", '↔'),
|
||
("<-->", '⟷'),
|
||
("~>", '⇝'),
|
||
("~~>", '⟿'),
|
||
("<~", '⇜'),
|
||
("<~~", '⬳'),
|
||
("=>", '⇒'),
|
||
("|=>", '⤇'),
|
||
("==>", '⟹'),
|
||
("<==", '⟸'),
|
||
("<=>", '⇔'),
|
||
("<==>", '⟺'),
|
||
("[|", '⟦'),
|
||
("|]", '⟧'),
|
||
("||", '‖'),
|
||
];
|
||
|
||
/// Get the shorthanded character.
|
||
pub fn get(self) -> char {
|
||
let text = self.0.text();
|
||
Self::LIST
|
||
.iter()
|
||
.find(|&&(s, _)| s == text)
|
||
.map_or_else(char::default, |&(_, c)| c)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An alignment point in math: `&`.
|
||
struct MathAlignPoint
|
||
}
|
||
|
||
node! {
|
||
/// Matched delimiters in math: `[x + y]`.
|
||
struct MathDelimited
|
||
}
|
||
|
||
impl<'a> MathDelimited<'a> {
|
||
/// The opening delimiter.
|
||
pub fn open(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The contents, including the delimiters.
|
||
pub fn body(self) -> Math<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The closing delimiter.
|
||
pub fn close(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A base with optional attachments in math: `a_1^2`.
|
||
struct MathAttach
|
||
}
|
||
|
||
impl<'a> MathAttach<'a> {
|
||
/// The base, to which things are attached.
|
||
pub fn base(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The bottom attachment.
|
||
pub fn bottom(self) -> Option<Expr<'a>> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore))
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// The top attachment.
|
||
pub fn top(self) -> Option<Expr<'a>> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat))
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// Extract attached primes if present.
|
||
pub fn primes(self) -> Option<MathPrimes<'a>> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|node| node.cast::<Expr<'_>>().is_none())
|
||
.nth(1)
|
||
.and_then(|n| n.cast())
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// Grouped primes in math: `a'''`.
|
||
struct MathPrimes
|
||
}
|
||
|
||
impl MathPrimes<'_> {
|
||
/// The number of grouped primes.
|
||
pub fn count(self) -> usize {
|
||
self.0
|
||
.children()
|
||
.filter(|node| matches!(node.kind(), SyntaxKind::Prime))
|
||
.count()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A fraction in math: `x/2`
|
||
struct MathFrac
|
||
}
|
||
|
||
impl<'a> MathFrac<'a> {
|
||
/// The numerator.
|
||
pub fn num(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The denominator.
|
||
pub fn denom(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A root in math: `√x`, `∛x` or `∜x`.
|
||
struct MathRoot
|
||
}
|
||
|
||
impl<'a> MathRoot<'a> {
|
||
/// The index of the root.
|
||
pub fn index(self) -> Option<usize> {
|
||
match self.0.children().next().map(|node| node.text().as_str()) {
|
||
Some("∜") => Some(4),
|
||
Some("∛") => Some(3),
|
||
Some("√") => Option::None,
|
||
_ => Option::None,
|
||
}
|
||
}
|
||
|
||
/// The radicand.
|
||
pub fn radicand(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An identifier: `it`.
|
||
struct Ident
|
||
}
|
||
|
||
impl<'a> Ident<'a> {
|
||
/// Get the identifier.
|
||
pub fn get(self) -> &'a EcoString {
|
||
self.0.text()
|
||
}
|
||
|
||
/// Get the identifier as a string slice.
|
||
pub fn as_str(self) -> &'a str {
|
||
self.get()
|
||
}
|
||
}
|
||
|
||
impl Deref for Ident<'_> {
|
||
type Target = str;
|
||
|
||
/// Dereference to a string. Note that this shortens the lifetime, so you
|
||
/// may need to use [`get()`](Self::get) instead in some situations.
|
||
fn deref(&self) -> &Self::Target {
|
||
self.as_str()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// The `none` literal.
|
||
struct None
|
||
}
|
||
|
||
node! {
|
||
/// The `auto` literal.
|
||
struct Auto
|
||
}
|
||
|
||
node! {
|
||
/// A boolean: `true`, `false`.
|
||
struct Bool
|
||
}
|
||
|
||
impl Bool<'_> {
|
||
/// Get the boolean value.
|
||
pub fn get(self) -> bool {
|
||
self.0.text() == "true"
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An integer: `120`.
|
||
struct Int
|
||
}
|
||
|
||
impl Int<'_> {
|
||
/// Get the integer value.
|
||
pub fn get(self) -> i64 {
|
||
let text = self.0.text();
|
||
if let Some(rest) = text.strip_prefix("0x") {
|
||
i64::from_str_radix(rest, 16)
|
||
} else if let Some(rest) = text.strip_prefix("0o") {
|
||
i64::from_str_radix(rest, 8)
|
||
} else if let Some(rest) = text.strip_prefix("0b") {
|
||
i64::from_str_radix(rest, 2)
|
||
} else {
|
||
text.parse()
|
||
}
|
||
.unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A floating-point number: `1.2`, `10e-4`.
|
||
struct 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%`.
|
||
struct 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::Pt,
|
||
"mm" => Unit::Mm,
|
||
"cm" => Unit::Cm,
|
||
"in" => Unit::In,
|
||
"deg" => Unit::Deg,
|
||
"rad" => Unit::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 {
|
||
/// Points.
|
||
Pt,
|
||
/// Millimeters.
|
||
Mm,
|
||
/// Centimeters.
|
||
Cm,
|
||
/// Inches.
|
||
In,
|
||
/// Radians.
|
||
Rad,
|
||
/// Degrees.
|
||
Deg,
|
||
/// Font-relative: `1em` is the same as the font size.
|
||
Em,
|
||
/// Fractions: `fr`.
|
||
Fr,
|
||
/// Percentage: `%`.
|
||
Percent,
|
||
}
|
||
|
||
node! {
|
||
/// A quoted string: `"..."`.
|
||
struct 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 }`.
|
||
struct CodeBlock
|
||
}
|
||
|
||
impl<'a> CodeBlock<'a> {
|
||
/// The contained code.
|
||
pub fn body(self) -> Code<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// The body of a code block.
|
||
struct Code
|
||
}
|
||
|
||
impl<'a> Code<'a> {
|
||
/// The list of expressions contained in the code.
|
||
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A content block: `[*Hi* there!]`.
|
||
struct ContentBlock
|
||
}
|
||
|
||
impl<'a> ContentBlock<'a> {
|
||
/// The contained markup.
|
||
pub fn body(self) -> Markup<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A grouped expression: `(1 + 2)`.
|
||
struct Parenthesized
|
||
}
|
||
|
||
impl<'a> Parenthesized<'a> {
|
||
/// The wrapped expression.
|
||
///
|
||
/// Should only be accessed if this is contained in an `Expr`.
|
||
pub fn expr(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The wrapped pattern.
|
||
///
|
||
/// Should only be accessed if this is contained in a `Pattern`.
|
||
pub fn pattern(self) -> Pattern<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An array: `(1, "hi", 12cm)`.
|
||
struct Array
|
||
}
|
||
|
||
impl<'a> Array<'a> {
|
||
/// The array's items.
|
||
pub fn items(self) -> impl DoubleEndedIterator<Item = ArrayItem<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
/// An item in an array.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum ArrayItem<'a> {
|
||
/// A bare expression: `12`.
|
||
Pos(Expr<'a>),
|
||
/// A spread expression: `..things`.
|
||
Spread(Spread<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for ArrayItem<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Spread => Some(Self::Spread(Spread(node))),
|
||
_ => node.cast().map(Self::Pos),
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Pos(v) => v.to_untyped(),
|
||
Self::Spread(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A dictionary: `(thickness: 3pt, dash: "solid")`.
|
||
struct Dict
|
||
}
|
||
|
||
impl<'a> Dict<'a> {
|
||
/// The dictionary's items.
|
||
pub fn items(self) -> impl DoubleEndedIterator<Item = DictItem<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
/// An item in an dictionary expression.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum DictItem<'a> {
|
||
/// A named pair: `thickness: 3pt`.
|
||
Named(Named<'a>),
|
||
/// A keyed pair: `"spacy key": true`.
|
||
Keyed(Keyed<'a>),
|
||
/// A spread expression: `..things`.
|
||
Spread(Spread<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for DictItem<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Named => Some(Self::Named(Named(node))),
|
||
SyntaxKind::Keyed => Some(Self::Keyed(Keyed(node))),
|
||
SyntaxKind::Spread => Some(Self::Spread(Spread(node))),
|
||
_ => Option::None,
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Named(v) => v.to_untyped(),
|
||
Self::Keyed(v) => v.to_untyped(),
|
||
Self::Spread(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A named pair: `thickness: 3pt`.
|
||
struct Named
|
||
}
|
||
|
||
impl<'a> Named<'a> {
|
||
/// The name: `thickness`.
|
||
pub fn name(self) -> Ident<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The right-hand side of the pair: `3pt`.
|
||
///
|
||
/// This should only be accessed if this `Named` is contained in a
|
||
/// `DictItem`, `Arg`, or `Param`.
|
||
pub fn expr(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
|
||
/// The right-hand side of the pair as a pattern.
|
||
///
|
||
/// This should only be accessed if this `Named` is contained in a
|
||
/// `Destructuring`.
|
||
pub fn pattern(self) -> Pattern<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A keyed pair: `"spacy key": true`.
|
||
struct Keyed
|
||
}
|
||
|
||
impl<'a> Keyed<'a> {
|
||
/// The key: `"spacy key"`.
|
||
pub fn key(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The right-hand side of the pair: `true`.
|
||
///
|
||
/// This should only be accessed if this `Keyed` is contained in a
|
||
/// `DictItem`.
|
||
pub fn expr(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A spread: `..x` or `..x.at(0)`.
|
||
struct Spread
|
||
}
|
||
|
||
impl<'a> Spread<'a> {
|
||
/// The spread expression.
|
||
///
|
||
/// This should only be accessed if this `Spread` is contained in an
|
||
/// `ArrayItem`, `DictItem`, or `Arg`.
|
||
pub fn expr(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The sink identifier, if present.
|
||
///
|
||
/// This should only be accessed if this `Spread` is contained in a
|
||
/// `Param` or binding `DestructuringItem`.
|
||
pub fn sink_ident(self) -> Option<Ident<'a>> {
|
||
self.0.try_cast_first()
|
||
}
|
||
|
||
/// The sink expressions, if present.
|
||
///
|
||
/// This should only be accessed if this `Spread` is contained in a
|
||
/// `DestructuringItem`.
|
||
pub fn sink_expr(self) -> Option<Expr<'a>> {
|
||
self.0.try_cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A unary operation: `-x`.
|
||
struct Unary
|
||
}
|
||
|
||
impl<'a> Unary<'a> {
|
||
/// 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<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
/// 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<Self> {
|
||
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`.
|
||
struct Binary
|
||
}
|
||
|
||
impl<'a> Binary<'a> {
|
||
/// 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<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The right-hand side of the operation: `b`.
|
||
pub fn rhs(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
/// 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 inverse containment operator: `not in`.
|
||
NotIn,
|
||
/// The add-assign operator: `+=`.
|
||
AddAssign,
|
||
/// The subtract-assign operator: `-=`.
|
||
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<Self> {
|
||
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`.
|
||
struct FieldAccess
|
||
}
|
||
|
||
impl<'a> FieldAccess<'a> {
|
||
/// The expression to access the field on.
|
||
pub fn target(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The name of the field.
|
||
pub fn field(self) -> Ident<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An invocation of a function or method: `f(x, y)`.
|
||
struct FuncCall
|
||
}
|
||
|
||
impl<'a> FuncCall<'a> {
|
||
/// The function to call.
|
||
pub fn callee(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The arguments to the function.
|
||
pub fn args(self) -> Args<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A function call's argument list: `(12pt, y)`.
|
||
struct Args
|
||
}
|
||
|
||
impl<'a> Args<'a> {
|
||
/// The positional and named arguments.
|
||
pub fn items(self) -> impl DoubleEndedIterator<Item = Arg<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// Whether there is a comma at the end.
|
||
pub fn trailing_comma(self) -> bool {
|
||
self.0
|
||
.children()
|
||
.rev()
|
||
.skip(1)
|
||
.find(|n| !n.kind().is_trivia())
|
||
.is_some_and(|n| n.kind() == SyntaxKind::Comma)
|
||
}
|
||
}
|
||
|
||
/// An argument to a function call.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum Arg<'a> {
|
||
/// A positional argument: `12`.
|
||
Pos(Expr<'a>),
|
||
/// A named argument: `draw: false`.
|
||
Named(Named<'a>),
|
||
/// A spread argument: `..things`.
|
||
Spread(Spread<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for Arg<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Named => Some(Self::Named(Named(node))),
|
||
SyntaxKind::Spread => Some(Self::Spread(Spread(node))),
|
||
_ => node.cast().map(Self::Pos),
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Pos(v) => v.to_untyped(),
|
||
Self::Named(v) => v.to_untyped(),
|
||
Self::Spread(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A closure: `(x, y) => z`.
|
||
struct Closure
|
||
}
|
||
|
||
impl<'a> Closure<'a> {
|
||
/// The name of the closure.
|
||
///
|
||
/// This only exists if you use the function syntax sugar: `let f(x) = y`.
|
||
pub fn name(self) -> Option<Ident<'a>> {
|
||
self.0.children().next()?.cast()
|
||
}
|
||
|
||
/// The parameter bindings.
|
||
pub fn params(self) -> Params<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The body of the closure.
|
||
pub fn body(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A closure's parameters: `(x, y)`.
|
||
struct Params
|
||
}
|
||
|
||
impl<'a> Params<'a> {
|
||
/// The parameter bindings.
|
||
pub fn children(self) -> impl DoubleEndedIterator<Item = Param<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
/// A parameter to a closure.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum Param<'a> {
|
||
/// A positional parameter: `x`.
|
||
Pos(Pattern<'a>),
|
||
/// A named parameter with a default value: `draw: false`.
|
||
Named(Named<'a>),
|
||
/// An argument sink: `..args` or `..`.
|
||
Spread(Spread<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for Param<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Named => Some(Self::Named(Named(node))),
|
||
SyntaxKind::Spread => Some(Self::Spread(Spread(node))),
|
||
_ => node.cast().map(Self::Pos),
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Pos(v) => v.to_untyped(),
|
||
Self::Named(v) => v.to_untyped(),
|
||
Self::Spread(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The kind of a pattern.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum Pattern<'a> {
|
||
/// A single expression: `x`.
|
||
Normal(Expr<'a>),
|
||
/// A placeholder: `_`.
|
||
Placeholder(Underscore<'a>),
|
||
/// A parenthesized pattern.
|
||
Parenthesized(Parenthesized<'a>),
|
||
/// A destructuring pattern: `(x, _, ..y)`.
|
||
Destructuring(Destructuring<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for Pattern<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Underscore => Some(Self::Placeholder(Underscore(node))),
|
||
SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))),
|
||
SyntaxKind::Destructuring => Some(Self::Destructuring(Destructuring(node))),
|
||
_ => node.cast().map(Self::Normal),
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Normal(v) => v.to_untyped(),
|
||
Self::Placeholder(v) => v.to_untyped(),
|
||
Self::Parenthesized(v) => v.to_untyped(),
|
||
Self::Destructuring(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'a> Pattern<'a> {
|
||
/// Returns a list of all new bindings introduced by the pattern.
|
||
pub fn bindings(self) -> Vec<Ident<'a>> {
|
||
match self {
|
||
Self::Normal(Expr::Ident(ident)) => vec![ident],
|
||
Self::Parenthesized(v) => v.pattern().bindings(),
|
||
Self::Destructuring(v) => v.bindings(),
|
||
_ => vec![],
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for Pattern<'_> {
|
||
fn default() -> Self {
|
||
Self::Normal(Expr::default())
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An underscore: `_`
|
||
struct Underscore
|
||
}
|
||
|
||
node! {
|
||
/// A destructuring pattern: `x` or `(x, _, ..y)`.
|
||
struct Destructuring
|
||
}
|
||
|
||
impl<'a> Destructuring<'a> {
|
||
/// The items of the destructuring.
|
||
pub fn items(self) -> impl DoubleEndedIterator<Item = DestructuringItem<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// Returns a list of all new bindings introduced by the destructuring.
|
||
pub fn bindings(self) -> Vec<Ident<'a>> {
|
||
self.items()
|
||
.flat_map(|binding| match binding {
|
||
DestructuringItem::Pattern(pattern) => pattern.bindings(),
|
||
DestructuringItem::Named(named) => named.pattern().bindings(),
|
||
DestructuringItem::Spread(spread) => {
|
||
spread.sink_ident().into_iter().collect()
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
/// The kind of an element in a destructuring pattern.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum DestructuringItem<'a> {
|
||
/// A sub-pattern: `x`.
|
||
Pattern(Pattern<'a>),
|
||
/// A renamed destructuring: `x: y`.
|
||
Named(Named<'a>),
|
||
/// A destructuring sink: `..y` or `..`.
|
||
Spread(Spread<'a>),
|
||
}
|
||
|
||
impl<'a> AstNode<'a> for DestructuringItem<'a> {
|
||
fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Named => Some(Self::Named(Named(node))),
|
||
SyntaxKind::Spread => Some(Self::Spread(Spread(node))),
|
||
_ => node.cast().map(Self::Pattern),
|
||
}
|
||
}
|
||
|
||
fn to_untyped(self) -> &'a SyntaxNode {
|
||
match self {
|
||
Self::Pattern(v) => v.to_untyped(),
|
||
Self::Named(v) => v.to_untyped(),
|
||
Self::Spread(v) => v.to_untyped(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A let binding: `let x = 1`.
|
||
struct LetBinding
|
||
}
|
||
|
||
/// The kind of a let binding, either a normal one or a closure.
|
||
#[derive(Debug)]
|
||
pub enum LetBindingKind<'a> {
|
||
/// A normal binding: `let x = 1`.
|
||
Normal(Pattern<'a>),
|
||
/// A closure binding: `let f(x) = 1`.
|
||
Closure(Ident<'a>),
|
||
}
|
||
|
||
impl<'a> LetBindingKind<'a> {
|
||
/// Returns a list of all new bindings introduced by the let binding.
|
||
pub fn bindings(self) -> Vec<Ident<'a>> {
|
||
match self {
|
||
LetBindingKind::Normal(pattern) => pattern.bindings(),
|
||
LetBindingKind::Closure(ident) => vec![ident],
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'a> LetBinding<'a> {
|
||
/// The kind of the let binding.
|
||
pub fn kind(self) -> LetBindingKind<'a> {
|
||
match self.0.cast_first() {
|
||
Pattern::Normal(Expr::Closure(closure)) => {
|
||
LetBindingKind::Closure(closure.name().unwrap_or_default())
|
||
}
|
||
pattern => LetBindingKind::Normal(pattern),
|
||
}
|
||
}
|
||
|
||
/// The expression the binding is initialized with.
|
||
pub fn init(self) -> Option<Expr<'a>> {
|
||
match self.kind() {
|
||
LetBindingKind::Normal(Pattern::Normal(_) | Pattern::Parenthesized(_)) => {
|
||
self.0.children().filter_map(SyntaxNode::cast).nth(1)
|
||
}
|
||
LetBindingKind::Normal(_) => self.0.try_cast_first(),
|
||
LetBindingKind::Closure(_) => self.0.try_cast_first(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An assignment expression `(x, y) = (1, 2)`.
|
||
struct DestructAssignment
|
||
}
|
||
|
||
impl<'a> DestructAssignment<'a> {
|
||
/// The pattern of the assignment.
|
||
pub fn pattern(self) -> Pattern<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The expression that is assigned.
|
||
pub fn value(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A set rule: `set text(...)`.
|
||
struct SetRule
|
||
}
|
||
|
||
impl<'a> SetRule<'a> {
|
||
/// The function to set style properties for.
|
||
pub fn target(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The style properties to set.
|
||
pub fn args(self) -> Args<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
|
||
/// A condition under which the set rule applies.
|
||
pub fn condition(self) -> Option<Expr<'a>> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|child| child.kind() != SyntaxKind::If)
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A show rule: `show heading: it => emph(it.body)`.
|
||
struct ShowRule
|
||
}
|
||
|
||
impl<'a> ShowRule<'a> {
|
||
/// Defines which nodes the show rule applies to.
|
||
pub fn selector(self) -> Option<Expr<'a>> {
|
||
self.0
|
||
.children()
|
||
.rev()
|
||
.skip_while(|child| child.kind() != SyntaxKind::Colon)
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// The transformation recipe.
|
||
pub fn transform(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A contextual expression: `context text.lang`.
|
||
struct Contextual
|
||
}
|
||
|
||
impl<'a> Contextual<'a> {
|
||
/// The expression which depends on the context.
|
||
pub fn body(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// An if-else conditional: `if x { y } else { z }`.
|
||
struct Conditional
|
||
}
|
||
|
||
impl<'a> Conditional<'a> {
|
||
/// The condition which selects the body to evaluate.
|
||
pub fn condition(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The expression to evaluate if the condition is true.
|
||
pub fn if_body(self) -> Expr<'a> {
|
||
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<Expr<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast).nth(2)
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A while loop: `while x { y }`.
|
||
struct WhileLoop
|
||
}
|
||
|
||
impl<'a> WhileLoop<'a> {
|
||
/// The condition which selects whether to evaluate the body.
|
||
pub fn condition(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The expression to evaluate while the condition is true.
|
||
pub fn body(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A for loop: `for x in y { z }`.
|
||
struct ForLoop
|
||
}
|
||
|
||
impl<'a> ForLoop<'a> {
|
||
/// The pattern to assign to.
|
||
pub fn pattern(self) -> Pattern<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The expression to iterate over.
|
||
pub fn iterable(self) -> Expr<'a> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|&c| c.kind() != SyntaxKind::In)
|
||
.find_map(SyntaxNode::cast)
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// The expression to evaluate for each iteration.
|
||
pub fn body(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A module import: `import "utils.typ": a, b, c`.
|
||
struct ModuleImport
|
||
}
|
||
|
||
impl<'a> ModuleImport<'a> {
|
||
/// The module or path from which the items should be imported.
|
||
pub fn source(self) -> Expr<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The items to be imported.
|
||
pub fn imports(self) -> Option<Imports<'a>> {
|
||
self.0.children().find_map(|node| match node.kind() {
|
||
SyntaxKind::Star => Some(Imports::Wildcard),
|
||
SyntaxKind::ImportItems => node.cast().map(Imports::Items),
|
||
_ => Option::None,
|
||
})
|
||
}
|
||
|
||
/// The name that will be bound for a bare import. This name must be
|
||
/// statically known. It can come from:
|
||
/// - an identifier
|
||
/// - a field access
|
||
/// - a string that is a valid file path where the file stem is a valid
|
||
/// identifier
|
||
/// - a string that is a valid package spec
|
||
pub fn bare_name(self) -> Result<EcoString, BareImportError> {
|
||
match self.source() {
|
||
Expr::Ident(ident) => Ok(ident.get().clone()),
|
||
Expr::FieldAccess(access) => Ok(access.field().get().clone()),
|
||
Expr::Str(string) => {
|
||
let string = string.get();
|
||
let name = if string.starts_with('@') {
|
||
PackageSpec::from_str(&string)
|
||
.map_err(|_| BareImportError::PackageInvalid)?
|
||
.name
|
||
} else {
|
||
Path::new(string.as_str())
|
||
.file_stem()
|
||
.and_then(|path| path.to_str())
|
||
.ok_or(BareImportError::PathInvalid)?
|
||
.into()
|
||
};
|
||
|
||
if !is_ident(&name) {
|
||
return Err(BareImportError::PathInvalid);
|
||
}
|
||
|
||
Ok(name)
|
||
}
|
||
_ => Err(BareImportError::Dynamic),
|
||
}
|
||
}
|
||
|
||
/// The name this module was assigned to, if it was renamed with `as`
|
||
/// (`renamed` in `import "..." as renamed`).
|
||
pub fn new_name(self) -> Option<Ident<'a>> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|child| child.kind() != SyntaxKind::As)
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
}
|
||
|
||
/// Reasons why a bare name cannot be determined for an import source.
|
||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||
pub enum BareImportError {
|
||
/// There is no statically resolvable binding name.
|
||
Dynamic,
|
||
/// The import source is not a valid path or the path stem not a valid
|
||
/// identifier.
|
||
PathInvalid,
|
||
/// The import source is not a valid package spec.
|
||
PackageInvalid,
|
||
}
|
||
|
||
/// The items that ought to be imported from a file.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum Imports<'a> {
|
||
/// All items in the scope of the file should be imported.
|
||
Wildcard,
|
||
/// The specified items from the file should be imported.
|
||
Items(ImportItems<'a>),
|
||
}
|
||
|
||
node! {
|
||
/// Items to import from a module: `a, b, c`.
|
||
struct ImportItems
|
||
}
|
||
|
||
impl<'a> ImportItems<'a> {
|
||
/// Returns an iterator over the items to import from the module.
|
||
pub fn iter(self) -> impl DoubleEndedIterator<Item = ImportItem<'a>> {
|
||
self.0.children().filter_map(|child| match child.kind() {
|
||
SyntaxKind::RenamedImportItem => child.cast().map(ImportItem::Renamed),
|
||
SyntaxKind::ImportItemPath => child.cast().map(ImportItem::Simple),
|
||
_ => Option::None,
|
||
})
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A path to a submodule's imported name: `a.b.c`.
|
||
struct ImportItemPath
|
||
}
|
||
|
||
impl<'a> ImportItemPath<'a> {
|
||
/// An iterator over the path's components.
|
||
pub fn iter(self) -> impl DoubleEndedIterator<Item = Ident<'a>> {
|
||
self.0.children().filter_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// The name of the imported item. This is the last segment in the path.
|
||
pub fn name(self) -> Ident<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
/// An imported item, potentially renamed to another identifier.
|
||
#[derive(Debug, Copy, Clone, Hash)]
|
||
pub enum ImportItem<'a> {
|
||
/// A non-renamed import (the item's name in the scope is the same as its
|
||
/// name).
|
||
Simple(ImportItemPath<'a>),
|
||
/// A renamed import (the item was bound to a different name in the scope
|
||
/// than the one it was defined as).
|
||
Renamed(RenamedImportItem<'a>),
|
||
}
|
||
|
||
impl<'a> ImportItem<'a> {
|
||
/// The path to the imported item.
|
||
pub fn path(self) -> ImportItemPath<'a> {
|
||
match self {
|
||
Self::Simple(path) => path,
|
||
Self::Renamed(renamed_item) => renamed_item.path(),
|
||
}
|
||
}
|
||
|
||
/// The original name of the imported item, at its source. This will be the
|
||
/// equal to the bound name if the item wasn't renamed with 'as'.
|
||
pub fn original_name(self) -> Ident<'a> {
|
||
match self {
|
||
Self::Simple(path) => path.name(),
|
||
Self::Renamed(renamed_item) => renamed_item.original_name(),
|
||
}
|
||
}
|
||
|
||
/// The name which this import item was bound to. Corresponds to the new
|
||
/// name, if it was renamed; otherwise, it's just its original name.
|
||
pub fn bound_name(self) -> Ident<'a> {
|
||
match self {
|
||
Self::Simple(path) => path.name(),
|
||
Self::Renamed(renamed_item) => renamed_item.new_name(),
|
||
}
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A renamed import item: `a as d`
|
||
struct RenamedImportItem
|
||
}
|
||
|
||
impl<'a> RenamedImportItem<'a> {
|
||
/// The path to the imported item.
|
||
pub fn path(self) -> ImportItemPath<'a> {
|
||
self.0.cast_first()
|
||
}
|
||
|
||
/// The original name of the imported item (`a` in `a as d` or `c.b.a as d`).
|
||
pub fn original_name(self) -> Ident<'a> {
|
||
self.path().name()
|
||
}
|
||
|
||
/// The new name of the imported item (`d` in `a as d`).
|
||
pub fn new_name(self) -> Ident<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A module include: `include "chapter1.typ"`.
|
||
struct ModuleInclude
|
||
}
|
||
|
||
impl<'a> ModuleInclude<'a> {
|
||
/// The module or path from which the content should be included.
|
||
pub fn source(self) -> Expr<'a> {
|
||
self.0.cast_last()
|
||
}
|
||
}
|
||
|
||
node! {
|
||
/// A break from a loop: `break`.
|
||
struct LoopBreak
|
||
}
|
||
|
||
node! {
|
||
/// A continue in a loop: `continue`.
|
||
struct LoopContinue
|
||
}
|
||
|
||
node! {
|
||
/// A return from a function: `return`, `return x + 1`.
|
||
struct FuncReturn
|
||
}
|
||
|
||
impl<'a> FuncReturn<'a> {
|
||
/// The expression to return.
|
||
pub fn body(self) -> Option<Expr<'a>> {
|
||
self.0.try_cast_last()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_expr_default() {
|
||
assert!(Expr::default().to_untyped().cast::<Expr>().is_some());
|
||
}
|
||
}
|