Skip to main content

rustledger_parser/cst/
ast.rs

1//! Typed AST wrappers over the lossless CST.
2//!
3//! Phase 3 of #1262. The CST (phase 1-2) preserves every byte of
4//! the source as an untyped tree of `SyntaxKind` nodes and tokens.
5//! This module adds a thin typed layer on top: newtype wrappers
6//! around `SyntaxNode` / `SyntaxToken` with `kind()`-gated
7//! constructors (`cast`) and structural accessors (`date()`,
8//! `account()`, `amount()`, etc.).
9//!
10//! Two traits anchor the surface:
11//!
12//! - [`AstNode`]: typed wrapper around a `SyntaxNode`. Each wrapper
13//!   pins its expected `SyntaxKind` via `can_cast` and offers
14//!   accessors that walk direct children.
15//! - [`AstToken`]: typed wrapper around a `SyntaxToken`. Provides
16//!   `text()` for the raw bytes; specific token wrappers (`Date`,
17//!   `Account`, `Number`, ...) can layer parsing on top.
18//!
19//! The wrappers are zero-cost — they store a `SyntaxNode` /
20//! `SyntaxToken` by value and forward to it. Cloning is cheap
21//! (rowan's nodes/tokens are `Arc`-backed). All accessors return
22//! `Option<_>` because the CST is lossless: a malformed input
23//! still produces a tree, just one with missing children.
24//!
25//! # Round-trip
26//!
27//! Every wrapper exposes `syntax()` returning the underlying
28//! `SyntaxNode`/`SyntaxToken`, whose `text()` reproduces the
29//! original bytes exactly. Typed-AST consumers that want to
30//! modify the source can therefore navigate via accessors and
31//! splice via raw text ranges.
32#![allow(missing_docs)] // Accessors are self-documenting via function name + return type.
33
34use crate::cst::syntax_kind::{SyntaxKind, SyntaxNode, SyntaxToken};
35
36/// Re-export of rowan's `SyntaxText` — a rope view over a
37/// `SyntaxNode`'s text without allocation. Returned by
38/// [`ErrorNode::text`] so consumers don't need a direct
39/// `rowan` dependency.
40pub use rowan::SyntaxText;
41
42/// Typed wrapper around a `SyntaxNode` of a specific
43/// `SyntaxKind`.
44pub trait AstNode: Sized {
45    /// Returns true iff `kind` is the wrapper's expected node
46    /// kind. Used by `cast` and by enum dispatch.
47    fn can_cast(kind: SyntaxKind) -> bool;
48
49    /// Wrap `syntax` if its kind matches; otherwise `None`.
50    fn cast(syntax: SyntaxNode) -> Option<Self>;
51
52    /// The underlying CST node. `text()` reproduces the original
53    /// bytes; `children()` / `children_with_tokens()` walk the
54    /// tree.
55    fn syntax(&self) -> &SyntaxNode;
56}
57
58/// Typed wrapper around a `SyntaxToken` of a specific
59/// `SyntaxKind`. Like [`AstNode`] but for leaf tokens.
60pub trait AstToken: Sized {
61    fn can_cast(kind: SyntaxKind) -> bool;
62    fn cast(token: SyntaxToken) -> Option<Self>;
63    fn syntax(&self) -> &SyntaxToken;
64
65    /// The raw token text (borrowed from the green tree, zero
66    /// allocation). Tokens are always contiguous, so a `&str`
67    /// slice is well-defined.
68    fn text(&self) -> &str {
69        self.syntax().text()
70    }
71}
72
73// ---- Helpers --------------------------------------------------
74
75/// First direct-child token of `kind` under `node`, or `None`.
76fn first_token(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
77    node.children_with_tokens()
78        .filter_map(rowan::NodeOrToken::into_token)
79        .find(|t| t.kind() == kind)
80}
81
82/// Nth (0-indexed) direct-child token of `kind` under `node`.
83fn nth_token(node: &SyntaxNode, kind: SyntaxKind, n: usize) -> Option<SyntaxToken> {
84    node.children_with_tokens()
85        .filter_map(rowan::NodeOrToken::into_token)
86        .filter(|t| t.kind() == kind)
87        .nth(n)
88}
89
90/// All direct-child tokens of `kind` under `node`.
91fn tokens_of_kind(node: &SyntaxNode, kind: SyntaxKind) -> impl Iterator<Item = SyntaxToken> + '_ {
92    node.children_with_tokens()
93        .filter_map(rowan::NodeOrToken::into_token)
94        .filter(move |t| t.kind() == kind)
95}
96
97/// First direct-child node castable to `N`.
98fn first_child<N: AstNode>(node: &SyntaxNode) -> Option<N> {
99    node.children().find_map(N::cast)
100}
101
102/// All direct-child nodes castable to `N`.
103fn children<'a, N: AstNode + 'a>(node: &'a SyntaxNode) -> impl Iterator<Item = N> + 'a {
104    node.children().filter_map(N::cast)
105}
106
107// ---- Macros ---------------------------------------------------
108
109macro_rules! ast_node {
110    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
111        $(#[$meta])*
112        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
113        pub struct $name(SyntaxNode);
114
115        impl AstNode for $name {
116            fn can_cast(kind: SyntaxKind) -> bool {
117                kind == SyntaxKind::$kind
118            }
119            fn cast(syntax: SyntaxNode) -> Option<Self> {
120                Self::can_cast(syntax.kind()).then_some(Self(syntax))
121            }
122            fn syntax(&self) -> &SyntaxNode {
123                &self.0
124            }
125        }
126    };
127}
128
129macro_rules! ast_token {
130    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
131        $(#[$meta])*
132        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
133        pub struct $name(SyntaxToken);
134
135        impl AstToken for $name {
136            fn can_cast(kind: SyntaxKind) -> bool {
137                kind == SyntaxKind::$kind
138            }
139            fn cast(token: SyntaxToken) -> Option<Self> {
140                Self::can_cast(token.kind()).then_some(Self(token))
141            }
142            fn syntax(&self) -> &SyntaxToken {
143                &self.0
144            }
145        }
146    };
147}
148
149// ---- Token wrappers -------------------------------------------
150
151ast_token!(
152    /// `DATE` token (e.g., `2024-01-15`).
153    Date, DATE
154);
155ast_token!(
156    /// `ACCOUNT` token (e.g., `Assets:Cash`).
157    Account, ACCOUNT
158);
159ast_token!(
160    /// `CURRENCY` token (e.g., `USD`).
161    CurrencyName, CURRENCY
162);
163ast_token!(
164    /// `STRING` literal (e.g., `"Coffee"`). `text()` includes the
165    /// surrounding quotes; use `text_unquoted()` for the content.
166    StringLit, STRING
167);
168
169impl StringLit {
170    /// String content with surrounding `"` stripped. Returns
171    /// `None` if the raw text isn't a well-formed quoted string.
172    /// Borrowed from the green tree (zero allocation).
173    ///
174    /// The result still contains raw escape sequences (e.g. `\"`); use
175    /// [`Self::text_decoded`] for the semantic string value.
176    pub fn text_unquoted(&self) -> Option<&str> {
177        let raw = self.text();
178        let bytes = raw.as_bytes();
179        if bytes.len() < 2 || bytes[0] != b'"' || bytes[bytes.len() - 1] != b'"' {
180            return None;
181        }
182        Some(&raw[1..raw.len() - 1])
183    }
184
185    /// The semantic string value: surrounding quotes stripped and escape
186    /// sequences decoded. `\"`→`"`, `\\`→`\`, `\n`→newline, `\t`→tab, `\r`→CR;
187    /// an unknown escape drops the backslash (`\x`→`x`), matching Beancount's
188    /// lexer. Returns an owned `String` (decoding may shrink the content).
189    /// Returns `None` if the raw text isn't a well-formed quoted string.
190    pub fn text_decoded(&self) -> Option<String> {
191        // Shared text-based decoder (one source of truth for red + green paths).
192        super::convert::decode_string_token(self.text())
193    }
194}
195
196ast_token!(
197    /// `NUMBER` token (e.g., `100.00`).
198    Number, NUMBER
199);
200ast_token!(
201    /// `META_KEY` token (e.g., `note:`). Note the trailing colon
202    /// is part of the token; use `text_without_colon()` to strip it.
203    MetaKey, META_KEY
204);
205
206impl MetaKey {
207    /// Key name with the trailing `:` stripped. Borrowed from the
208    /// green tree (zero allocation).
209    pub fn text_without_colon(&self) -> &str {
210        let raw = self.text();
211        raw.strip_suffix(':').unwrap_or(raw)
212    }
213}
214
215ast_token!(
216    /// `TAG` token (e.g., `#trip`).
217    Tag, TAG
218);
219ast_token!(
220    /// `LINK` token (e.g., `^expense-123`).
221    Link, LINK
222);
223ast_token!(
224    /// `BOOL_TRUE` token literal.
225    BoolTrue, BOOL_TRUE
226);
227ast_token!(
228    /// `BOOL_FALSE` token literal.
229    BoolFalse, BOOL_FALSE
230);
231
232// ---- Heterogeneous flag/sign token wrappers --------------------
233//
234// These wrap a SyntaxToken whose kind is one of several
235// possibilities (a transaction flag may be STAR, PENDING_KW, FLAG
236// letter, HASH, TXN_KW, or single-char CURRENCY). We deliberately
237// do NOT implement AstToken for them: AstToken::can_cast is
238// kind-only, and the CURRENCY case needs a length check (only
239// single-character CURRENCY counts as a ticker-letter flag).
240// Inherent cast() runs the full check.
241//
242// Downstream code that needs exhaustive matching should use the
243// `kind()` method paired with the dedicated `*FlagKind` enum
244// returned by `classify()` (or `Sign::classify()`), which is
245// pinned to the same variant set as `cast`.
246
247/// Exhaustive classification of a [`TransactionFlag`] token.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum TransactionFlagKind {
250    /// `*` token.
251    Star,
252    /// `!` (the `PENDING_KW` token).
253    Pending,
254    /// Single-letter `FLAG` token (e.g. `P` from `posti P`).
255    Letter,
256    /// `#` token.
257    Hash,
258    /// `txn` keyword.
259    Txn,
260    /// Single-character `CURRENCY` token used as the
261    /// ticker-letter flag.
262    CurrencyLetter,
263}
264
265/// Typed wrapper for the transaction-header flag token.
266///
267/// May be `STAR` (`*`), `PENDING_KW` (`!`), `FLAG` (letter),
268/// `HASH` (`#`), `TXN_KW` (`txn`), or single-character `CURRENCY`
269/// (ticker-letter flag, e.g. `T`). Use [`Self::classify`] for
270/// exhaustive `match` ergonomics, or the `is_*` predicates for
271/// boolean checks.
272///
273/// **Note**: [`Self::cast`] is position-AGNOSTIC — it accepts any
274/// token of a flag-eligible kind regardless of where it sits in
275/// the tree. To get the leading flag of a transaction, use
276/// [`Transaction::flag`] (which scopes the search to the
277/// pre-content header region).
278#[derive(Debug, Clone, PartialEq, Eq, Hash)]
279pub struct TransactionFlag {
280    token: SyntaxToken,
281    classification: TransactionFlagKind,
282}
283
284impl TransactionFlag {
285    /// Wrap the token if its kind is a valid transaction flag.
286    /// For `CURRENCY`, only single-character forms qualify.
287    ///
288    /// Single source of truth: this match also derives
289    /// [`Self::classify`]'s result, so cast + classify cannot
290    /// drift.
291    pub fn cast(token: SyntaxToken) -> Option<Self> {
292        let classification = match token.kind() {
293            SyntaxKind::STAR => TransactionFlagKind::Star,
294            SyntaxKind::PENDING_KW => TransactionFlagKind::Pending,
295            SyntaxKind::FLAG => TransactionFlagKind::Letter,
296            SyntaxKind::HASH => TransactionFlagKind::Hash,
297            SyntaxKind::TXN_KW => TransactionFlagKind::Txn,
298            SyntaxKind::CURRENCY if token.text().len() == 1 => TransactionFlagKind::CurrencyLetter,
299            _ => return None,
300        };
301        Some(Self {
302            token,
303            classification,
304        })
305    }
306
307    pub const fn syntax(&self) -> &SyntaxToken {
308        &self.token
309    }
310    pub fn kind(&self) -> SyntaxKind {
311        self.token.kind()
312    }
313    pub fn text(&self) -> &str {
314        self.token.text()
315    }
316
317    /// Exhaustive classification — pair with a `match` for
318    /// compiler-checked coverage of every variant. Cached at
319    /// `cast()` time; no runtime panic risk.
320    pub const fn classify(&self) -> TransactionFlagKind {
321        self.classification
322    }
323
324    pub const fn is_star(&self) -> bool {
325        matches!(self.classification, TransactionFlagKind::Star)
326    }
327    pub const fn is_pending(&self) -> bool {
328        matches!(self.classification, TransactionFlagKind::Pending)
329    }
330    pub const fn is_hash(&self) -> bool {
331        matches!(self.classification, TransactionFlagKind::Hash)
332    }
333    pub const fn is_txn(&self) -> bool {
334        matches!(self.classification, TransactionFlagKind::Txn)
335    }
336    pub const fn is_letter_flag(&self) -> bool {
337        matches!(self.classification, TransactionFlagKind::Letter)
338    }
339    pub const fn is_currency_letter(&self) -> bool {
340        matches!(self.classification, TransactionFlagKind::CurrencyLetter)
341    }
342}
343
344/// Exhaustive classification of a [`PostingFlag`] token.
345/// Same as [`TransactionFlagKind`] minus `Txn` (postings cannot
346/// carry the `txn` keyword).
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
348pub enum PostingFlagKind {
349    Star,
350    Pending,
351    Letter,
352    Hash,
353    CurrencyLetter,
354}
355
356/// Typed wrapper for a posting-line flag token. Same as
357/// [`TransactionFlag`] minus the `TXN_KW` variant (postings can't
358/// carry the `txn` keyword). Use [`Self::classify`] for
359/// exhaustive `match` ergonomics.
360///
361/// **Note**: [`Self::cast`] is position-AGNOSTIC. To get the
362/// leading flag of a posting, use [`Posting::flag`] (which
363/// scopes the search to the pre-ACCOUNT region of the posting).
364#[derive(Debug, Clone, PartialEq, Eq, Hash)]
365pub struct PostingFlag {
366    token: SyntaxToken,
367    classification: PostingFlagKind,
368}
369
370impl PostingFlag {
371    /// Single source of truth for cast + classify — drift impossible.
372    pub fn cast(token: SyntaxToken) -> Option<Self> {
373        let classification = match token.kind() {
374            SyntaxKind::STAR => PostingFlagKind::Star,
375            SyntaxKind::PENDING_KW => PostingFlagKind::Pending,
376            SyntaxKind::FLAG => PostingFlagKind::Letter,
377            SyntaxKind::HASH => PostingFlagKind::Hash,
378            SyntaxKind::CURRENCY if token.text().len() == 1 => PostingFlagKind::CurrencyLetter,
379            _ => return None,
380        };
381        Some(Self {
382            token,
383            classification,
384        })
385    }
386
387    pub const fn syntax(&self) -> &SyntaxToken {
388        &self.token
389    }
390    pub fn kind(&self) -> SyntaxKind {
391        self.token.kind()
392    }
393    pub fn text(&self) -> &str {
394        self.token.text()
395    }
396
397    /// Exhaustive classification — cached at `cast()` time;
398    /// no runtime panic risk.
399    pub const fn classify(&self) -> PostingFlagKind {
400        self.classification
401    }
402
403    pub const fn is_star(&self) -> bool {
404        matches!(self.classification, PostingFlagKind::Star)
405    }
406    pub const fn is_pending(&self) -> bool {
407        matches!(self.classification, PostingFlagKind::Pending)
408    }
409    pub const fn is_hash(&self) -> bool {
410        matches!(self.classification, PostingFlagKind::Hash)
411    }
412    pub const fn is_letter_flag(&self) -> bool {
413        matches!(self.classification, PostingFlagKind::Letter)
414    }
415    pub const fn is_currency_letter(&self) -> bool {
416        matches!(self.classification, PostingFlagKind::CurrencyLetter)
417    }
418}
419
420/// Typed wrapper for an amount sign token (`PLUS` or `MINUS`).
421///
422/// `Sign::cast` is a position-AGNOSTIC kind check: it accepts ANY
423/// `PLUS` or `MINUS` token, including operator-position signs
424/// inside arithmetic (e.g., the `-` in `10 + -5 USD`). To get the
425/// LEADING sign of an `Amount`, use [`Amount::sign`] which scopes
426/// to the first non-whitespace token of `AMOUNT`. Calling
427/// `Sign::cast` on an arbitrary token does not imply the token
428/// occupies the leading-sign position.
429#[derive(Debug, Clone, PartialEq, Eq, Hash)]
430pub struct Sign(SyntaxToken);
431
432impl Sign {
433    pub fn cast(token: SyntaxToken) -> Option<Self> {
434        matches!(token.kind(), SyntaxKind::PLUS | SyntaxKind::MINUS).then_some(Self(token))
435    }
436    pub const fn syntax(&self) -> &SyntaxToken {
437        &self.0
438    }
439    pub fn kind(&self) -> SyntaxKind {
440        self.0.kind()
441    }
442    pub fn text(&self) -> &str {
443        self.0.text()
444    }
445    pub fn is_plus(&self) -> bool {
446        self.kind() == SyntaxKind::PLUS
447    }
448    pub fn is_minus(&self) -> bool {
449        self.kind() == SyntaxKind::MINUS
450    }
451}
452
453// ---- Source file root + Directive enum ------------------------
454
455ast_node!(
456    /// Root of a parsed Beancount file. `SourceFile::parse(src)` is
457    /// the typed-AST entry point — it wraps `parse_structured`.
458    SourceFile, SOURCE_FILE
459);
460
461impl SourceFile {
462    /// Parse `source` into a typed source-file tree.
463    #[must_use]
464    pub fn parse(source: &str) -> Self {
465        let node = crate::cst::parser::parse_structured(source);
466        // `parse_structured` always produces a SOURCE_FILE root, so this cast is
467        // infallible — a parser invariant, not an input-driven path.
468        #[allow(clippy::expect_used)]
469        let source_file = Self::cast(node).expect("parse_structured always returns a SOURCE_FILE");
470        source_file
471    }
472
473    /// All recognized directives, in source order.
474    pub fn directives(&self) -> impl Iterator<Item = Directive> + '_ {
475        self.syntax().children().filter_map(Directive::cast)
476    }
477
478    /// All `ERROR_NODE` wrappers (unrecognized / malformed lines).
479    pub fn errors(&self) -> impl Iterator<Item = ErrorNode> + '_ {
480        self.syntax().children().filter_map(ErrorNode::cast)
481    }
482}
483
484// Sum-type Directive enum + AstNode impl + per-variant struct
485// declarations, all derived from a single variant list. The
486// macro is the single source of truth for "what directives
487// exist": adding a new directive requires editing exactly one
488// line. Drift between any of {can_cast, cast, syntax, per-variant
489// struct decl, per-variant AstNode impl} is structurally
490// impossible.
491//
492// Per-variant accessor methods (date(), account(), etc.) stay
493// in separate `impl SomeDirective { ... }` blocks below.
494macro_rules! directive_enum {
495    ($($(#[$variant_meta:meta])* $variant:ident($struct:ident, $kind:ident)),* $(,)?) => {
496        // Per-variant struct + AstNode impl, formerly emitted via
497        // `ast_node!` invocations. Folded into directive_enum! so
498        // the variant list is the only source of truth.
499        $(
500            $(#[$variant_meta])*
501            #[derive(Debug, Clone, PartialEq, Eq, Hash)]
502            pub struct $struct(SyntaxNode);
503
504            impl AstNode for $struct {
505                fn can_cast(kind: SyntaxKind) -> bool {
506                    kind == SyntaxKind::$kind
507                }
508                fn cast(syntax: SyntaxNode) -> Option<Self> {
509                    Self::can_cast(syntax.kind()).then_some(Self(syntax))
510                }
511                fn syntax(&self) -> &SyntaxNode {
512                    &self.0
513                }
514            }
515        )*
516
517        /// Sum type over every recognized top-level directive wrapper.
518        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
519        pub enum Directive {
520            $($variant($struct),)*
521        }
522
523        impl AstNode for Directive {
524            fn can_cast(kind: SyntaxKind) -> bool {
525                matches!(kind, $(SyntaxKind::$kind)|*)
526            }
527
528            fn cast(node: SyntaxNode) -> Option<Self> {
529                Some(match node.kind() {
530                    $(SyntaxKind::$kind => Self::$variant($struct(node)),)*
531                    _ => return None,
532                })
533            }
534
535            fn syntax(&self) -> &SyntaxNode {
536                match self {
537                    $(Self::$variant(d) => d.syntax(),)*
538                }
539            }
540        }
541    };
542}
543
544directive_enum!(
545    /// `DATE open ACCOUNT [CURRENCY[,CURRENCY]*] ["BOOKING"]`.
546    Open(OpenDirective, OPEN_DIRECTIVE),
547    /// `DATE close ACCOUNT`.
548    Close(CloseDirective, CLOSE_DIRECTIVE),
549    /// `DATE balance ACCOUNT AMOUNT_TOKENS`. Amount stays flat
550    /// (phase 2.2c scopes AMOUNT wrapping to POSTING only); walk
551    /// `number()` and `currency()` to read it.
552    Balance(BalanceDirective, BALANCE_DIRECTIVE),
553    /// `DATE pad ACCOUNT_TARGET ACCOUNT_SOURCE`.
554    Pad(PadDirective, PAD_DIRECTIVE),
555    /// `DATE event "TYPE" "VALUE"`.
556    Event(EventDirective, EVENT_DIRECTIVE),
557    /// `DATE query "NAME" "QUERY"`.
558    Query(QueryDirective, QUERY_DIRECTIVE),
559    /// `DATE note ACCOUNT "TEXT"`.
560    Note(NoteDirective, NOTE_DIRECTIVE),
561    /// `DATE document ACCOUNT "PATH"`.
562    Document(DocumentDirective, DOCUMENT_DIRECTIVE),
563    /// `DATE price CURRENCY NUMBER CURRENCY`.
564    Price(PriceDirective, PRICE_DIRECTIVE),
565    /// `DATE commodity CURRENCY`.
566    Commodity(CommodityDirective, COMMODITY_DIRECTIVE),
567    /// `pushtag #TAG`.
568    Pushtag(PushtagDirective, PUSHTAG_DIRECTIVE),
569    /// `poptag #TAG`.
570    Poptag(PoptagDirective, POPTAG_DIRECTIVE),
571    /// `pushmeta KEY: VALUE`.
572    Pushmeta(PushmetaDirective, PUSHMETA_DIRECTIVE),
573    /// `popmeta KEY:`.
574    Popmeta(PopmetaDirective, POPMETA_DIRECTIVE),
575    /// `option "KEY" "VALUE"`.
576    Option(OptionDirective, OPTION_DIRECTIVE),
577    /// `include "PATH"`.
578    Include(IncludeDirective, INCLUDE_DIRECTIVE),
579    /// `plugin "MODULE" ["CONFIG"]`.
580    Plugin(PluginDirective, PLUGIN_DIRECTIVE),
581    /// `DATE custom "TYPE" values...`. Heterogeneous value list
582    /// stays flat (phase 2.3); walk the raw token sequence
583    /// via `syntax().children_with_tokens()`.
584    Custom(CustomDirective, CUSTOM_DIRECTIVE),
585    /// `DATE FLAG ["PAYEE"] "NARRATION" #TAG... ^LINK...`
586    /// followed by indented `POSTING` lines and `META_ENTRY`
587    /// sub-lines.
588    Transaction(Transaction, TRANSACTION),
589);
590
591impl Directive {
592    /// Metadata sub-lines attached to this directive (phase 2.2a
593    /// `META_ENTRY` wrapping). Every directive wrapper may carry
594    /// indented metadata.
595    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
596        children(self.syntax())
597    }
598}
599
600ast_node!(
601    /// Wrapper for unrecognized / malformed top-level content
602    /// (PR 2.4 `ERROR_NODE`). Typed-AST consumers can use this to
603    /// surface error regions to users (e.g., LSP diagnostics).
604    ErrorNode, ERROR_NODE
605);
606
607impl ErrorNode {
608    /// The raw bytes of the malformed region as a [`SyntaxText`]
609    /// rope view. Zero allocation; use `.to_string()` on the
610    /// result if you need an owned `String`, or `format!` /
611    /// `Display` for direct output.
612    #[must_use]
613    pub fn text(&self) -> SyntaxText {
614        self.syntax().text()
615    }
616}
617
618// ---- 10 dated single-line directives (PR 2.1a) -----------------
619//
620// The 19 directive struct declarations + AstNode impls are
621// generated by the `directive_enum!` macro invocation above.
622// Per-variant accessor methods live in the `impl` blocks below.
623
624impl OpenDirective {
625    pub fn date(&self) -> Option<Date> {
626        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
627    }
628    pub fn account(&self) -> Option<Account> {
629        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
630    }
631    /// Comma-separated currency constraint list (may be empty).
632    pub fn currencies(&self) -> impl Iterator<Item = CurrencyName> + '_ {
633        tokens_of_kind(self.syntax(), SyntaxKind::CURRENCY).filter_map(CurrencyName::cast)
634    }
635    /// Optional booking-method string (e.g., `"STRICT"`).
636    pub fn booking_method(&self) -> Option<StringLit> {
637        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
638    }
639}
640
641impl CloseDirective {
642    pub fn date(&self) -> Option<Date> {
643        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
644    }
645    pub fn account(&self) -> Option<Account> {
646        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
647    }
648}
649
650impl BalanceDirective {
651    pub fn date(&self) -> Option<Date> {
652        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
653    }
654    pub fn account(&self) -> Option<Account> {
655        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
656    }
657    pub fn number(&self) -> Option<Number> {
658        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
659    }
660    pub fn currency(&self) -> Option<CurrencyName> {
661        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
662    }
663}
664
665impl PadDirective {
666    pub fn date(&self) -> Option<Date> {
667        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
668    }
669    pub fn target_account(&self) -> Option<Account> {
670        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
671    }
672    pub fn source_account(&self) -> Option<Account> {
673        nth_token(self.syntax(), SyntaxKind::ACCOUNT, 1).and_then(Account::cast)
674    }
675}
676
677impl EventDirective {
678    pub fn date(&self) -> Option<Date> {
679        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
680    }
681    pub fn event_type(&self) -> Option<StringLit> {
682        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
683    }
684    pub fn value(&self) -> Option<StringLit> {
685        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
686    }
687}
688
689impl QueryDirective {
690    pub fn date(&self) -> Option<Date> {
691        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
692    }
693    pub fn name(&self) -> Option<StringLit> {
694        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
695    }
696    pub fn query(&self) -> Option<StringLit> {
697        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
698    }
699}
700
701impl NoteDirective {
702    pub fn date(&self) -> Option<Date> {
703        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
704    }
705    pub fn account(&self) -> Option<Account> {
706        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
707    }
708    pub fn text(&self) -> Option<StringLit> {
709        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
710    }
711}
712
713impl DocumentDirective {
714    pub fn date(&self) -> Option<Date> {
715        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
716    }
717    pub fn account(&self) -> Option<Account> {
718        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
719    }
720    pub fn path(&self) -> Option<StringLit> {
721        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
722    }
723}
724
725impl PriceDirective {
726    pub fn date(&self) -> Option<Date> {
727        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
728    }
729    pub fn base_currency(&self) -> Option<CurrencyName> {
730        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
731    }
732    pub fn number(&self) -> Option<Number> {
733        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
734    }
735    pub fn quote_currency(&self) -> Option<CurrencyName> {
736        nth_token(self.syntax(), SyntaxKind::CURRENCY, 1).and_then(CurrencyName::cast)
737    }
738}
739
740impl CommodityDirective {
741    pub fn date(&self) -> Option<Date> {
742        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
743    }
744    pub fn currency(&self) -> Option<CurrencyName> {
745        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
746    }
747}
748
749// ---- 4 standalone-keyword directives (PR 2.1a) -----------------
750
751impl PushtagDirective {
752    pub fn tag(&self) -> Option<Tag> {
753        first_token(self.syntax(), SyntaxKind::TAG).and_then(Tag::cast)
754    }
755}
756
757impl PoptagDirective {
758    pub fn tag(&self) -> Option<Tag> {
759        first_token(self.syntax(), SyntaxKind::TAG).and_then(Tag::cast)
760    }
761}
762
763impl PushmetaDirective {
764    pub fn key(&self) -> Option<MetaKey> {
765        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
766    }
767}
768
769impl PopmetaDirective {
770    pub fn key(&self) -> Option<MetaKey> {
771        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
772    }
773}
774
775// ---- 4 edge directives (PR 2.3) --------------------------------
776
777impl OptionDirective {
778    pub fn key(&self) -> Option<StringLit> {
779        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
780    }
781    pub fn value(&self) -> Option<StringLit> {
782        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
783    }
784}
785
786impl IncludeDirective {
787    pub fn path(&self) -> Option<StringLit> {
788        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
789    }
790}
791
792impl PluginDirective {
793    pub fn module(&self) -> Option<StringLit> {
794        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
795    }
796    pub fn config(&self) -> Option<StringLit> {
797        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
798    }
799}
800
801impl CustomDirective {
802    pub fn date(&self) -> Option<Date> {
803        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
804    }
805    /// The type-name string (always the first `STRING` after the
806    /// `custom` keyword).
807    pub fn custom_type(&self) -> Option<StringLit> {
808        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
809    }
810}
811
812// ---- TRANSACTION + body sub-nodes ------------------------------
813
814impl Transaction {
815    /// Direct-child tokens of TRANSACTION in the header region
816    /// only: leading trivia (whitespace, newlines, comments
817    /// attached as inter-directive leading trivia per the
818    /// Directive-Terminator Rule) is skipped, then tokens are
819    /// collected until the first NEWLINE that terminates the
820    /// header line. Body content (`POSTING` / `META_ENTRY` nodes;
821    /// flat tokens emitted by `emit_transaction_body`'s catch-all
822    /// for malformed indented lines) is excluded.
823    fn header_tokens(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
824        self.syntax()
825            .children_with_tokens()
826            .filter_map(rowan::NodeOrToken::into_token)
827            // Skip leading trivia (blank-line newlines, top-of-
828            // directive whitespace, leading comments). The first
829            // non-trivia token marks the start of the header.
830            //
831            // Comment-trivia covers all four comment kinds — ledger-
832            // style `%` comments and org-mode `#!`/`#+` lines are
833            // attached as leading trivia by the Directive-Terminator
834            // Rule the same way `;` comments are, so a transaction
835            // preceded by any of them must skip them too. BOM stays
836            // OUT of the skip set: a mid-file BOM in a transaction
837            // header is a corruption to surface, not trivia.
838            .skip_while(|t| {
839                matches!(
840                    t.kind(),
841                    SyntaxKind::WHITESPACE
842                        | SyntaxKind::NEWLINE
843                        | SyntaxKind::COMMENT
844                        | SyntaxKind::PERCENT_COMMENT
845                        | SyntaxKind::SHEBANG
846                        | SyntaxKind::EMACS_DIRECTIVE
847                )
848            })
849            .take_while(|t| t.kind() != SyntaxKind::NEWLINE)
850    }
851
852    /// Header tokens BEFORE the first STRING/TAG/LINK — i.e., the
853    /// flag-position region (between DATE and the first header
854    /// content token). Used by [`Self::flag`] to scope its search.
855    fn flag_region_tokens(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
856        self.header_tokens().take_while(|t| {
857            !matches!(
858                t.kind(),
859                SyntaxKind::STRING | SyntaxKind::TAG | SyntaxKind::LINK
860            )
861        })
862    }
863
864    pub fn date(&self) -> Option<Date> {
865        // DATE is in the header, so first_token over the whole node
866        // is fine — but for symmetry, scope to header_tokens.
867        self.header_tokens()
868            .find(|t| t.kind() == SyntaxKind::DATE)
869            .and_then(Date::cast)
870    }
871
872    /// Transaction flag token. May be `STAR` (`*`), `PENDING_KW`
873    /// (`!`), `FLAG` letter, `HASH` (`#`), `TXN_KW`
874    /// (the `txn` keyword), single-char `CURRENCY` (ticker-letter
875    /// flag), or absent (implied via a leading `STRING`
876    /// payee/narration).
877    ///
878    /// Scoped to the flag-position region (between `DATE` and the
879    /// first `STRING`/`TAG`/`LINK`) so a stray trailing
880    /// single-char `CURRENCY` after the narration is NOT
881    /// misclassified as a flag.
882    pub fn flag(&self) -> Option<TransactionFlag> {
883        self.flag_region_tokens().find_map(TransactionFlag::cast)
884    }
885
886    /// All `STRING` tokens in the header, in source order.
887    ///
888    /// Scoped to the header (tokens before the terminating
889    /// `NEWLINE`), so `STRING` tokens emitted into TRANSACTION by
890    /// `emit_transaction_body`'s catch-all for malformed indented
891    /// body lines are excluded.
892    ///
893    /// The 2-string convention (`"payee" "narration"`) is the
894    /// canonical form; [`Self::payee`] and [`Self::narration`]
895    /// follow it strictly. For 3+ strings (malformed but
896    /// losslessly parsed), use this method to surface every
897    /// header string.
898    pub fn strings(&self) -> impl Iterator<Item = StringLit> + '_ {
899        self.header_tokens()
900            .filter(|t| t.kind() == SyntaxKind::STRING)
901            .filter_map(StringLit::cast)
902    }
903
904    /// The payee string, if a separate payee + narration pair is
905    /// present. Returns `Some(first)` ONLY when exactly two
906    /// header `STRING` tokens appear (the canonical
907    /// `"payee" "narration"` shape). With 0, 1, or 3+ strings
908    /// the convention is ambiguous and this returns `None` —
909    /// use [`Self::strings`] for lossless access.
910    pub fn payee(&self) -> Option<StringLit> {
911        // Take up to 3 to disambiguate 2 from 3+ without
912        // allocating the whole sequence.
913        let mut iter = self.strings();
914        let first = iter.next()?;
915        let second = iter.next()?;
916        if iter.next().is_some() {
917            None
918        } else {
919            // Exactly 2 strings; first is payee.
920            let _ = second;
921            Some(first)
922        }
923    }
924
925    /// The narration string. Returns `Some(only)` for a single
926    /// header string and `Some(last)` for the 2-string
927    /// `"payee" "narration"` form. Returns `None` for 0 or 3+
928    /// strings — use [`Self::strings`] for lossless access on
929    /// malformed headers.
930    pub fn narration(&self) -> Option<StringLit> {
931        let mut iter = self.strings();
932        let first = iter.next()?;
933        let second = iter.next();
934        let third = iter.next();
935        match (second, third) {
936            (None, _) => Some(first),
937            (Some(s2), None) => Some(s2),
938            _ => None,
939        }
940    }
941
942    /// All `#TAG` tokens attached to the transaction header.
943    /// Scoped to the header region (excludes body tokens).
944    pub fn tags(&self) -> impl Iterator<Item = Tag> + '_ {
945        self.header_tokens()
946            .filter(|t| t.kind() == SyntaxKind::TAG)
947            .filter_map(Tag::cast)
948    }
949
950    /// All `^LINK` tokens attached to the transaction header.
951    /// Scoped to the header region (excludes body tokens).
952    pub fn links(&self) -> impl Iterator<Item = Link> + '_ {
953        self.header_tokens()
954            .filter(|t| t.kind() == SyntaxKind::LINK)
955            .filter_map(Link::cast)
956    }
957
958    /// All `POSTING` sub-lines, in source order.
959    pub fn postings(&self) -> impl Iterator<Item = Posting> + '_ {
960        children(self.syntax())
961    }
962
963    /// Transaction-level `META_ENTRY` sub-lines — those not attached
964    /// to a posting (chiefly metadata preceding the first posting).
965    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
966        children(self.syntax())
967    }
968}
969
970ast_node!(
971    /// `WS [(FLAG | STAR | PENDING_KW | HASH | single-char CURRENCY) WS] ACCOUNT [AMOUNT] [COST_SPEC] [PRICE_ANNOTATION]`.
972    Posting, POSTING
973);
974
975impl Posting {
976    /// Posting flag (optional). Same kinds as
977    /// [`TransactionFlag`] minus `TXN_KW` — indicates whether
978    /// THIS posting is pending, marked, etc.
979    pub fn flag(&self) -> Option<PostingFlag> {
980        // Walk children up to the ACCOUNT; the first non-whitespace
981        // token is the flag iff it's a valid PostingFlag kind.
982        for el in self.syntax().children_with_tokens() {
983            if let rowan::NodeOrToken::Token(t) = el {
984                match t.kind() {
985                    SyntaxKind::WHITESPACE => {}
986                    SyntaxKind::ACCOUNT => return None,
987                    _ => return PostingFlag::cast(t),
988                }
989            }
990        }
991        None
992    }
993
994    pub fn account(&self) -> Option<Account> {
995        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
996    }
997
998    /// Units `AMOUNT` (optional — auto postings have none).
999    pub fn amount(&self) -> Option<Amount> {
1000        first_child(self.syntax())
1001    }
1002
1003    /// `COST_SPEC` annotation, if present.
1004    pub fn cost_spec(&self) -> Option<CostSpec> {
1005        first_child(self.syntax())
1006    }
1007
1008    /// `PRICE_ANNOTATION`, if present.
1009    pub fn price_annotation(&self) -> Option<PriceAnnotation> {
1010        first_child(self.syntax())
1011    }
1012
1013    /// Posting-attached metadata (`META_ENTRY` sub-lines following
1014    /// the posting line at the same or deeper indent).
1015    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
1016        children(self.syntax())
1017    }
1018}
1019
1020// ---- AMOUNT / COST_SPEC / PRICE_ANNOTATION / META_ENTRY --------
1021
1022ast_node!(
1023    /// Units amount: `[sign] (NUMBER | PAREN_EXPR) ([WS] op
1024    /// [WS] [sign] (NUMBER | PAREN_EXPR))* [WS CURRENCY]`, or a
1025    /// bare `CURRENCY`. Phase 2.4 extension supports arithmetic.
1026    Amount, AMOUNT
1027);
1028
1029impl Amount {
1030    /// Sign token (`MINUS` or `PLUS`), if present as the FIRST
1031    /// non-whitespace child of AMOUNT. Returns `None` if no
1032    /// sign or if the leading non-whitespace token is something
1033    /// else (e.g., `L_PAREN`, `NUMBER`, `CURRENCY`).
1034    pub fn sign(&self) -> Option<Sign> {
1035        let first = self
1036            .syntax()
1037            .children_with_tokens()
1038            .filter_map(rowan::NodeOrToken::into_token)
1039            .find(|t| t.kind() != SyntaxKind::WHITESPACE)?;
1040        Sign::cast(first)
1041    }
1042
1043    /// First `NUMBER` child token (the leading operand). For an
1044    /// arithmetic expression like `10+5 USD`, this is `10`; for
1045    /// a bare CURRENCY amount this is `None`.
1046    pub fn number(&self) -> Option<Number> {
1047        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1048    }
1049
1050    /// The trailing currency at paren-depth 0.
1051    ///
1052    /// For `100 USD`, `100USD`, `(1+2) USD`: returns the trailing
1053    /// `USD`. For bare currency-only `AMOUNT(CURRENCY)`: returns
1054    /// the same token. For malformed `(1 USD)` (CURRENCY inside
1055    /// parens, no outer trailing currency): returns `None`. For
1056    /// unclosed `(1 USD\n` or stray-closer `1 USD)` (unbalanced
1057    /// parens): returns `None`, refusing to surface a possibly
1058    /// paren-internal currency.
1059    ///
1060    /// Single forward pass with paren-depth tracking; no
1061    /// allocation. `emit_amount_operand` keeps paren contents
1062    /// flat under AMOUNT (no `PAREN_EXPR` sub-node), so depth
1063    /// tracking is the only structural disambiguator.
1064    pub fn currency(&self) -> Option<CurrencyName> {
1065        let mut depth: i32 = 0;
1066        let mut last_at_depth_0: Option<SyntaxToken> = None;
1067        for el in self.syntax().children_with_tokens() {
1068            let rowan::NodeOrToken::Token(t) = el else {
1069                continue;
1070            };
1071            match t.kind() {
1072                SyntaxKind::L_PAREN => depth += 1,
1073                SyntaxKind::R_PAREN => depth -= 1,
1074                SyntaxKind::CURRENCY if depth == 0 => last_at_depth_0 = Some(t),
1075                _ => {}
1076            }
1077        }
1078        // Unbalanced parens (unclosed or stray closer): refuse to
1079        // surface a currency rather than guess.
1080        if depth != 0 {
1081            return None;
1082        }
1083        last_at_depth_0.and_then(CurrencyName::cast)
1084    }
1085
1086    /// Returns true iff the amount contains an arithmetic operator
1087    /// (`+`, `-` between operands, `*`, `/`) or a parenthesized
1088    /// sub-expression — useful for typed-AST consumers that need
1089    /// to defer to expression evaluation.
1090    #[must_use]
1091    pub fn is_arithmetic(&self) -> bool {
1092        let mut seen_first_operand = false;
1093        for el in self.syntax().children_with_tokens() {
1094            if let rowan::NodeOrToken::Token(t) = el {
1095                match t.kind() {
1096                    SyntaxKind::NUMBER => seen_first_operand = true,
1097                    SyntaxKind::L_PAREN | SyntaxKind::R_PAREN => return true,
1098                    SyntaxKind::STAR | SyntaxKind::SLASH => return true,
1099                    SyntaxKind::PLUS | SyntaxKind::MINUS if seen_first_operand => return true,
1100                    _ => {}
1101                }
1102            }
1103        }
1104        false
1105    }
1106}
1107
1108ast_node!(
1109    /// Bracketed cost annotation: `{...}` (per-unit), `{#...}`
1110    /// (per-unit + total), or `{{...}}` (total-only). Contents
1111    /// stay flat (phase 2.2c); accessors scan the children.
1112    CostSpec, COST_SPEC
1113);
1114
1115impl CostSpec {
1116    /// Returns true iff the opener is `{{` (total-cost form).
1117    #[must_use]
1118    pub fn is_total(&self) -> bool {
1119        first_token(self.syntax(), SyntaxKind::L_DOUBLE_BRACE).is_some()
1120    }
1121
1122    /// Returns true iff the opener is `{#` (per-unit + total
1123    /// form).
1124    #[must_use]
1125    pub fn is_per_unit_plus_total(&self) -> bool {
1126        first_token(self.syntax(), SyntaxKind::L_BRACE_HASH).is_some()
1127    }
1128
1129    /// Cost number (first NUMBER child token).
1130    pub fn number(&self) -> Option<Number> {
1131        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1132    }
1133
1134    /// Cost currency (first CURRENCY child token).
1135    pub fn currency(&self) -> Option<CurrencyName> {
1136        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
1137    }
1138
1139    /// Cost date (first DATE child token), if present.
1140    pub fn date(&self) -> Option<Date> {
1141        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
1142    }
1143
1144    /// Cost label (first STRING child token), if present.
1145    pub fn label(&self) -> Option<StringLit> {
1146        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
1147    }
1148
1149    /// Returns true iff the opener is immediately followed by a
1150    /// `*` merge marker (e.g., `{*}` or `{* 500 USD}`). A STAR
1151    /// elsewhere in the cost spec is the multiplication operator
1152    /// (e.g., `{500 * 2 USD}`), NOT a merge marker; position
1153    /// matters.
1154    #[must_use]
1155    pub fn is_merge(&self) -> bool {
1156        let mut past_opener = false;
1157        for el in self.syntax().children_with_tokens() {
1158            if let rowan::NodeOrToken::Token(t) = el {
1159                match t.kind() {
1160                    SyntaxKind::L_BRACE | SyntaxKind::L_DOUBLE_BRACE | SyntaxKind::L_BRACE_HASH => {
1161                        past_opener = true;
1162                    }
1163                    SyntaxKind::WHITESPACE if past_opener => {}
1164                    SyntaxKind::STAR if past_opener => return true,
1165                    _ if past_opener => return false,
1166                    _ => {}
1167                }
1168            }
1169        }
1170        false
1171    }
1172}
1173
1174ast_node!(
1175    /// Price annotation: `AT [WS AMOUNT]` (per-unit) or
1176    /// `AT_AT [WS AMOUNT]` (total).
1177    PriceAnnotation, PRICE_ANNOTATION
1178);
1179
1180impl PriceAnnotation {
1181    /// Returns true iff the opener is `@@` (total-price form).
1182    #[must_use]
1183    pub fn is_total(&self) -> bool {
1184        first_token(self.syntax(), SyntaxKind::AT_AT).is_some()
1185    }
1186
1187    /// The price's inner `AMOUNT`, if present.
1188    pub fn amount(&self) -> Option<Amount> {
1189        first_child(self.syntax())
1190    }
1191}
1192
1193ast_node!(
1194    /// Metadata sub-line: `WS META_KEY ... (NEWLINE | EOF)`.
1195    /// Key is the `META_KEY` token; value is the remaining flat
1196    /// content tokens. Use `key()` and `value_*()` accessors.
1197    MetaEntry, META_ENTRY
1198);
1199
1200impl MetaEntry {
1201    pub fn key(&self) -> Option<MetaKey> {
1202        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
1203    }
1204
1205    /// Value as a typed STRING, if the value is a quoted string.
1206    pub fn value_string(&self) -> Option<StringLit> {
1207        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
1208    }
1209
1210    /// Value as a NUMBER token, if the value is numeric.
1211    pub fn value_number(&self) -> Option<Number> {
1212        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1213    }
1214
1215    /// Value as a DATE token, if the value is a date literal.
1216    pub fn value_date(&self) -> Option<Date> {
1217        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
1218    }
1219
1220    /// Value as an ACCOUNT token.
1221    pub fn value_account(&self) -> Option<Account> {
1222        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
1223    }
1224
1225    /// Value as a CURRENCY token.
1226    pub fn value_currency(&self) -> Option<CurrencyName> {
1227        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
1228    }
1229
1230    /// Value as a boolean (true / false token).
1231    pub fn value_bool(&self) -> Option<bool> {
1232        for el in self.syntax().children_with_tokens() {
1233            if let rowan::NodeOrToken::Token(t) = el {
1234                match t.kind() {
1235                    SyntaxKind::BOOL_TRUE => return Some(true),
1236                    SyntaxKind::BOOL_FALSE => return Some(false),
1237                    _ => {}
1238                }
1239            }
1240        }
1241        None
1242    }
1243}