mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
1768 lines
46 KiB
Rust
1768 lines
46 KiB
Rust
//! 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<Self>;
|
||
|
||
/// 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<Self> {
|
||
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<Item = Expr> + '_ {
|
||
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: `<intro>`.
|
||
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<Self> {
|
||
match node.kind() {
|
||
SyntaxKind::Space => node.cast().map(Self::Space),
|
||
_ => Self::from_untyped(node),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl AstNode for Expr {
|
||
fn from_untyped(node: &SyntaxNode) -> Option<Self> {
|
||
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: `<intro>`.
|
||
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<NonZeroUsize> {
|
||
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<Item = Expr> + '_ {
|
||
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<Expr> {
|
||
self.0
|
||
.children()
|
||
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore))
|
||
.find_map(SyntaxNode::cast)
|
||
}
|
||
|
||
/// The top attachment.
|
||
pub fn top(&self) -> Option<Expr> {
|
||
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<Item = Expr> + '_ {
|
||
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<Item = ArrayItem> + '_ {
|
||
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<Self> {
|
||
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<Item = DictItem> + '_ {
|
||
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<Self> {
|
||
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::<Str>())
|
||
.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<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`.
|
||
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<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`.
|
||
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<Item = Arg> + '_ {
|
||
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<Self> {
|
||
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<Ident> {
|
||
self.0.children().next()?.cast()
|
||
}
|
||
|
||
/// The parameter bindings.
|
||
pub fn params(&self) -> impl DoubleEndedIterator<Item = Param> + '_ {
|
||
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<Self> {
|
||
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<Expr> {
|
||
if self.0.cast_first_match::<Ident>().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<Expr> {
|
||
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<Expr> {
|
||
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<Expr> {
|
||
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<Ident> {
|
||
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<Imports> {
|
||
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<Ident>),
|
||
}
|
||
|
||
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<Expr> {
|
||
self.0.cast_last_match()
|
||
}
|
||
}
|