1use crate::{
2 lexer::Token,
3 options::{BeancountOption, BeancountOptionError, DEFAULT_LONG_STRING_MAXLINES, ParserOptions},
4 types::*,
5};
6use chumsky::{
7 input::BorrowInput,
8 prelude::{
9 IterParser, Parser, Rich, any_ref, choice, end, extra, group, just, recursive, select_ref,
10 skip_then_retry_until,
11 },
12};
13use either::Either;
14use rust_decimal::Decimal;
15use std::{
16 collections::{HashMap, HashSet, hash_map},
17 iter::once,
18 ops::Deref,
19 path::Path,
20};
21use time::Date;
22
23pub(crate) fn includes<'s, I>() -> impl Parser<'s, I, Vec<String>, Extra<'s>>
25where
26 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
27{
28 (just(Token::Include).ignore_then(string()).map(Some))
29 .or(any_ref().map(|_| None))
30 .repeated()
31 .collect::<Vec<_>>()
32 .map(|includes| {
33 includes
34 .into_iter()
35 .filter_map(|s| s.as_ref().map(|s| s.to_string()))
36 .collect::<Vec<_>>()
37 })
38}
39
40pub(crate) fn file<'s, I>(
42 source_path: Option<&'s Path>,
43) -> impl Parser<'s, I, Vec<Spanned<Declaration<'s>>>, Extra<'s>>
44where
45 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
46{
47 declaration(source_path).repeated().collect::<Vec<_>>()
48}
49
50pub(crate) fn declaration<'s, I>(
52 source_path: Option<&'s Path>,
53) -> impl Parser<'s, I, Spanned<Declaration<'s>>, Extra<'s>>
54where
55 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
56{
57 use Declaration::*;
58
59 choice((directive().map(Directive), pragma(source_path).map(Pragma)))
60 .map_with(spanned_extra)
61 .recover_with(skip_then_retry_until(any_ref().ignored(), end()))
62}
63
64pub(crate) fn directive<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
66where
67 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
68{
69 choice((
70 transaction().labelled("transaction").as_context(),
71 choice((
72 price(),
73 balance(),
74 open(),
75 close(),
76 commodity(),
77 pad(),
78 document(),
79 note(),
80 event(),
81 query(),
82 custom(),
83 ))
84 .labelled("directive")
85 .as_context(),
86 ))
87}
88
89pub(crate) fn pragma<'s, I>(
91 source_path: Option<&'s Path>,
92) -> impl Parser<'s, I, Pragma<'s>, Extra<'s>>
93where
94 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
95{
96 choice((
97 just(Token::Pushtag)
98 .ignore_then(tag())
99 .map_with(|tag, e| Pragma::Pushtag(spanned_(tag, e.span()))),
100 just(Token::Poptag)
101 .ignore_then(tag())
102 .map_with(|tag, e| Pragma::Poptag(spanned_(tag, e.span()))),
103 just(Token::Pushmeta)
104 .ignore_then(meta_key_value())
105 .map(Pragma::Pushmeta),
106 just(Token::Popmeta)
107 .ignore_then(key())
108 .then_ignore(just(Token::Colon))
109 .map_with(|key, e| Pragma::Popmeta(spanned_(key, e.span()))),
110 just(Token::Include)
111 .ignore_then(string().map_with(|path, e| Pragma::Include(spanned_(path, e.span())))),
112 option(source_path).map(Pragma::Option),
113 just(Token::Plugin)
114 .ignore_then(string().map_with(spanned_extra))
115 .then(string().map_with(spanned_extra).or_not())
116 .map(|(module_name, config)| {
117 Pragma::Plugin(Plugin {
118 module_name,
119 config,
120 })
121 }),
122 ))
123 .then_ignore(choice((just(Token::Eol).ignored(), end())))
124 .labelled("directive") .as_context()
126}
127
128pub(crate) fn option<'s, I>(
130 source_path: Option<&'s Path>,
131) -> impl Parser<'s, I, BeancountOption<'s>, Extra<'s>>
132where
133 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
134{
135 just(Token::Option)
136 .ignore_then(string().map_with(|name, e| spanned_(name, e.span())))
137 .then(string().map_with(|value, e| spanned_(value, e.span())))
138 .validate(move |(name, value), e, emitter| {
139 use BeancountOptionError::*;
143
144 let opt = BeancountOption::parse(name, value, source_path)
145 .map_err(|e| match e {
146 UnknownOption => Rich::custom(name.span.into(), e.to_string()),
147 BadValue(_) => Rich::custom(value.span.into(), e.to_string()),
148 })
149 .and_then(|opt| {
150 let parser_state: &mut extra::SimpleState<ParserState> = e.state();
151 parser_state
152 .options
153 .assimilate(opt)
154 .map_err(|e| Rich::custom(value.span.into(), e.to_string()))
155 });
156
157 opt.unwrap_or_else(|e| {
158 emitter.emit(e);
159 BeancountOption::ignored()
160 })
161 })
162}
163
164pub(crate) fn transaction<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
166where
167 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
168{
169 group((
170 transaction_header_line(),
171 metadata().map_with(spanned_extra),
172 posting().repeated().collect::<Vec<_>>(),
173 ))
174 .validate(
175 |((date, flag, (payee, narration), (tags, links)), mut metadata, postings),
176 _span,
177 emitter| {
178 metadata.merge_tags(&tags, emitter);
179 metadata.merge_links(&links, emitter);
180
181 Directive {
182 date,
183 metadata,
184 variant: DirectiveVariant::Transaction(Transaction {
185 flag,
186 payee,
187 narration,
188 postings,
189 }),
190 }
191 },
192 )
193}
194
195type TransactionHeaderLine<'s> = (
196 Spanned<Date>,
197 Spanned<Flag>,
198 (Option<Spanned<&'s str>>, Option<Spanned<&'s str>>),
199 (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>),
200);
201
202fn transaction_header_line<'s, I>() -> impl Parser<'s, I, TransactionHeaderLine<'s>, Extra<'s>>
204where
205 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
206{
207 group((
208 date().map_with(spanned_extra),
209 txn().map_with(spanned_extra),
210 group((
212 string().map_with(spanned_extra).or_not(),
213 string().map_with(spanned_extra).or_not(),
214 ))
215 .map(|(s1, s2)| match (s1, s2) {
216 (Some(s1), None) => (None, Some(s1)),
218 (s1, s2) => (s1, s2),
219 })
220 .map(|(payee, narration)| {
221 (
222 replace_some_empty_with_none(payee),
223 replace_some_empty_with_none(narration),
224 )
225 }),
226 tags_links(),
227 ))
228 .then_ignore(choice((just(Token::Eol).ignored(), end())))
229}
230
231fn replace_some_empty_with_none(s: Option<Spanned<&str>>) -> Option<Spanned<&str>> {
232 match s {
233 Some(maybe_empty) => {
234 if maybe_empty.is_empty() {
235 None
236 } else {
237 s
238 }
239 }
240 None => None,
241 }
242}
243
244pub(crate) fn price<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
246where
247 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
248{
249 group((
250 date().map_with(spanned_extra),
251 just(Token::Price),
252 currency().map_with(spanned_extra),
253 amount().map_with(spanned_extra),
254 tags_links(),
255 ))
256 .then_ignore(choice((just(Token::Eol).ignored(), end())))
257 .then(metadata().map_with(spanned_extra))
258 .validate(
259 |((date, _, currency, amount, (tags, links)), mut metadata), _span, emitter| {
260 metadata.merge_tags(&tags, emitter);
261 metadata.merge_links(&links, emitter);
262 Directive {
263 date,
264 metadata,
265 variant: DirectiveVariant::Price(Price { currency, amount }),
266 }
267 },
268 )
269 .labelled("price")
270 .as_context()
271}
272
273pub(crate) fn balance<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
275where
276 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
277{
278 group((
279 date().map_with(spanned_extra),
280 just(Token::Balance),
281 account().map_with(spanned_extra),
282 amount_with_tolerance().map_with(spanned_extra),
283 tags_links(),
284 ))
285 .then_ignore(choice((just(Token::Eol).ignored(), end())))
286 .then(metadata().map_with(spanned_extra))
287 .validate(
288 |((date, _, account, atol, (tags, links)), mut metadata), _span, emitter| {
289 metadata.merge_tags(&tags, emitter);
290 metadata.merge_links(&links, emitter);
291 Directive {
292 date,
293 metadata,
294 variant: DirectiveVariant::Balance(Balance { account, atol }),
295 }
296 },
297 )
298 .labelled("balance")
299 .as_context()
300}
301
302pub(crate) fn open<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
304where
305 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
306{
307 group((open_header_line(), metadata().map_with(spanned_extra))).validate(
308 |((date, account, currencies, booking, (tags, links)), mut metadata), _span, emitter| {
309 metadata.merge_tags(&tags, emitter);
310 metadata.merge_links(&links, emitter);
311
312 Directive {
313 date,
314 metadata,
315 variant: DirectiveVariant::Open(Open {
316 account,
317 currencies,
318 booking,
319 }),
320 }
321 },
322 )
323}
324
325type OpenHeaderLine<'s> = (
326 Spanned<Date>,
327 Spanned<Account<'s>>,
328 HashSet<Spanned<Currency<'s>>>,
329 Option<Spanned<Booking>>,
330 (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>),
331);
332
333fn open_header_line<'s, I>() -> impl Parser<'s, I, OpenHeaderLine<'s>, Extra<'s>>
335where
336 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
337{
338 group((
339 date().map_with(spanned_extra),
340 just(Token::Open),
341 account().map_with(spanned_extra),
342 currency_list(),
343 booking().map_with(spanned_extra).or_not(),
344 tags_links(),
345 ))
346 .then_ignore(choice((just(Token::Eol).ignored(), end())))
347 .map(|(date, _, account, currency, booking, tags_links)| {
348 (date, account, currency, booking, tags_links)
349 })
350}
351
352fn currency_list<'s, I>() -> impl Parser<'s, I, HashSet<Spanned<Currency<'s>>>, Extra<'s>>
354where
355 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
356{
357 group((
358 currency().map_with(spanned_extra),
359 (just(Token::Comma).ignore_then(currency().map_with(spanned_extra)))
360 .repeated()
361 .collect::<Vec<_>>(),
362 ))
363 .validate(|(first_currency, mut currencies), _span, emitter| {
364 currencies.push(first_currency);
365 currencies
366 .into_iter()
367 .fold(HashSet::new(), |mut currencies, currency| {
368 if currencies.contains(¤cy) {
369 emitter.emit(Rich::custom(
370 currency.span.into(),
371 format!("duplicate currency {}", currency),
372 ))
373 } else {
374 currencies.insert(currency);
375 }
376
377 currencies
378 })
379 })
380 .or_not()
381 .map(|currencies| currencies.unwrap_or_default())
382}
383
384fn account<'s, I>() -> impl Parser<'s, I, Account<'s>, Extra<'s>>
386where
387 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
388{
389 let s = select_ref!(Token::Account(s) => *s);
390
391 s.try_map_with(|s, e| {
392 let span = e.span();
393 let parser_state: &mut extra::SimpleState<ParserState> = e.state();
396 let account_type_names = &parser_state.options.account_type_names;
397
398 Account::new(s, account_type_names).map_err(|e| Rich::custom(span, e.to_string()))
399 })
400}
401
402fn booking<'s, I>() -> impl Parser<'s, I, Booking, Extra<'s>>
404where
405 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
406{
407 string().try_map(|s, span| Booking::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
408}
409
410pub(crate) fn close<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
412where
413 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
414{
415 group((
416 date().map_with(spanned_extra),
417 just(Token::Close),
418 account().map_with(spanned_extra),
419 tags_links(),
420 ))
421 .then_ignore(choice((just(Token::Eol).ignored(), end())))
422 .then(metadata().map_with(spanned_extra))
423 .validate(
424 |((date, _, account, (tags, links)), mut metadata), _span, emitter| {
425 metadata.merge_tags(&tags, emitter);
426 metadata.merge_links(&links, emitter);
427
428 Directive {
429 date,
430 metadata,
431 variant: DirectiveVariant::Close(Close { account }),
432 }
433 },
434 )
435}
436
437pub(crate) fn commodity<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
439where
440 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
441{
442 group((
443 date().map_with(spanned_extra),
444 just(Token::Commodity),
445 currency().map_with(spanned_extra),
446 tags_links(),
447 ))
448 .then_ignore(choice((just(Token::Eol).ignored(), end())))
449 .then(metadata().map_with(spanned_extra))
450 .validate(
451 |((date, _, currency, (tags, links)), mut metadata), _span, emitter| {
452 metadata.merge_tags(&tags, emitter);
453 metadata.merge_links(&links, emitter);
454
455 Directive {
456 date,
457 metadata,
458 variant: DirectiveVariant::Commodity(Commodity { currency }),
459 }
460 },
461 )
462}
463
464pub(crate) fn pad<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
466where
467 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
468{
469 group((
470 date().map_with(spanned_extra),
471 just(Token::Pad),
472 account().map_with(spanned_extra),
473 account().map_with(spanned_extra),
474 tags_links(),
475 ))
476 .then_ignore(choice((just(Token::Eol).ignored(), end())))
477 .then(metadata().map_with(spanned_extra))
478 .validate(
479 |((date, _, account, source, (tags, links)), mut metadata), _span, emitter| {
480 metadata.merge_tags(&tags, emitter);
481 metadata.merge_links(&links, emitter);
482
483 Directive {
484 date,
485 metadata,
486 variant: DirectiveVariant::Pad(Pad { account, source }),
487 }
488 },
489 )
490}
491
492pub(crate) fn document<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
494where
495 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
496{
497 group((
498 date().map_with(spanned_extra),
499 just(Token::Document),
500 account().map_with(spanned_extra),
501 string().map_with(spanned_extra),
502 tags_links(),
503 ))
504 .then_ignore(choice((just(Token::Eol).ignored(), end())))
505 .then(metadata().map_with(spanned_extra))
506 .validate(
507 |((date, _, account, path, (tags, links)), mut metadata), _span, emitter| {
508 metadata.merge_tags(&tags, emitter);
509 metadata.merge_links(&links, emitter);
510
511 Directive {
512 date,
513 metadata,
514 variant: DirectiveVariant::Document(Document { account, path }),
515 }
516 },
517 )
518}
519
520pub(crate) fn note<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
522where
523 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
524{
525 group((
526 date().map_with(spanned_extra),
527 just(Token::Note),
528 account().map_with(spanned_extra),
529 string().map_with(spanned_extra),
530 tags_links(),
531 ))
532 .then_ignore(choice((just(Token::Eol).ignored(), end())))
533 .then(metadata().map_with(spanned_extra))
534 .validate(
535 |((date, _, account, comment, (tags, links)), mut metadata), _span, emitter| {
536 metadata.merge_tags(&tags, emitter);
537 metadata.merge_links(&links, emitter);
538
539 Directive {
540 date,
541 metadata,
542 variant: DirectiveVariant::Note(Note { account, comment }),
543 }
544 },
545 )
546}
547
548pub(crate) fn event<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
550where
551 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
552{
553 group((
554 date().map_with(spanned_extra),
555 just(Token::Event),
556 string().map_with(spanned_extra),
557 string().map_with(spanned_extra),
558 tags_links(),
559 ))
560 .then_ignore(choice((just(Token::Eol).ignored(), end())))
561 .then(metadata().map_with(spanned_extra))
562 .validate(
563 |((date, _, event_type, description, (tags, links)), mut metadata), _span, emitter| {
564 metadata.merge_tags(&tags, emitter);
565 metadata.merge_links(&links, emitter);
566
567 Directive {
568 date,
569 metadata,
570 variant: DirectiveVariant::Event(Event {
571 event_type,
572 description,
573 }),
574 }
575 },
576 )
577}
578
579pub(crate) fn query<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
581where
582 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
583{
584 group((
585 date().map_with(spanned_extra),
586 just(Token::Query),
587 string().map_with(spanned_extra),
588 string().map_with(spanned_extra),
589 tags_links(),
590 ))
591 .then_ignore(choice((just(Token::Eol).ignored(), end())))
592 .then(metadata().map_with(spanned_extra))
593 .validate(
594 |((date, _, name, content, (tags, links)), mut metadata), _span, emitter| {
595 metadata.merge_tags(&tags, emitter);
596 metadata.merge_links(&links, emitter);
597
598 Directive {
599 date,
600 metadata,
601 variant: DirectiveVariant::Query(Query { name, content }),
602 }
603 },
604 )
605}
606
607pub(crate) fn custom<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
609where
610 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
611{
612 group((
613 date().map_with(spanned_extra),
614 just(Token::Custom),
615 string().map_with(spanned_extra),
616 meta_value()
617 .map_with(spanned_extra)
618 .repeated()
619 .collect::<Vec<_>>(),
620 ))
621 .then_ignore(choice((just(Token::Eol).ignored(), end())))
622 .then(metadata().map_with(spanned_extra))
623 .map(|((date, _, type_, values), metadata)| Directive {
624 date,
625 metadata,
626 variant: DirectiveVariant::Custom(Custom { type_, values }),
627 })
628}
629
630pub(crate) fn txn<'s, I>() -> impl Parser<'s, I, Flag, Extra<'s>>
632where
633 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
634{
635 choice((just(Token::Txn).to(Flag::default()), flag()))
636}
637
638pub(crate) fn flag<'s, I>() -> impl Parser<'s, I, Flag, Extra<'s>>
640where
641 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
642{
643 let dedicated_flag = select_ref!(Token::DedicatedFlag(flag) => *flag);
644
645 choice((
646 dedicated_flag,
647 just(Token::Asterisk).to(Flag::Asterisk),
648 just(Token::Hash).to(Flag::Hash),
649 ))
650}
651
652fn posting<'s, I>() -> impl Parser<'s, I, Spanned<Posting<'s>>, Extra<'s>>
654where
655 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
656{
657 just(Token::Indent)
658 .ignore_then(
659 group((
660 flag().map_with(spanned_extra).or_not(),
661 account().map_with(spanned_extra),
662 expr_value().map_with(spanned_extra).or_not(),
663 currency().map_with(spanned_extra).or_not(),
664 cost_spec().or_not().map_with(|cost_spec, e| {
665 cost_spec.map(|cost_spec| spanned_(cost_spec, e.span()))
666 }),
667 price_annotation().or_not().map_with(|price_spec, e| {
668 price_spec.map(|price_spec| spanned_(price_spec, e.span()))
669 }),
670 ))
671 .map_with(spanned_extra)
672 .then_ignore(choice((just(Token::Eol).ignored(), end())))
673 .then(metadata().map_with(spanned_extra))
674 .map(
675 |(
676 Spanned {
677 item: (flag, account, amount, currency, cost_spec, price_annotation),
678 span: posting_span_without_metadata,
679 },
680 metadata,
681 )| {
682 spanned(
683 Posting {
684 flag,
685 account,
686 amount,
687 currency,
688 cost_spec,
689 price_annotation,
690 metadata,
691 },
692 posting_span_without_metadata,
693 )
694 },
695 ),
696 )
697 .labelled("posting")
698 .as_context()
699}
700
701fn metadata<'s, I>() -> impl Parser<'s, I, Metadata<'s>, Extra<'s>>
703where
704 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
705{
706 use Metadatum::*;
707
708 metadatum_line()
709 .repeated()
710 .collect::<Vec<_>>()
711 .validate(|metadata, _span, emitter| {
712 metadata
714 .into_iter()
715 .fold(Metadata::default(), |mut m, item| match item {
716 KeyValue(kv) => {
717 use hash_map::Entry::*;
718
719 let MetaKeyValue { key, value } = kv.item;
720
721 let key_span = key.span;
722 match m.key_values.entry(key) {
723 Occupied(entry) => emitter.emit(Rich::custom(
724 key_span.into(),
725 format!("duplicate key {}", entry.key()),
726 )),
727 Vacant(entry) => {
728 entry.insert(value);
729 }
730 }
731
732 m
733 }
734 Tag(tag) => {
735 if m.tags.contains(&tag) {
736 emitter.emit(Rich::custom(
737 tag.span.into(),
738 format!("duplicate tag {}", tag),
739 ))
740 } else {
741 m.tags.insert(tag);
742 }
743
744 m
745 }
746 Link(link) => {
747 if m.links.contains(&link) {
748 emitter.emit(Rich::custom(
749 link.span.into(),
750 format!("duplicate link {}", link),
751 ))
752 } else {
753 m.links.insert(link);
754 }
755
756 m
757 }
758 })
759 })
760}
761
762enum Metadatum<'a> {
764 KeyValue(Spanned<MetaKeyValue<'a>>),
765 Tag(Spanned<Tag<'a>>),
766 Link(Spanned<Link<'a>>),
767}
768
769fn meta_key_value<'s, I>() -> impl Parser<'s, I, MetaKeyValue<'s>, Extra<'s>>
771where
772 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
773{
774 key()
775 .map_with(spanned_extra)
776 .then(just(Token::Colon).ignore_then(meta_value().or_not().map_with(spanned_extra)))
777 .map(|(key, value)| MetaKeyValue {
778 key,
779 value: value.map_into(|value| value.unwrap_or(MetaValue::Simple(SimpleValue::Null))),
780 })
781}
782
783fn metadatum_line<'s, I>() -> impl Parser<'s, I, Metadatum<'s>, Extra<'s>>
785where
786 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
787{
788 use Metadatum::*;
789
790 just(Token::Indent)
791 .ignore_then(
792 choice((
793 meta_key_value().map_with(spanned_extra).map(KeyValue),
794 tag().map_with(spanned_extra).map(Tag),
795 link().map_with(spanned_extra).map(Link),
796 ))
797 .then_ignore(choice((just(Token::Eol).ignored(), end()))),
798 )
799 .labelled("metadata")
800 .as_context()
801}
802
803pub(crate) fn meta_value<'s, I>() -> impl Parser<'s, I, MetaValue<'s>, Extra<'s>>
805where
806 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
807{
808 use MetaValue::*;
809
810 choice((amount().map(Amount), simple_value().map(Simple)))
812}
813
814pub(crate) fn simple_value<'s, I>() -> impl Parser<'s, I, SimpleValue<'s>, Extra<'s>>
816where
817 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
818{
819 use SimpleValue::*;
820
821 choice((
822 string().map(String),
823 currency().map(Currency),
824 account().map(Account),
825 tag().map(Tag),
826 link().map(Link),
827 date().map(Date),
828 bool().map(Bool),
829 just(Token::Null).to(Null),
830 expr_value().map(Expr),
831 ))
832}
833
834pub(crate) fn amount<'s, I>() -> impl Parser<'s, I, Amount<'s>, Extra<'s>>
835where
836 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
837{
838 group((
839 expr_value().map_with(spanned_extra),
840 currency().map_with(spanned_extra),
841 ))
842 .map(Amount::new)
843}
844
845pub(crate) fn amount_with_tolerance<'s, I>()
846-> impl Parser<'s, I, AmountWithTolerance<'s>, Extra<'s>>
847where
848 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
849{
850 choice((
851 amount().map_with(|amount, e| AmountWithTolerance::new((spanned_extra(amount, e), None))),
852 group((
853 expr_value().map_with(spanned_extra),
854 just(Token::Tilde),
855 decimal().map_with(spanned_extra),
856 currency().map_with(spanned_extra),
857 ))
858 .map_with(|(number, _, tolerance, currency), e| {
859 AmountWithTolerance::new((
860 spanned_extra(Amount::new((number, currency)), e),
861 Some(tolerance),
862 ))
863 }),
864 ))
865}
866
867pub(crate) fn loose_amount<'s, I>() -> impl Parser<'s, I, LooseAmount<'s>, Extra<'s>>
868where
869 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
870{
871 group((
872 expr_value().map_with(spanned_extra).or_not(),
873 currency().map_with(spanned_extra).or_not(),
874 ))
875 .map(LooseAmount::new)
876}
877
878pub(crate) fn compound_amount<'s, I>() -> impl Parser<'s, I, CompoundAmount<'s>, Extra<'s>>
879where
880 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
881{
882 use CompoundAmount::*;
883
884 choice((
885 (compound_expr().then(currency())).map(|(amount, cur)| CurrencyAmount(amount, cur)),
886 compound_expr().map(BareAmount),
887 just(Token::Hash) .or_not()
889 .ignore_then(currency().map(BareCurrency)),
890 ))
891}
892
893pub(crate) fn compound_expr<'s, I>() -> impl Parser<'s, I, CompoundExprValue, Extra<'s>>
894where
895 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
896{
897 use CompoundExprValue::*;
898
899 choice((
900 expr_value()
902 .then_ignore(just(Token::Hash))
903 .then(expr_value())
904 .map(|(per_unit, total)| PerUnitAndTotal(per_unit, total)),
905 expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
906 expr_value().map(PerUnit),
907 just(Token::Hash).ignore_then(expr_value()).map(Total),
908 ))
909}
910
911pub(crate) fn scoped_expr<'s, I>() -> impl Parser<'s, I, ScopedExprValue, Extra<'s>>
912where
913 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
914{
915 use ScopedExprValue::*;
916
917 choice((
918 expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
919 expr_value().map(PerUnit),
920 just(Token::Hash).ignore_then(expr_value()).map(Total),
921 ))
922}
923
924pub(crate) fn price_annotation<'s, I>() -> impl Parser<'s, I, PriceSpec<'s>, Extra<'s>>
925where
926 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
927{
928 use PriceSpec::*;
929
930 fn scope(amount: ExprValue, is_total: bool) -> ScopedExprValue {
931 use ScopedExprValue::*;
932
933 if is_total {
934 Total(amount)
935 } else {
936 PerUnit(amount)
937 }
938 }
939
940 group((
941 choice((just(Token::At).to(false), just(Token::AtAt).to(true))),
942 expr_value().or_not(),
943 currency().or_not(),
944 ))
945 .try_map(|(is_total, amount, cur), _span| match (amount, cur) {
946 (Some(amount), Some(cur)) => Ok(CurrencyAmount(scope(amount, is_total), cur)),
947 (Some(amount), None) => Ok(BareAmount(scope(amount, is_total))),
948 (None, Some(cur)) => Ok(BareCurrency(cur)),
949 (None, None) => Ok(Unspecified),
950 })
951}
952
953fn cost_spec<'s, I>() -> impl Parser<'s, I, CostSpec<'s>, Extra<'s>>
956where
957 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
958{
959 use self::CompoundAmount::*;
960 use CostComp::*;
961
962 just(Token::Lcurl)
963 .ignore_then(
964 group((
965 cost_comp().map_with(spanned_extra),
966 (just(Token::Comma).ignore_then(cost_comp().map_with(spanned_extra)))
967 .repeated()
968 .collect::<Vec<_>>(),
969 ))
970 .or_not(), )
972 .then_ignore(just(Token::Rcurl))
973 .try_map(move |cost_spec, span| {
974 let mut builder = match cost_spec {
975 Some((head, tail)) => {
976 once(head).chain(tail).fold(
977 CostSpecBuilder::default(),
979 |builder, cost_comp| match cost_comp.item {
980 CompoundAmount(compound_amount) => match compound_amount {
981 BareCurrency(cur) => builder.currency(cur, cost_comp.span),
982 BareAmount(amount) => builder.compound_expr(amount, cost_comp.span),
983 CurrencyAmount(amount, cur) => builder
984 .compound_expr(amount, cost_comp.span)
985 .currency(cur, cost_comp.span),
986 },
987 Date(date) => builder.date(date, cost_comp.span),
988 Label(s) => builder.label(s, cost_comp.span),
989 Merge => builder.merge(cost_comp.span),
990 },
991 )
992 }
993 None => CostSpecBuilder::default(),
994 };
995 builder
996 .build()
997 .map_err(|e| Rich::custom(span, e.to_string()))
998 })
999}
1000
1001#[derive(PartialEq, Eq, Clone, Debug)]
1002enum CostComp<'a> {
1005 CompoundAmount(CompoundAmount<'a>),
1006 Date(Date),
1007 Label(&'a str),
1008 Merge,
1009}
1010
1011fn cost_comp<'s, I>() -> impl Parser<'s, I, CostComp<'s>, Extra<'s>>
1013where
1014 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1015{
1016 use CostComp::*;
1017
1018 choice((
1019 compound_amount().map(CompoundAmount),
1020 date().map(Date),
1021 string().map(Label),
1022 just(Token::Asterisk).to(Merge),
1023 ))
1024}
1025
1026pub(crate) fn tags_links<'s, I>()
1029-> impl Parser<'s, I, (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>), Extra<'s>>
1030where
1031 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1032{
1033 choice((
1034 tag().map_with(spanned_extra).map(Either::Left),
1035 link().map_with(spanned_extra).map(Either::Right),
1036 ))
1037 .repeated()
1038 .collect::<Vec<_>>()
1039 .validate(|tags_or_links, _span, emitter| {
1040 tags_or_links.into_iter().fold(
1041 (HashSet::new(), HashSet::new()),
1042 |(mut tags, mut links), item| match item {
1043 Either::Left(tag) => {
1044 if tags.contains(&tag) {
1045 emitter.emit(Rich::custom(
1046 tag.span.into(),
1047 format!("duplicate tag {}", tag),
1048 ))
1049 } else {
1050 tags.insert(tag);
1051 }
1052
1053 (tags, links)
1054 }
1055 Either::Right(link) => {
1056 if links.contains(&link) {
1057 emitter.emit(Rich::custom(
1058 link.span.into(),
1059 format!("duplicate link {}", link),
1060 ))
1061 } else {
1062 links.insert(link);
1063 }
1064
1065 (tags, links)
1066 }
1067 },
1068 )
1069 })
1070}
1071
1072pub(crate) fn bool<'s, I>() -> impl Parser<'s, I, bool, Extra<'s>>
1074where
1075 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1076{
1077 choice((just(Token::True).to(true), just(Token::False).to(false)))
1078}
1079
1080pub(crate) fn expr_value<'s, I>() -> impl Parser<'s, I, ExprValue, Extra<'s>>
1082where
1083 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1084{
1085 expr().map(ExprValue::from)
1086}
1087
1088pub(crate) fn expr<'s, I>() -> impl Parser<'s, I, Expr, Extra<'s>>
1090where
1091 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1092{
1093 use Token::*;
1094
1095 recursive(|expr| {
1096 let parens = expr
1098 .clone()
1099 .delimited_by(just(Lparen), just(Rparen))
1100 .map(|x| Expr::Paren(Box::new(x)));
1101
1102 let number = select_ref! { Number(x) => Expr::Value(*x) };
1104
1105 let factor = choice((just(Minus), just(Plus)))
1107 .or_not()
1108 .then(number.or(parens.clone()))
1109 .map(|(negated, x)| {
1110 if negated.is_some_and(|tok| tok == Minus) {
1111 Expr::Neg(Box::new(x))
1112 } else {
1113 x
1114 }
1115 });
1116
1117 let product = factor.clone().foldl(
1119 choice((
1120 just(Asterisk).to(Expr::Mul as fn(_, _) -> _),
1121 just(Slash).to(Expr::Div as fn(_, _) -> _),
1122 ))
1123 .then(factor.clone())
1124 .repeated(),
1125 |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1126 );
1127
1128 product.clone().foldl(
1130 choice((
1131 just(Plus).to(Expr::Add as fn(_, _) -> _),
1132 just(Minus).to(Expr::Sub as fn(_, _) -> _),
1133 ))
1134 .then(product.clone())
1135 .repeated(),
1136 |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1137 )
1138 })
1139}
1140
1141fn tag<'s, I>() -> impl Parser<'s, I, Tag<'s>, Extra<'s>>
1143where
1144 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1145{
1146 let tag = select_ref!(Token::Tag(s) => *s);
1147 tag.try_map(|s, span| Tag::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1148}
1149
1150fn link<'s, I>() -> impl Parser<'s, I, Link<'s>, Extra<'s>>
1152where
1153 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1154{
1155 let link = select_ref!(Token::Link(s) => *s);
1156 link.try_map(|s, span| Link::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1157}
1158
1159fn key<'s, I>() -> impl Parser<'s, I, Key<'s>, Extra<'s>>
1163where
1164 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1165{
1166 let key = select_ref!(Token::Key(s) => *s);
1167
1168 key.try_map(|s, span| Key::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1169}
1170
1171fn currency<'s, I>() -> impl Parser<'s, I, Currency<'s>, Extra<'s>>
1173where
1174 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1175{
1176 let currency = select_ref!(Token::Currency(s) => *s);
1177 currency.try_map(|s, span| Currency::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1178}
1179
1180fn date<'s, I>() -> impl Parser<'s, I, Date, Extra<'s>>
1182where
1183 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1184{
1185 select_ref!(Token::Date(date) => *date)
1186}
1187
1188fn decimal<'s, I>() -> impl Parser<'s, I, Decimal, Extra<'s>>
1190where
1191 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1192{
1193 select_ref!(Token::Number(x) => *x)
1194}
1195
1196fn string<'s, I>() -> impl Parser<'s, I, &'s str, Extra<'s>>
1198where
1199 I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1200{
1201 let string = select_ref!(Token::StringLiteral(s) => s.deref());
1202
1203 string.map_with(|s, e| {
1204 let span: Span_ = e.span();
1205 let simple_state: &mut extra::SimpleState<ParserState> = e.state();
1206 let parser_state: &mut ParserState = simple_state;
1207 let ParserState { warnings, options } = parser_state;
1208 let line_count = s.chars().filter(|c| *c == '\n').count() + 1;
1209 let long_string_maxlines = options.long_string_maxlines.as_ref().map(|n| *n.item()).unwrap_or(DEFAULT_LONG_STRING_MAXLINES);
1210 if line_count > long_string_maxlines {
1211 let option_span = options.long_string_maxlines.as_ref().map(|s| s.source.value);
1212 let is_default = option_span.is_none();
1213 let warning = Warning::new(
1214 "string too long",
1215 format!(
1216 "exceeds long_string_maxlines({}{}) - hint: would require option \"long_string_maxlines\" \"{}\"",
1217 if is_default { "default " } else { "" },
1218 long_string_maxlines,
1219 line_count
1220 ),
1221 span.into(),
1222 );
1223
1224 if let Some(option_span) = option_span {
1225 warnings.push(warning.related_to_named_span("max allowed", option_span));
1226 } else {
1227 warnings.push(warning)
1228 }
1229 }
1230 s
1231 })
1232}
1233
1234impl<'a> Metadata<'a> {
1235 pub(crate) fn merge_tags<E>(&mut self, tags: &HashSet<Spanned<Tag<'a>>>, emitter: &mut E)
1236 where
1237 E: Emit<ParserError<'a>>,
1238 {
1239 for tag in tags {
1240 match self.tags.get(tag) {
1241 None => {
1242 self.tags.insert(*tag);
1243 }
1244 Some(existing_tag) => {
1245 let error =
1246 Rich::custom(existing_tag.span.into(), format!("duplicate tag {}", tag));
1247 emitter.emit(error);
1256 }
1257 }
1258 }
1259 }
1260
1261 pub(crate) fn augment_tags(&mut self, tags: &HashMap<Spanned<Tag<'a>>, Vec<Spanned<Tag<'a>>>>) {
1264 for (tag, spans) in tags.iter() {
1265 if !self.tags.contains(tag) {
1266 let most_recently_pushed_tag = spans.last().unwrap_or(tag);
1267 self.tags.insert(*most_recently_pushed_tag);
1268 }
1269 }
1270 }
1271
1272 pub(crate) fn merge_links<E>(&mut self, links: &HashSet<Spanned<Link<'a>>>, emitter: &mut E)
1273 where
1274 E: Emit<ParserError<'a>>,
1275 {
1276 for link in links {
1277 match self.links.get(link) {
1278 None => {
1279 self.links.insert(*link);
1280 }
1281 Some(existing_link) => {
1282 let error = Rich::custom(
1283 existing_link.span.into(),
1284 format!("duplicate link {}", link),
1285 );
1286 emitter.emit(error);
1295 }
1296 }
1297 }
1298 }
1299
1300 pub(crate) fn augment_key_values(
1303 &mut self,
1304 key_values: &HashMap<Spanned<Key<'a>>, Vec<(Span, Spanned<MetaValue<'a>>)>>,
1305 ) {
1306 for (key, values) in key_values {
1307 if !self.key_values.contains_key(key) {
1308 let (key_span, value) = values.last().unwrap();
1309 self.key_values.insert(
1310 spanned(*key.item(), *key_span),
1311 value.clone(),
1315 );
1316 }
1317 }
1318 }
1319}
1320
1321type ParserError<'a> = Rich<'a, Token<'a>, Span_>;
1322
1323impl From<ParserError<'_>> for Error {
1324 fn from(error: ParserError) -> Self {
1325 let error = error.map_token(|tok| tok.to_string());
1326
1327 Error::new(
1328 error.to_string(),
1329 error.reason().to_string(),
1330 error.span().into(),
1331 )
1332 .in_explicitly_labelled_contexts(
1333 error
1334 .contexts()
1335 .map(|(label, span)| (label.to_string(), span.into())),
1336 )
1337 }
1338}
1339
1340#[derive(Default, Debug)]
1342pub(crate) struct ParserState<'a> {
1343 pub(crate) options: ParserOptions<'a>,
1344 pub(crate) warnings: Vec<Warning>,
1345}
1346
1347pub(crate) type Extra<'a> = extra::Full<ParserError<'a>, extra::SimpleState<ParserState<'a>>, ()>;
1349
1350pub(crate) trait Emit<E> {
1352 fn emit(&mut self, err: E);
1353}
1354
1355impl<E> Emit<E> for chumsky::input::Emitter<E> {
1356 fn emit(&mut self, err: E) {
1357 self.emit(err)
1358 }
1359}
1360
1361impl<E> Emit<E> for Vec<Error>
1363where
1364 E: Into<Error>,
1365{
1366 fn emit(&mut self, err: E) {
1367 self.push(err.into())
1368 }
1369}
1370struct NullEmitter;
1372
1373impl<E> Emit<E> for NullEmitter {
1374 fn emit(&mut self, _err: E) {}
1375}
1376
1377mod tests;