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}