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
// CSS whitespace classification, shared by the value parser (separation) and the
// printer (value-text normalization) so both agree on what counts as whitespace.
/// CSS whitespace is ASCII-only — tab, line feed, form feed, carriage return, and space.
///
/// ⚠️ **Five, where css-syntax-3 §4.2 defines three.** §4.2 is *newline* (which it fixes at
/// U+000A alone), tab and space; carriage return and form feed are absent from it **because
/// §3.3's input preprocessing has already folded them to U+000A** by the time tokenization
/// runs. tsv does not run that preprocessing — `parse` takes the author's bytes so its offsets
/// stay a drop-in contract with `parseCss` — which puts the two folded code points back on this
/// predicate's plate. Reading §4.2 on its own and narrowing this to `[' ', '\t', '\n']` is
/// therefore a plausible-looking change that stops every value scanner on a `<CR>` or a form
/// feed. The rule is §4.2 **plus** §3.3, and `is_ascii_whitespace` is exactly that union.
///
/// ⚠️ **Not the class the lexer skips at a token boundary**, which is JS `\s`
/// ([`tsv_lang::is_js_whitespace`]) because that is what `parseCss`'s `allow_whitespace()`
/// is. The two answer different questions and differ at every code point at or above
/// U+00A0: this one is about value *separation* and value-text *collapsing*, where a
/// `<NBSP>` is content that must not be rewritten to a space; that one is about whether a
/// token even starts here. Both are right, and swapping either for the other is a bug.
///
/// This is deliberately **not** `char::is_whitespace`,
/// which follows the Unicode `White_Space` property and would treat NBSP
/// (U+00A0), NEL, the ideographic space, etc. as separators (it also includes
/// U+000B VT, which CSS does not). Those code points are ordinary value content —
/// both prettier and Svelte's `parseCss` keep them inside their token — so all of
/// CSS value *separation* (`ValueCursor` here, and the byte-scanning
/// `classify_separators` via the equivalent `u8::is_ascii_whitespace`) and
/// value-text whitespace *collapsing* (the printer's `normalize_css_whitespace`)
/// act only on ASCII whitespace; otherwise a non-ASCII-whitespace code point would
/// be silently rewritten to a space.
pub
/// Whether `c` is whitespace to `parseCss`'s `allow_whitespace()` **and** identifier content
/// to its `read_identifier` — the ambiguous class, and the exact run a token boundary skips
/// and the printer puts back.
///
/// The two sides are one fact asked from opposite directions: the parser decides what the AST
/// does **not** contain (`CssParser::skip_boundary_whitespace`, and its comment-looping twin
/// `skip_boundary_whitespace_registering_comments`), and the printer decides what the output
/// must still carry. Spelling it twice is a drift hazard with two silent failure modes — a
/// printer that restores less than the parser skipped deletes source, and one that restores
/// more duplicates it — so every one of them asks *this* rather than re-deriving a class.
///
/// ⚠️ **A skip and a preservation are one change.** Adding a juncture to the parser without
/// the matching restore turns a graceful over-rejection into content loss, which is the worse
/// trade; both failure modes above have been live. The printer has two shapes of restore
/// because the junctures have two shapes of anchor, and which one applies is a property of
/// what follows the run:
///
/// - `Printer::preserved_boundary_ws` — a BACKWARD scan from the node that follows the run,
/// for a gap that ends at a name (a compound, an explicit combinator). It takes a FLOOR,
/// because a run glued to the previous identifier is already inside that name's span and an
/// unbounded scan emits it a second time.
/// - `Printer::spell_gap` — a FORWARD collection over a gap that ends at a
/// STRUCTURAL token instead (`,`, `{`, `)`, `]`, an attribute value), which has no node to
/// anchor on and may hold comments the printer emits separately.
///
/// Both live in `printer/boundary_ws.rs`, with the whole model — the partition between them,
/// where a claim is emitted, and the residue — in that module's own doc, for the same reason
/// this class is named once.
///
/// It is JS `\s` minus its ASCII members: an ASCII space really is a separator on both sides
/// of `parseCss`, so it is the printer's to regenerate as indentation, not to preserve
/// verbatim.
pub
/// The whole run `CssParser::skip_boundary_whitespace` steps: the lexer's own whitespace
/// (`is_ascii_css_whitespace` plus the sub-U+00A0 non-ASCII whitespace its dispatch admits)
/// **and** the members hiding inside an identifier token.
///
/// Named once because the printer scans that run **backwards** out of the source, where the
/// parser met it as a token stream, and the two must agree on where it starts — a printer
/// that stops early deletes the members behind its stopping point. Both terms are load-
/// bearing at that seam: `<NBSP><VT>div` needs the ASCII half to reach the `<NBSP>`, and
/// `<NBSP><NEL>div` needs the `White_Space` half.
///
/// ⚠️ Neither term alone is this set, and neither is [`is_css_whitespace`] — that one is the
/// css-syntax-3 *tokenization* class (ASCII, no `<VT>`), the right answer for value
/// separation and the wrong one here. This is JS `\s` ∪ Unicode `White_Space`, which is JS
/// `\s` plus `<NEL>` (U+0085): the lexer reads a `<NEL>` as whitespace where `parseCss`
/// rejects it, so the run the parser really steps includes one, and the printer must scan
/// past it to reach anything behind it. That over-acceptance is the tracked `<NEL>` gap — see
/// [`tests/css_boundary_whitespace.rs`](../../../tests/css_boundary_whitespace.rs) — and
/// mirroring it here is what keeps the printer preserving exactly what the parser skipped,
/// gap included, rather than inventing a second answer.
pub
/// The ASCII members of [`is_boundary_whitespace`] — the six code points below U+0080 that
/// JS `\s` and `White_Space` share: `<TAB>`, `<LF>`, `<VT>`, `<FF>`, `<CR>` and space.
///
/// The half of a boundary run the printer REGENERATES (as indentation, or as the one space
/// it puts around an operator) rather than preserves — so a trim over text that may hold both
/// halves asks this and leaves the non-ASCII members standing, where `str::trim` (Unicode
/// `White_Space`) would take the `<NBSP>` with them and `str::trim_ascii` would stop on the
/// `<VT>`. The byte spelling is `Printer::boundary_run`'s inner loop
/// (`u8::is_ascii_whitespace() || b == 0x0b`); this is the char one.
pub const
/// Could the char that begins or ends at this byte be [`is_boundary_whitespace`]? `true` for
/// the six ASCII members — the two classes agree there: `<TAB>`, `<LF>`, `<VT>`, `<FF>`,
/// `<CR>` and space — and for every non-ASCII byte, which may belong to a multi-byte member.
/// `false` is a proof and `true` is a question for the char predicate: an ASCII byte outside
/// the six is a whole char of its own, and not one either class holds.
///
/// The gate ahead of a trim over that class. A property name settles both of its ends here in
/// four compares, where the char-predicate searchers each decode a char from one end to find
/// nothing to trim — and a searcher's construction, not its walk, was the declaration
/// printer's largest single cost.
pub const
/// Byte length of the [`is_boundary_only_whitespace`] run at the head of `text`, or `0`.
///
/// The one measurement of "how much of this identifier token is really the run
/// `allow_whitespace()` would have stepped", asked by the parser three ways: to decide
/// whether a run stands here (`CssParser::boundary_run_len`), to step past one
/// (`CssParser::skip_boundary_whitespace`'s lookaheads), and to tell a glued compound
/// continuation from a boundary (`compound_continues_across_comments`). They are one
/// question, so they are one function — a `starts_with` spelling beside a `take_while` one
/// is how two of the three come to disagree about a mixed run.
pub
/// Byte offset of the first [`is_boundary_only_whitespace`] member INSIDE `text`, past its
/// head, or `None`.
///
/// The twin of [`boundary_prefix_len`] for a reader whose own class is JS `\s` where the
/// lexer's is "everything at or above U+00A0": `read_attribute_value` ends a bare value at the
/// first `\s` (`[a=b<NBSP>]` is the value `b`, `[a=b<NBSP>i]` the value `b` and the flag `i`),
/// where the lexer handed `parse_attribute_value` one identifier token — this is where that
/// token really ends. The case flag beside it is a narrower reader still
/// (`REGEX_ATTRIBUTE_FLAGS` reads letters only, so `[a=b i<NBSP>]` is the flag `i`) and takes
/// its own ASCII-letter prefix rather than asking this.
///
/// A `\` escapes the code point after it on both sides — `read_attribute_value` appends `\`
/// plus the char, the lexer reads the escape — so an escaped member is content and is stepped
/// over (`[a=b\<NBSP>]` is the value `b\<NBSP>` to both).
pub
/// Whether `[from, to)` of `source` holds a boundary MEMBER outside its comments — the test
/// that hands a gap to the speller. A non-ASCII character inside a comment (`/* é */`) is not
/// a member, and neither is a `U+FEFF` at offset 0, the byte-order mark tsv strips by policy.
///
/// The one statement of the question for the printer's claims (`Printer::gap_holds_member`,
/// which asks it behind the document precondition) and the parser's prelude readers, which
/// spell a gap into text a node carries.
pub
// ## Spelling a boundary gap
//
// The one spelling every boundary claim emits, and its edge vocabulary. Named here rather than
// in the printer because the parser spells three kinds of gap into text a node carries — a
// condition part's `not` gap and an operator run's gaps (`parser/atrules/preludes.rs`), and a
// verbatim prelude's two ends (`parser/atrules/raw.rs`, `PreludeEnds`) — and a second speller
// is the drift this module exists to prevent; see `printer/boundary_ws.rs` §How a claim is
// spelled for the model.
/// How a spelled boundary gap meets the output on one side of it.
///
/// The gap's own interior is not a choice: its items (members, and comments where the claim
/// prints them) stay in source order, and each ASCII whitespace stretch between two is one
/// space, whoever claims it. What differs per claim is only what stands BESIDE the gap,
/// which the claim knows and the speller does not.
pub
/// What [`spell_run`] does with a comment in the gap it spells.
pub
/// The one spelling of a boundary gap, which every claim emits (the printer's, in
/// `printer/boundary_ws.rs`, and the parser's three that spell a gap into a node's text — see
/// [`Comments::Carried`]): its ITEMS —
/// the members and, where the claim owns them ([`Comments::InPlace`]), the comments — in
/// source order, every ASCII whitespace stretch between two items as ONE space and its
/// absence as none, and the two edges as `lead` and `trail` say ([`Edge`]).
///
/// Two readers make the rule, and they agree on it. `parseCss` skips the whole run, so
/// nothing about the AST depends on the spelling. css-syntax-3, whose whitespace is ASCII
/// only, reads each member as identifier content, each ASCII stretch as one
/// `<whitespace-token>`, and a comment as nothing at all, so the spelling IS the
/// tokenization: `<NBSP><NBSP>` is one identifier where `<NBSP> <NBSP>` is two, a stretch
/// welded to nothing merges two tokens, a comment glued to a member keeps it glued to what
/// is on the comment's other side (`<NBSP>/* c */:hover` is the compound `<NBSP>:hover`),
/// and a stretch respelled as a tab or a line break is the same token — but a line break
/// carried inside the text the printer emits is one the doc renderer neither indents after
/// nor counts. So the items stay where the author put them and each stretch between them is
/// one space: the spelling that keeps every token the author wrote and nothing else, and
/// prettier's too, at every juncture where prettier keeps the run.
///
/// Scans `[from, to)` of `source`. Any other character in the gap (the `(` a caller's range
/// opens on) is neither an item nor a separation. `bom_at_zero` excludes a `U+FEFF` at
/// offset 0: the byte-order mark of a whole document, which tsv strips by policy (see
/// `Printer::boundary_run`); it is neither a member nor a separation. An in-place comment
/// is reported to the print-once ledger here, since this is the seam that prints it.
pub
/// Append one item of a spelled gap: behind one space when `separated` says the author put
/// ASCII whitespace ahead of it — or, for the gap's first item, as `lead` says.
///
/// [`Edge::Space`] is a claim about a MEMBER, which a name ahead of it would take in; a
/// comment cannot be taken in (the name's token ends at its `/*`, and a comment tokenizes to
/// nothing), so a gap that opens on a comment keeps the author's separation there instead.