1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! The tag-internals grammar: what appears between `{%` and `%}`.
//!
//! Mirrors upstream `src/grammar/tag.pegjs` -- a 176-line PEG covering tag
//! names, attributes, values, variables, function calls, and annotations. Here
//! it becomes a hand-written recursive-descent parser over the same grammar.
//!
//! This is the crate's outermost attack surface: it is fed arbitrary text from
//! arbitrary documents. It must never panic, and a property test asserts that
//! against generated input rather than trusting review.
//!
//! # How the port is arranged
//!
//! | File | Upstream |
//! |---|---|
//! | `tag.rs` | `Top`, `Annotation`, `TagOpen`, `TagClose`, the attribute list |
//! | `value.rs` | `Value` and everything it reaches: literals, arrays, hashes, `Function`, `Variable` |
//! | `cursor.rs` | peggy's position, backtracking and expectation bookkeeping |
//! | `error.rs` | peggy's `SyntaxError` message algorithm |
//!
//! One function per production, named after it, in declaration order. Read a
//! rule beside the `.pegjs` and the two should say the same thing.
//!
//! # Fidelity notes
//!
//! Three behaviours are easy to mistake for bugs and are none of them. They
//! have tests, and the tests say why.
//!
//! - **The start rule must consume the whole body.** A PEG start rule that
//! matches and leaves text behind is an error, and no other alternative is
//! tried. `foo=1a` does not fall back to a tag named `foo`; it fails.
//! - **A falsy primary value is parsed and dropped.** `{% foo 0 %}` is a tag
//! with no attributes, because upstream unshifts the primary attribute under
//! a JavaScript truthiness test. See [`Value::is_truthy`].
//! - **A `$$mdtype` hash key is discarded.** It is upstream's runtime type tag,
//! and dropping the guard would let authored content forge one.
//!
//! [`Value::is_truthy`]: crate::ast::Value::is_truthy
pub use MAX_VALUE_DEPTH;
pub use TagError;
use crateValue;
use Cursor;
/// Upstream's `Top` production: the four things that can appear between `{%`
/// and `%}`.
///
/// Upstream returns a markdown-it token whose `type` and `nesting` encode the
/// same four cases, and this is the mapping the tokenizer above reverses:
///
/// | Here | `type` | `nesting` |
/// |---|---|---|
/// | [`TagItem::Variable`] | `variable` | 0 |
/// | [`TagItem::Annotation`] | `annotation` | 0 |
/// | [`TagItem::TagOpen`] with `self_closing: false` | `tag_open` | 1 |
/// | [`TagItem::TagOpen`] with `self_closing: true` | `tag` | 0 |
/// | [`TagItem::TagClose`] | `tag_close` | -1 |
///
/// The `self_closing` flag replaces upstream's `type`/`nesting` pair because
/// the pair is one decision spelled twice: `['tag', 0]` and `['tag_open', 1]`
/// differ in exactly the presence of the trailing `/`. Nesting is a property of
/// the token stream, not of the tag body, so it belongs to the layer that
/// builds the stream.
/// One entry of a tag's attribute list.
///
/// The `#id` shortcut is not a variant: upstream expands it to an ordinary
/// attribute named `id` with a string value, and a consumer that special-cased
/// it would be handling a syntax that no longer exists by this point. The
/// `.class` shortcut *is* a variant, because a node collects classes into a set
/// rather than overwriting one attribute.
/// Parses the internals of a tag: everything between `{%` and `%}`.
///
/// Pass the body with the delimiters removed and both ends trimmed, which is
/// what upstream's tokenizer passes (`content.trim()` in
/// `src/tokenizer/plugins/annotations.ts`). The grammar has no leading- or
/// trailing-whitespace rule of its own beyond an annotation's trailing `_*`, so
/// untrimmed input is a syntax error rather than a lenient parse. Trimming here
/// instead would be a divergence, and a silent one.
///
/// ```
/// use accent_proust::ast::Value;
/// use accent_proust::grammar::{parse_tag, Attribute, TagItem};
///
/// let item = parse_tag(r#"callout type="note" /"#)?;
/// assert_eq!(
/// item,
/// TagItem::TagOpen {
/// name: "callout".to_string(),
/// attributes: vec![Attribute::Attribute {
/// name: "type".to_string(),
/// value: Value::String("note".to_string()),
/// }],
/// self_closing: true,
/// }
/// );
/// # Ok::<(), accent_proust::grammar::TagError>(())
/// ```
///
/// # Errors
///
/// Returns a [`TagError`] when the body is not a well-formed tag, and also when
/// it *is* one followed by anything else: the start rule has to consume the
/// whole body, so `foo=1 bar` fails rather than parsing the part it
/// understands. The message is upstream's message for the same input, and the
/// offsets are byte offsets into `input`.