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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
//! Prose block elements — the built-in screenplay preset's *grammar*
//! (`docs/prose-dialect-spec.md` §8b/§8d, RULED 2026-07-25 across sittings
//! 4–5).
//!
//! Four line shapes plus one body shape:
//!
//! ```text
//! INT. MARKET SQUARE - NIGHT [market] #tense #act1 scene heading (§8b.2/.3)
//! The square is empty. ↑ header-scoped body
//!
//! @VENDOR #(v.o.) block cue (§8d.4)
//! (hushed) parenthetical
//! You shouldn't be here after dark. dialogue (chain rule)
//!
//! @KID: Says who? compact cue (§8b.9)
//! ```
//!
//! # What this module is *not*
//!
//! Classification only — nothing here lowers. Element **roles**
//! (attached-forward vs content vs structural), compile-time attachment
//! and the preset's data payloads are issues #1717/#1720. (The conventions
//! schema's `lower:` column this doc used to name is **dissolved** —
//! `docs/decision-log.md` 2026-07-31, "Conventions are annotated
//! handlers": there is a handler or there isn't.)
//!
//! Downstream, `hir::lower_native::element` (issue #1838) now claims a
//! heading whose text a natural-notation `@[element(claims = "…")]`
//! handler matches, rewriting it to one call. Every *other* shape here —
//! and any heading nothing claims — still meets `hir::lower_native`'s
//! loud-`E129` default arm ("parses cleanly but has no HIR lowering yet in
//! this slice"), which is the deliberate staging, not a silent drop.
//!
//! # Two rulings this module implements literally
//!
//! **Header-scoped stitch bodies (§8b.2).** A scene heading's body runs to
//! the next heading or the enclosing close — which *amends charter §4's
//! "braces are the universal body delimiter"* for preset heading-elements
//! in prose-ground only. Consequences were embraced, not overlooked:
//! heading-stitches are **flat siblings** (scenes don't nest, as on a real
//! page), and deeper nesting keeps the general `flow x { … }` spelling,
//! which stays first-class. This restores ink's own header-scoped stitch;
//! it is not an invention.
//!
//! **The lyrics element is dropped (§8b.1)** — Fountain's `~` force-marker
//! collides with the logic-line escape, and the conflict dies with the
//! element. There is deliberately no `LYRICS` shape here.
use crate;
use Parser;
// ── Scene headings & header-scoped stitches (§8b.2/.3) ───────────────
/// The declared heading pattern: an `INT.`/`EXT.` prefix at the very start
/// of a body item (§8b.3; the inventory's "`INT.`/`EXT.` prefix pattern").
/// `INT./EXT.` — the combined prefix — starts with the same two tokens, so
/// it is covered by the same guard.
///
/// The prefix is matched by **text**, not by a reserved keyword: `INT` and
/// `EXT` stay ordinary identifiers everywhere else in the language (a
/// `var INT = 1` binding is untouched). Only the exact upper-case spelling
/// followed by a `.` at item position claims a line, which is the
/// explicit-format posture — the preset never guesses from ALL-CAPS shape.
pub
/// [`at_scene_heading`], checked at non-trivia lookahead offset `n` instead
/// of the current position — the shared core [`at_scene_heading_past_leading_doc`]
/// reuses after skipping past a `///` run.
/// True when a scene heading is at the current position, **or** follows a
/// leading `///` doc-comment run (review finding on #1715): `scene_stitch`'s
/// body loop calls this, not the bare [`at_scene_heading`], as its
/// terminator check.
///
/// Without this, a documented second heading wasn't recognized as ending
/// the current stitch's body: `DOC_COMMENT_OUTER` is not trivia
/// (`SyntaxKind::is_trivia` = `WHITESPACE`/`LINE_COMMENT`/`BLOCK_COMMENT`
/// only), so [`at_scene_heading`]'s `p.at(IDENT)` check failed on the `///`
/// token, the loop did not break, and `block::item` went on to consume the
/// doc run and recurse into a *nested* `scene_stitch` — silently violating
/// §8b.2's "heading-stitches are flat siblings, scenes do not nest" for any
/// heading past the first that carries a doc comment.
///
/// Mirrors `doc_comment::consume_doc_run`'s "one or more `///` lines, each
/// terminated by a `NEWLINE`" shape closely enough for a lookahead (not a
/// consumer): it does not special-case a blank line ending the run early,
/// because over-breaking on that rare shape is harmless here — the loop
/// would stop one iteration sooner than strictly necessary, and the
/// dispatcher it hands off to (`block::item`) still resolves the
/// doc-attachment question exactly the same way it always does.
/// `INT. TITLE [slug] #tags` followed by every item up to the next heading
/// / the enclosing `}` / EOF — one `SCENE_STITCH` wrapping a
/// `SCENE_HEADING` and its braceless `SCENE_BODY` (§8b.2).
///
/// `doc` is a leading `///` run already consumed by `block::item`, threaded
/// through so it attaches as this node's leading `DOC_COMMENT` child, the
/// same way every declaration header does (B0.6b) — a heading declares a
/// stitch (§3.2's structural exception), so it documents like one.
///
/// No `enter_depth` guard here, unlike `braced_item_list`: this rule is not
/// self-recursive. The body loop *stops* at the next heading rather than
/// recursing into it (that is exactly what "flat siblings" means), so the
/// only way to nest deeper is through a `flow x { … }`, whose own
/// `braced_item_list` carries the depth guard.
pub
/// One heading line. Line order is fixed by §8b.3 — **pattern, `[slug]`,
/// tags** — and the two rejected slug spellings stay rejected: `#x#`
/// (clashes with the tag lexer) and `{x}` (lexes as interpolation;
/// headings get no carve-out, so a `{` on a heading line is just title
/// text as far as this rule is concerned).
/// Everything before the optional `[slug]`/tags: the display name (§3.3).
/// Raw-bumped like `content::text_run_until`, so interior spacing and any
/// run of `.`/`-`/digits in a slugline survives verbatim in one `TEXT`-
/// shaped node.
///
/// **`\#` escapes the title-boundary role of `#` (issue #1738), mirroring
/// `content::tag()`'s and `cue_name()`'s identical fix.** `#` is one of the
/// four members of the ruled, final inline escape set (§8d.6), and before
/// this fix `scene_title` gave it zero escape treatment — an unconditional
/// `HASH` stop with no backslash awareness, the exact pre-fix shape
/// `tag()`/`cue_name()` had. Same `backslash_count`-parity carve-out, same
/// "backslash not stripped from the literal text" precedent: this scan
/// already tests `nth_raw(0)` directly (no `cur`/`raw` adjacency hazard
/// like `tag()`'s), so the parity check is safe to apply unconditionally.
/// Pinned by `a_scene_title_with_an_escaped_hash_does_not_end_the_title_early`.
///
/// **Superseded in part by issue #2045:** the raw CST node built here is
/// still an unstripped, lossless copy of the source — that part of the
/// precedent holds. But `ast::SceneTitle::text()` (`ast/nodes.rs`) is a
/// *later* materialization point that now strips a recognized escape's
/// backslash from the title's rendered display text, in parity with
/// `markup::escape`; `try_claim`/`try_dispatch`'s natural-notation matching
/// deliberately keeps reading the raw, unstripped `SyntaxNode` text instead
/// (its byte offsets are load-bearing for capture-group provenance, #1838)
/// — so "not stripped" is still true of the CST node and of that one
/// pattern-matching reader, but no longer true of every reader.
/// True at a trailing `[ident]` slug — recognized **only** at the tail of
/// the heading (nothing but tags or the line end may follow), so a `[` in
/// the middle of a title stays title text rather than silently claiming
/// part of the display name.
// ── Cues: the block form and the compact form (§8b.9, §8d.4) ─────────
/// `@NAME` at item position. The `@` and the name must be **adjacent** —
/// `@ home tomorrow` keeps `SyntaxKind::AT`'s documented promise that "a
/// lone `@` in prose stays plain text", and `@[…]` is a different token
/// (`AT_L_BRACKET`) entirely, so the annotation channel cannot collide.
pub
/// Parses **both** ruled cue patterns off one prefix, deciding between
/// them at the `:` (§8b.9 — the compact cue is "a second declared pattern
/// beside the block cue", not a rewrite of it, so each gets its own node
/// kind):
///
/// ```text
/// @VENDOR #(v.o.) → CUE (+ tags: the ruled home for
/// (hushed) cue extensions, §8d.4)
/// @KID: Says who? → COMPACT_CUE (+ the fused CONTENT_LINE)
/// ```
pub
/// Spec'd in `docs/prose-dialect-spec.md` §4.7a.
///
/// The name run after the `@` sigil. Raw-bumped up to `:`/tags/line end,
/// so a multi-word character name (`@MARKET VENDOR`) is one name rather
/// than a name plus stray text.
///
/// #1786: confirmed to share `content::tag()`'s pre-#1728 shape — an
/// unconditional stop at the first raw `R_BRACE` mistook a `}` that merely
/// *echoed* a `{` already inside the name (e.g. `@NAME {gold} coins.`
/// inside a `flow f() { … }` body) for the enclosing block's own closer,
/// ending the name — and the flow's `BLOCK` — early. Fixed the same way
/// `tag()` was: `depth` counts literal, unpaired `{`s bumped so far, and a
/// `}` only stops the scan once depth is back to zero. An `L_BRACE` is
/// excluded from the counter when it is preceded by an *odd* number of
/// consecutive raw `BACKSLASH`es (#1852) — `\{` is the literal-brace escape
/// (#1716/PR #1732), but `\\{` is an escaped backslash followed by a real,
/// depth-counted brace, so counting consecutive backslashes (not just the
/// immediately preceding token) is required to tell the two apart.
///
/// Same tradeoff as `tag()`, stated the same way: a *balanced* brace in a
/// cue name no longer terminates it early (the bug this fixes), but a
/// genuinely *unbalanced*, unescaped `{` left open in a name now eats the
/// enclosing single-line block's own same-line `}` closer instead of
/// stopping there — inherent to depth-based balancing over raw text with
/// no real grammar to bound it. Pinned by
/// `an_unbalanced_open_brace_in_a_cue_name_eats_the_enclosing_blocks_own_closer`.
///
/// This is not full parity with `tag()`, though: `cue_name`'s stop set has
/// one member `tag()`'s does not, `HASH`, checked *before* the depth guard,
/// exactly like `NEWLINE`/`EOF` — so a `#` still cuts a name short even
/// while a brace is open. `COLON`, unlike `HASH`, is now depth-guarded the
/// same way `R_BRACE` is (#1851): a colon inside an unclosed `{` is part of
/// an interpolation, not the cue's terminator, so `@NAME {a:b} c.` no
/// longer stops at the `:` — it scans through to the balanced `}` like
/// `tag()` does.
///
/// **CONFIRMED (issue #1883, item 1): existing #1787 reasoning still
/// applies — `HASH` staying a hard, depth-blind reset is intentional, not
/// a residual gap to close.** `COLON`/`R_BRACE` are raw, ungrammared
/// punctuation this scan locally balances as "just text within this scan";
/// `HASH` is never that — an unescaped `HASH` always begins its own `TAG`
/// node, a real, tokenized CST boundary (the exact reasoning §4.7's
/// per-tag-scope ruling already states for why a fresh `HASH` must reset
/// `depth` to zero between sibling tags applies just as directly here).
/// Gating `HASH` by `depth == 0` the way `COLON` is would let an unescaped
/// `#` merge into the name's own text whenever a brace happens to be
/// open — turning an always-starts-a-new-`TAG` token into
/// sometimes-just-name-text depending on unrelated brace balance, which
/// would blur that same absolute boundary from the other direction. So
/// `@NAME {a#b} c.` still fails to parse: the name ends at `a`, `#b`
/// becomes a sibling `TAG`, and the still-open `{`'s matching `}` becomes
/// a stray top-level token once the name's own scan is long over. Pinned
/// by `a_hash_inside_an_open_brace_still_ends_a_cue_name_early`. See
/// `docs/prose-dialect-spec.md` §4.7b for the durable spec-level home.
///
/// **CONFIRMED (issue #1883, item 2): `\}`'s unconditional significance to
/// the depth check (mirroring `tag()`, above) is intentional, not a
/// residual asymmetry to close.** `\{`'s backslash-parity carve-out exists
/// because `\{` is one of the ruled, final four-character inline escape
/// set (§8d.6: `\< \{ \# \\`) — #1716/PR #1732 ruled it the literal-brace
/// escape. `}` is not a member of that set, so there is no equivalent
/// "`\}` is a literal, non-metacharacter close-brace" ruling to protect —
/// an `R_BRACE` preceded by a `BACKSLASH` is exactly what it looks like,
/// an ordinary backslash followed by an ordinary, structurally
/// significant `}`, so it keeps ending the name exactly like an unescaped
/// `}` would, at depth zero. Pinned by
/// `a_cue_names_own_unescaped_closing_brace_remains_the_terminator_even_when_preceded_by_a_backslash`.
/// See `docs/prose-dialect-spec.md` §4.7b for the durable spec-level home.
///
/// **`\#` escapes the name-boundary role of `#` (issue #1738), mirroring
/// `tag()`'s identical fix** — an unescaped `HASH` still cuts the name short
/// (the paragraph above, and #1883 — resolved, see §4.7b — are both about
/// *that* case and are unchanged by this), but `#` is one of the four
/// members of the ruled, final inline escape set (§8d.6), and `cue_name()`
/// gave it zero escape treatment before this fix: a `\#` inside a name
/// still ended it at the `#`, same defect `tag()` had. Same
/// `backslash_count`-parity carve-out,
/// same "backslash not stripped from the literal text" precedent as `\{`
/// just above. Pinned by
/// `a_cue_name_with_an_escaped_hash_does_not_end_the_name_early`.
///
/// **Superseded in part by issue #2045:** the raw CST node built here is
/// still an unstripped, lossless copy of the source — that part of the
/// precedent holds. But `ast::CueName::text()` (`ast/nodes.rs`) is a
/// *later* materialization point that now strips a recognized escape's
/// backslash the same way `ast::Tag::text()` does, so "not stripped" is
/// still true of the CST node but no longer true of every reader.
// ── `!name` sigil dispatch (§3.5b, issue #2004) ──────────────────────
/// `!name` at item position — the self-announcing annotation-element
/// dispatch sigil. The `!` and the name must be **adjacent**, the same
/// adjacency discipline [`at_cue`] applies to `@NAME`: a bare `!` followed
/// by a gap (`! Wait, listen.`) is ordinary prose punctuation, never a
/// malformed dispatch attempt.
///
/// Recognition only — whether `name` actually names a declared
/// `@[element(args = "…")]` handler, and whether that handler's pattern
/// matches the remainder, is a lowering-time question
/// (`hir::lower_native::element::try_dispatch`), not this function's. An
/// unresolved `!name` line still parses cleanly (see [`bang_dispatch`]'s
/// doc for why that composes with `\!`, the ruled line-start escape).
pub
/// Parses a confirmed [`at_bang_dispatch`] position into one `BANG_DISPATCH`:
/// the `!`, a `DISPATCH_NAME` holding the dispatching identifier, and the
/// remainder as a fused `CONTENT_LINE` via `content::content_line` —
/// the exact same fused-line technique [`cue_line`]'s `COMPACT_CUE` arm uses
/// for `@NAME: text`, so interpolation, glue, inline markup and trailing tags
/// all parse in the remainder exactly as they would in any other content
/// line.
///
/// Reusing `content_line` here, rather than a bespoke raw-text scan, is
/// deliberate: `hir::lower_native::element::try_dispatch` requires the
/// remainder to be **wholly literal** (no interpolation etc.) before a
/// portable-regex pattern can match it — exactly the same requirement
/// natural-notation claiming already enforces
/// (`hir::lower_native::element::candidate`) — so a dynamic remainder still
/// parses, and is diagnosed loudly downstream (`E129`, "parses cleanly but
/// has no HIR lowering yet") rather than being rejected here at the grammar
/// level.
///
/// Composes with `\!` (§8d.6, the ruled line-start escape,
/// `markup::at_line_start_escape`) by construction, not by a special case
/// here: `\!` lexes as `BACKSLASH` `BANG`, never a bare `BANG`, so
/// `body_line`'s own dispatch on `p.current()` never reaches this function
/// for an escaped `!` — it falls to the ordinary content-line default arm,
/// exactly like it did before this sigil existed.
pub
// ── Parentheticals (chain-gated) ─────────────────────────────────────
/// True at a whole-line `( … )` **inside a live cue chain** — the
/// inventory's "`(…)` line, chain: after cue or dialogue".
///
/// The chain gate is load-bearing, not decoration. A bare `(name)` line is
/// already a shipped construct: G-1's content-line label
/// (`content::at_content_label`), used as a backward-divert target — the
/// `tests/tier1-brink-respell/labeled-mid-flow-gather` fixture has two of
/// them. Requiring a live chain means those lines, and every other
/// `(label)` outside dialogue, parse exactly as they did before this rule
/// existed; only a `(…)` line that follows a cue can become a
/// parenthetical. That is also why the residual ambiguity
/// `content::at_content_label` documents ("`(Sighing) I trudge on.` is
/// indistinguishable from a real label by construction") does not widen
/// here: a parenthetical must fill its whole line.
pub
/// Raw offset of the `)` closing the `(` at offset 0, or `None` if the
/// line ends first. Bounded by the physical line — it never scans past a
/// `NEWLINE`/EOF, so this cannot become an unbounded walk of the token
/// stream.
/// `(hushed)` — the delivery line. Trailing tags are accepted for the same
/// reason every other prose line accepts them; the parenthetical's own
/// text is a single raw run, since a delivery is literal text, never
/// interpolated content.
pub