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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! Printing an AST back to canonical Markdoc source.
//!
//! Mirrors upstream `src/formatter.ts`, the largest single file in the port at
//! 506 lines, and the hardest: it is the only layer whose correctness is
//! judged against the exact bytes it emits.
//!
//! It is also what makes tooling possible -- editing a document as a tree and
//! writing it back, mechanically migrating one syntax to another, showing an
//! author the canonical form of what they wrote.
//!
//! Two properties gate it, beyond the corpus:
//!
//! - `format(parse(s))` is idempotent.
//! - `parse(format(ast))` round-trips the AST.
//!
//! # Why the output is a list of chunks rather than a string
//!
//! Upstream is written as nested generators, and one case reads the *shape* of
//! what its children yielded rather than their concatenation. A `tr` yields a
//! JavaScript array of formatted cells; a `table` inside a `{% table %}` tag
//! walks the yielded items and prints an array as `- cell` lines and a string
//! as a line of its own. Concatenating as you go destroys that distinction, and
//! it also destroys the *boundaries* between strings, which the same loop reads:
//! each yielded string becomes its own line, so merging two of them merges two
//! lines.
//!
//! So the walk keeps the yields as a `Vec` of chunks, one per `yield`, and
//! joins only at the end. That is the generator stream, made data.
//!
//! # Where upstream has no defined behaviour
//!
//! A few branches upstream reach only with a hand-built tree: an attribute that
//! is a plain object where a scalar is expected, a `title` that is not a
//! string, a `text` node whose `content` is neither a string nor an AST value.
//! JavaScript answers those with a `TypeError`, an unhandled `Error`, or
//! `"[object Object]"`. None of the three is a specification, and this crate
//! promises not to panic, so each prints the value in its Markdoc literal
//! spelling instead -- the spelling that re-parses. Nothing a parsed document
//! contains reaches any of them.
//!
//! # Bounds
//!
//! The walk is recursive, over a tree whose depth is attacker-controlled, so it
//! is bounded: see [`MAX_FORMAT_DEPTH`] and `DIVERGENCES.md` entry 15.
use crate;
use crate;
/// A single space. Upstream's `SPACE`.
const SPACE: &str = " ";
/// The value separator inside arrays, hashes and parameter lists.
const SEP: &str = ", ";
/// A newline. Upstream's `NL`.
const NL: &str = "\n";
/// The default ordered-list marker. Upstream's `OL`.
const OL: &str = ".";
/// The default unordered-list marker. Upstream's `UL`.
const UL: &str = "-";
/// The node types whose text children escape `*`, `_` and `~`.
///
/// Upstream's `WRAPPING_TYPES`. Inside one of these, an unescaped marker
/// character would close the wrapper early.
const WRAPPING_TYPES: = ;
/// The width past which a block tag's opening is broken across lines.
///
/// Upstream's `MAX_TAG_OPENING_WIDTH`. Measured in UTF-16 code units, because
/// that is what `String.prototype.length` counts and the threshold is a
/// formatting decision upstream already made.
pub const MAX_TAG_OPENING_WIDTH: usize = 80;
/// How deep the formatter will walk before it stops.
///
/// The same argument as `grammar::MAX_VALUE_DEPTH` and the transform stage's
/// `MAX_TRANSFORM_DEPTH`, one layer further up: nesting depth is
/// attacker-controlled, and in Rust unbounded recursion over it is a stack
/// overflow, which aborts the process and cannot be caught. A node below this
/// depth formats to nothing; its ancestors print normally. See `DIVERGENCES.md`
/// entry 15.
///
/// # Why 128 and not the transform stage's 512
///
/// Because the number has to be measured, not chosen. This walk carries a
/// fatter frame than the transform's: printing a tag builds several strings
/// before it recurses. Measured on a 2 MiB thread stack in a debug build --
/// which is what `cargo test` gives every test -- the walk overflows a little
/// past 700 levels of nested tags. A bound of 512 would sit inside that by less
/// than half, which is not a margin for a published promise; 128 sits inside it
/// by a factor of five, in the least favourable configuration this crate is
/// built in.
///
/// The cost is nothing a document pays. 128 is around forty levels of authored
/// nesting, because a paragraph of text is already four, and no document a
/// person writes is close.
pub const MAX_FORMAT_DEPTH: usize = 128;
/// The largest number of `#` characters a heading prints.
///
/// `level` is an ordinary attribute, so a host can set it to anything a
/// [`f64`](f64) holds, and `"#".repeat(n)` for a large `n` is an allocation
/// failure rather than a formatting bug. The bound is far above CommonMark's
/// six levels, so it never changes the output of a parsed document. See
/// `DIVERGENCES.md` entry 15.
const MAX_HEADING_LEVEL: usize = 1024;
/// Whether a numbered list reprints its numbers or repeats the first one.
///
/// Upstream's `orderedListMode`. The default is [`OrderedListMode::Repeat`],
/// which writes `1.` for every item after the first and lets the Markdown
/// renderer number them -- the form that survives inserting an item in the
/// middle.
/// How to print a tree.
///
/// Upstream's `Options`, minus two fields. `parent` and `indent` are internal
/// bookkeeping rather than caller-facing choices, so they live in the walk;
/// `allowIndentation` does not exist here at all, because the tokenizer option
/// it mirrors does not (`DIVERGENCES.md` entry 8).
/// Print a tree as canonical Markdoc source, with upstream's default options.
///
/// Canonical means the spacing inside a tag is normalised while the author's
/// own spellings are not: an annotation reprints as it was written, and
/// `__bold__` does not become `**bold**`.
///
/// ```
/// # #[cfg(feature = "pulldown-cmark-tokenizer")]
/// # {
/// let document = accent_proust::parse::parse("{% callout type=\"note\" %}\nBody\n{% /callout %}\n");
/// assert_eq!(
/// accent_proust::format::format(&document),
/// "{% callout type=\"note\" %}\nBody\n{% /callout %}\n"
/// );
/// # }
/// ```
/// Print a tree as canonical Markdoc source.
///
/// ```
/// use accent_proust::format::{format_with, FormatOptions, OrderedListMode};
///
/// # #[cfg(feature = "pulldown-cmark-tokenizer")]
/// # {
/// let document = accent_proust::parse::parse("1. one\n1. two\n1. three\n");
/// let options = FormatOptions::new().ordered_list_mode(OrderedListMode::Increment);
/// assert_eq!(format_with(&document, &options), "1. one\n2. two\n3. three\n");
/// # }
/// ```
/// Print a value -- a variable, a function call, a literal -- as it would be
/// written inside a tag.
///
/// Upstream's `format` takes `Value | Value[]`, because a `Node` is one of its
/// values. Here it is not, so the two entry points are separate: this one is
/// what upstream's `format(null)` and `format($x)` reach.
///
/// ```
/// use accent_proust::ast::{PathSegment, Value, Variable};
///
/// let value = Value::Variable(Variable::new(vec![
/// PathSegment::Key("user".into()),
/// PathSegment::Key("name".into()),
/// ]));
/// assert_eq!(accent_proust::format::format_value(&value), "$user.name");
/// assert_eq!(accent_proust::format::format_value(&Value::Null), "");
/// ```
/// Print a value as it would be written inside a tag.
///
/// See [`format_value`]. The options reach nothing a bare value can contain and
/// are taken for symmetry, so a caller threading one set of options does not
/// have to know which entry point ignores them.
/// `String.prototype.length`: UTF-16 code units.
///
/// Every width the formatter measures is a JavaScript string length -- the
/// tag-opening threshold and the table column widths -- and counting bytes or
/// `char`s instead would move a line break for a document with an astral
/// character in it.
/// `String::trim_start` without a second allocation.
/// One `yield` of upstream's generator.
///
/// [`Chunk::Row`] is the array a `tr` yields. Keeping it apart from text is
/// what lets the `{% table %}` branch tell a row from a tag written between
/// rows, which is the one place in the file where the difference is read.
/// The chunks yielded so far.
/// What a node needs to know about where it sits.
///
/// Upstream carries the parent node itself in its options and reads exactly two
/// facts off it: whether it wraps its text (`strong`, `em`, `s`), and whether it
/// is the `{% table %}` tag. Carrying the two answers instead of the node keeps
/// the walk free of the AST's lifetime, which is otherwise threaded through
/// every helper for no gain.
/// The walk.
///
/// `depth` counts nodes, `stack` counts nested values; both are bounded at
/// [`MAX_FORMAT_DEPTH`] because both recurse over structure a document
/// controls.