@@ -48,7 +48,10 @@ interface TmGrammar {
4848 $schema : string ;
4949 name : string ;
5050 scopeName : string ;
51- patterns : ( { include : string } ) [ ] ;
51+ // Usually a flat include list; an indentation grammar with a block scalar wraps these in a
52+ // line-spanning `meta.stream` region (a `TmPattern` with begin/while/patterns) — see the grammar
53+ // root return — so the top level admits region wrappers too, like `TmPattern.patterns` does.
54+ patterns : ( TmPattern | { include : string } ) [ ] ;
5255 repository : Record < string , TmPattern > ;
5356}
5457
@@ -4627,6 +4630,12 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
46274630 // since the lookahead requires the rest of the header line to be only indicators + an optional
46284631 // ` #comment`.
46294632 const blockScalar = grammar . indent ?. blockScalar ;
4633+ // Block-scalar helpers shared with §2b (the `? |` explicit-key block scalar): the introducer
4634+ // sub-pattern, the funky body builder, the inner introducer rule, and the header-prefix includes.
4635+ // Assigned inside §2a; reused in §2b so both block-scalar shapes use one portable structure.
4636+ let bsIntro = '' ;
4637+ let bsFunkyIntroRule : ( ( content : string ) => TmPattern ) | null = null ;
4638+ let bsHeaderIncludes : { include : string } [ ] = [ ] ;
46304639 if ( blockScalar ) {
46314640 const bsTok = grammar . tokens . find ( t => t . name === blockScalar . token ) ;
46324641 const bsKey = blockScalar . token . toLowerCase ( ) ;
@@ -4635,37 +4644,110 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
46354644 // introducer + indentation/chomping indicators (a digit and a `+`/`-`, in either order, or a
46364645 // lone `+`/`-`), then a lookahead requiring the rest of the header line to be blank or a comment.
46374646 const indicators = '(?:[1-9][-+]?|[-+][1-9]?|[-+])?' ;
4647+ const intro = `${ introClass } ${ indicators } ` ;
46384648 const commentIncs = commentIncludeKeys . map ( k => ( { include : `#${ k } ` } ) ) ;
4639- // This is the proven textmate/yaml.tmbundle structure (a `begin`/`end` region, NOT `begin`/
4640- // `while`): a flat grammar has no enclosing line-spanning rule, and a top-level `while: \G`
4641- // region cannot sustain itself across lines — vscode-textmate sets the while-anchor to -1
4642- // unless a PARENT begin captured the end-of-line, so the chain collapses after the header line
4643- // (the maintained RedCMD grammar's `while: \G` only survives because it is nested many regions
4644- // deep). Two load-bearing details:
4645- // • the begin's trailing `(.*\n?)` group CAPTURES the newline. Without it the `(?!\G)` arm of
4646- // the `end` (below) fires at the very start of line 2 and the body is never scoped.
4647- // • `end: ^(?=\S)|(?!\G)` — the body ends at a line that dedents to a non-space at column 0
4648- // (a sibling key, a new node, a `---`/`...` marker), and `(?!\G)` guards the contiguity the
4649- // captured newline establishes.
4650- // The inner rule auto-detects the body indentation (§8.1.1): `^([ \t]+)(?! )` captures the
4651- // first content line's full indent as `\1`, and `end: ^(?!\1|[ \t]*$)` holds the body for every
4652- // line that re-matches `\1` (or is blank) and releases at the first shallower non-blank line —
4653- // so a deeper-nested scalar's siblings (`- a: |` … ` b: |`) are NOT swallowed.
4654- repository [ bsKey ] = {
4655- begin : `(${ introClass } ${ indicators } )(?=[\\t ]*(?:#|$))(.*\\n?)` ,
4656- beginCaptures : {
4657- '1' : { name : `${ bsScope } .${ langName } ` } ,
4658- '2' : { patterns : commentIncs } ,
4649+ const bsContent = `${ bsScope } .${ langName } ` ;
4650+ // A block scalar BODY must scope `string.unquoted.block` across EMPTY lines. A flat `begin`/`end`
4651+ // region (the old textmate/yaml.tmbundle shape) collapses at the first LEADING empty line: the
4652+ // inner indent rule has not opened yet, nothing is consumed, and the `(?!\G)` arm of the `end`
4653+ // fires (vscode-textmate#114 — a top-level region's `\G` anchor dies once a line is not contiguous
4654+ // with the begin's captured EOL). The maintained RedCMD grammar survives empties only because its
4655+ // block scalar sits many `while: \G` regions deep and each parent re-anchors `\G` at every line,
4656+ // blank ones included. We replicate the MINIMAL slice of that nesting, in three PORTABLE pieces
4657+ // (no variable-length lookbehind — those are rejected by TextMate 2.0/Onigmo and GitHub-Linguist):
4658+ // 1. a `meta.stream` parent (`begin: ^(?!\G)`/`while: ^`) wraps ALL top patterns (added at the
4659+ // grammar root below) so `\G` is re-anchored every line — the empty-line survival lever.
4660+ // 2. the BODY is RedCMD's "funky wrapper" (`begin: $`/`while: \G`): three sub-rules auto-detect
4661+ // the content indent `\1`, scope it `string.unquoted.block`, and — via the middle rule's
4662+ // `end: \G(?!\1)(?=[\t ]*#)` — release at a SHALLOWER comment line (so a dedented `# c` is a
4663+ // real comment, not swallowed). Empty-line-proof.
4664+ // 3. the OUTER region bounds SIBLINGS by the NODE indentation, captured by a FORWARD group: the
4665+ // begin starts AT LINE START `^([ \t]*)` and CONSUMES the indent into `\1`, with a lookahead
4666+ // (the value-prefix `bsVp` below) confirming the line actually carries a value-position block
4667+ // scalar. `while: \G(?=\1[ \t]|[ \t]*$)` continues while a line is blank or indented past the
4668+ // node and ends at a sibling at the node column. The header line's key / `:` / anchor / tag
4669+ // are re-scoped by the normal token includes (`bsHeaderIncs`) since the indent consume put
4670+ // them INSIDE the region; an inner `bsIntroRule` matches the `|`/`>` introducer (+ trailing
4671+ // comment) and runs the funky body.
4672+ // Because the begin matches at LINE START it competes at column 0 with `#key`/quoted-keys (which
4673+ // also start there); on a same-start tie oniguruma picks the FIRST listed pattern, so these rules
4674+ // are ranked ABOVE the key/scalar tokens in scopeOrder. Their lookahead requires a real
4675+ // `[|>]…(#|$)` value-position header, so they never steal a non-block-scalar line.
4676+ const funkyBody = ( content : string ) => [
4677+ {
4678+ begin : '$' ,
4679+ while : '\\G' ,
4680+ patterns : [
4681+ { begin : '\\G( ++)$' , while : '\\G(?>(\\1)$|(?!\\1)( *+)($|.))' , contentName : content } ,
4682+ {
4683+ begin : '\\G(?!$)(?=( *+))' ,
4684+ end : '\\G(?!\\1)(?=[\\t ]*+#)' ,
4685+ patterns : [
4686+ { begin : '\\G( *+)' , while : '\\G(?>(\\1)|( *+)($|[^\\t#]|[\\t ]++[^#]))' , contentName : content } ,
4687+ ] ,
4688+ } ,
4689+ { begin : '(?!\\G)(?=[\\t ]*+#)' , while : '\\G' , patterns : commentIncs } ,
4690+ ] ,
46594691 } ,
4660- end : '^(?=\\S)|(?!\\G)' ,
4661- patterns : [
4662- {
4663- begin : '^([ \\t]+)(?! )' ,
4664- end : '^(?!\\1|[ \\t]*$)' ,
4665- contentName : `${ bsScope } .${ langName } ` ,
4666- } ,
4667- ] ,
4692+ ] ;
4693+ // Inner introducer rule: leading `[\t ]*` skips the separator whitespace (the space after `:`/`-`),
4694+ // captures the `|`/`>` (+indicators) and the rest-of-line trailing comment, then runs the funky
4695+ // body (its `begin: $` opens at the header-line EOL). `while: \G` keeps it alive across the body.
4696+ const bsIntroRule = ( content : string ) => ( {
4697+ begin : `[\\t ]*(${ intro } )(?=[\\t ]*(?:#|$))([\\t ]*.*)` ,
4698+ beginCaptures : { '1' : { name : content } , '2' : { patterns : commentIncs } } ,
4699+ while : '\\G' ,
4700+ patterns : funkyBody ( content ) ,
4701+ } ) ;
4702+ // Header-prefix token includes: re-scope the part of the header line BEFORE the introducer (a doc
4703+ // marker / key / `:` / anchor / tag), since the line-start indent consume swallowed the engine
4704+ // position past them. Derived from what this grammar actually emits (an unresolved include is a
4705+ // no-op in vscode-textmate, but we list only the keys that exist to keep the grammar clean). The
4706+ // explicit-key entries are emitted in §2b (after this block) so they are gated by the same
4707+ // `detectExplicitKey` predicate; punctuation is emitted late so it is included unconditionally
4708+ // here (an indentation grammar with a block scalar always has the `:`/`-` punctuation token).
4709+ const bsHasExplicitKey = ! ! detectExplicitKey ( grammar ) ;
4710+ const bsHeaderIncs = [
4711+ ...( repository [ 'docstart' ] ? [ { include : '#docstart' } ] : [ ] ) ,
4712+ ...( repository [ 'docend' ] ? [ { include : '#docend' } ] : [ ] ) ,
4713+ ...( bsHasExplicitKey ? [ { include : '#explicit-key' } , { include : '#explicit-key-indicator' } ] : [ ] ) ,
4714+ ...( repository [ 'dquotekey' ] ? [ { include : '#dquotekey' } ] : [ ] ) ,
4715+ ...( repository [ 'squotekey' ] ? [ { include : '#squotekey' } ] : [ ] ) ,
4716+ ...( repository [ 'key' ] ? [ { include : '#key' } ] : [ ] ) ,
4717+ ...( repository [ 'anchor' ] ? [ { include : '#anchor' } ] : [ ] ) ,
4718+ ...( repository [ 'tag' ] ? [ { include : '#tag' } ] : [ ] ) ,
4719+ { include : '#punctuation' } ,
4720+ ] ;
4721+ // Value-PREFIX: the structural lead-in that may precede a value-position introducer on its header
4722+ // line AFTER the node indent is stripped. A genuine `|`/`>` introducer is the FIRST value token, so
4723+ // only separators (sequence dash `-`, explicit-key `?`, doc markers `---`/`...`), an optional
4724+ // mapping key + `:` separator, and node properties (anchors `&` / tags `!`) may sit before it —
4725+ // never plain-scalar content. This is what stops `a: foo|` / `a: foo |` (plain scalars that merely
4726+ // END in a pipe) from opening a region: after `a: ` the next token is `foo`, not a separator/
4727+ // key-colon/property, so the lookahead fails. The key arm matches up to the FIRST `: ` separator.
4728+ const bsProp = '(?:[&!][^\\t\\n\\f\\r \\[\\]{},]*[\\t ]+)*' ;
4729+ const bsVp = `(?:(?:---|\\.\\.\\.)[\\t ]+)?(?:[-?][\\t ]+)*(?:[^\\n]*?:[\\t ]+)?${ bsProp } ` ;
4730+ // Expose the introducer / inner-rule / header-includes to §2b (the `? |` explicit-key variant).
4731+ bsIntro = intro ;
4732+ bsFunkyIntroRule = bsIntroRule ;
4733+ bsHeaderIncludes = bsHeaderIncs ;
4734+ repository [ bsKey ] = {
4735+ begin : `^([ \\t]*)(?=${ bsVp } ${ intro } [\\t ]*(?:#|$))` ,
4736+ while : '\\G(?=\\1[ \\t]|[ \\t]*$)' ,
4737+ patterns : [ bsIntroRule ( bsContent ) , ...bsHeaderIncs ] ,
46684738 } ;
4739+ // Sequence entry whose mapping VALUE is the block scalar (`- a: |` … ` b:`): bound siblings at the
4740+ // KEY column, not the dash column, else the next entry key is swallowed. The begin consumes the
4741+ // leading indent `\1` AND the dash + its trailing spaces `\3`, and the bound `\1[ \t]\3[ \t]` is
4742+ // one column past the key. A pure-space backref (`\1`, `\3`) standing in for the dash column keeps
4743+ // it portable (no literal `- ` backref, which would never match space-indented body lines).
4744+ repository [ `${ bsKey } -seq` ] = {
4745+ begin : `^([ \\t]*)(-)([ \\t]+)(?=(?:[-?][\\t ]+)*[^\\n]*?:[\\t ]+${ bsProp } ${ intro } [\\t ]*(?:#|$))` ,
4746+ beginCaptures : { '2' : { name : `punctuation.${ langName } ` } } ,
4747+ while : '\\G(?=\\1[ \\t]\\3[ \\t]|[ \\t]*$)' ,
4748+ patterns : [ bsIntroRule ( bsContent ) , ...bsHeaderIncs ] ,
4749+ } ;
4750+ topPatterns . push ( { include : `#${ bsKey } -seq` } ) ;
46694751 }
46704752
46714753 // ── 2b. Explicit mapping key (`? key`) ──
@@ -4714,26 +4796,18 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
47144796 // A block scalar can ALSO be an explicit key (`? |` / `? >`). An implicit key must be a single
47154797 // line, so a multi-line block scalar key is ALWAYS `?`-introduced — and like every other scalar
47164798 // key (plain / quoted, both already `entity.name.tag`) its content is the KEY NAME, not a value
4717- // string. The §2a region scopes a block body `string.unquoted.block`; here the SAME begin/end
4718- // machinery is gated on the `?` indicator and scopes the introducer + body with the key scope.
4719- // Leftmost-match makes the `?`-anchored begin win over the bare §2a block scalar; a block scalar
4720- // in VALUE position (`: |`) has no leading `?`, so it is untouched.
4721- if ( blockScalar ) {
4722- const introClass = `[${ blockScalar . introducers . map ( escapeForCharClass ) . join ( '' ) } ]` ;
4723- const indicators = '(?:[1-9][-+]?|[-+][1-9]?|[-+])?' ;
4724- const commentIncs = commentIncludeKeys . map ( k => ( { include : `#${ k } ` } ) ) ;
4799+ // string. Same PORTABLE structure as §2a (forward-captured node indent + funky body), but gated on
4800+ // the `?` indicator and scoping the introducer + body with the KEY scope. The `?` is captured as
4801+ // map-key punctuation; the inner introducer rule scopes the `|`/`>` and the body as the key name.
4802+ // Ranked above the value-position block scalar (scopeOrder) so `? |` wins; a `: |` value has no
4803+ // leading `?`, so it is untouched.
4804+ if ( blockScalar && bsFunkyIntroRule ) {
47254805 const keyScope = `${ explicitKey . keyScope } .${ langName } ` ;
47264806 repository [ 'blockscalar-key' ] = {
4727- begin : `(${ escapeRegex ( explicitKey . indicator ) } )([\\t ]+)(${ introClass } ${ indicators } )(?=[\\t ]*(?:#|$))(.*\\n?)` ,
4728- beginCaptures : {
4729- '1' : { name : `punctuation.definition.map.key.${ langName } ` } ,
4730- '3' : { name : keyScope } ,
4731- '4' : { patterns : commentIncs } ,
4732- } ,
4733- end : '^(?=\\S)|(?!\\G)' ,
4734- patterns : [
4735- { begin : '^([ \\t]+)(?! )' , end : '^(?!\\1|[ \\t]*$)' , contentName : keyScope } ,
4736- ] ,
4807+ begin : `^([ \\t]*)(${ escapeRegex ( explicitKey . indicator ) } )([\\t ]+)(?=${ bsIntro } [\\t ]*(?:#|$))` ,
4808+ beginCaptures : { '2' : { name : `punctuation.definition.map.key.${ langName } ` } } ,
4809+ while : '\\G(?=\\1[ \\t]|[ \\t]*$)' ,
4810+ patterns : [ bsFunkyIntroRule ( keyScope ) , ...bsHeaderIncludes ] ,
47374811 } ;
47384812 topPatterns . push ( { include : '#blockscalar-key' } ) ;
47394813 }
@@ -7178,11 +7252,22 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
71787252 // The bare explicit-key indicator (`?` alone on its line) must beat the generic `?` punctuation
71797253 // token (rank 9) so it scopes as the map-key punctuation, not a plain bracket.
71807254 if ( key === 'explicit-key-indicator' ) return 0.82 ;
7181- // A block scalar that is an explicit key (`? |` / `? >`) — its `?`-anchored begin must beat the
7182- // bare `?` punctuation token AND the value-position block scalar so the body scopes as the key
7183- // name. Its begin is highly specific (`?` + block introducer + blank/comment-only header), so
7184- // ranking it with #explicit-key is safe. Mutually exclusive with #explicit-key (plain key body).
7185- if ( key === 'blockscalar-key' ) return 0.8 ;
7255+ // The value-position block scalars (`key: |` / `- |` / `--- |`) and the `? |` explicit-key
7256+ // variant now begin AT LINE START (`^([ \t]*)`), so they compete at column 0 with #key /
7257+ // quoted-keys / #docstart / #explicit-key (all of which also start there). On a same-start tie
7258+ // oniguruma's scanner picks the FIRST listed pattern, so these MUST out-rank every key/scalar/
7259+ // doc-marker token (rank ≥ 0.8) — their lookahead requires a real `[|>]…(#|$)` value-position
7260+ // header, so they never steal a non-block-scalar line. Three precedence facts decide the order:
7261+ // • `-seq` (dash + KEY + `:`) and the plain rule BOTH match `- a: |`, but `-seq` bounds siblings
7262+ // at the deeper KEY column, so it must be tried first → lowest rank.
7263+ // • `blockscalar-key` (`?`-anchored) and the plain rule BOTH match `? |` (the plain VP admits a
7264+ // leading `?`), but the key variant scopes the body as the key NAME, so it must win → below
7265+ // the plain rule.
7266+ // • the plain `blockscalar` is the fallback (bare `|`, `key: |`, `--- |`).
7267+ const bsRank = grammar . indent ?. blockScalar ?. token . toLowerCase ( ) ;
7268+ if ( bsRank && key === `${ bsRank } -seq` ) return 0.5 ;
7269+ if ( key === 'blockscalar-key' ) return 0.55 ;
7270+ if ( bsRank && key === bsRank ) return 0.6 ;
71867271 // A flow collection (`{ … }` / `[ … ]`) is a begin/end region opened by a bracket; it must be
71877272 // tried before #punctuation (which would otherwise claim the `{`/`[` as a bare bracket) and
71887273 // before the scalar tokens. Its `{`/`[` can never lead a plain scalar, so this ranking is safe.
@@ -7260,11 +7345,28 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
72607345 // clobbers a key that already matches by name. Pure rename → tokenization unchanged.
72617346 applyCanonicalRepoNames ( grammar , repository , orderedPatterns ) ;
72627347
7348+ // ── meta.stream wrapper (indentation grammars with a block scalar only) ──
7349+ // A block scalar BODY must survive EMPTY lines (see §2a). The mechanism is a line-spanning
7350+ // `while: \G` parent that RE-ANCHORS `\G` at the start of every line — including blank ones — so a
7351+ // nested `while: \G` body region stays alive across blanks instead of collapsing. We wrap ALL top
7352+ // patterns in RedCMD's two-arm `meta.stream` region: the first arm (`begin: ^(?!\G)`/`while: ^`)
7353+ // drives normal top-of-stream tokenisation; the second (`begin: \G(?!$)`/`while: \G`) is the
7354+ // embedded-start case (YAML inside a Markdown fence). Tokenisation of every construct is unchanged
7355+ // — the wrapper only adds the persistent `\G` anchor the block scalar needs. Gated to grammars that
7356+ // actually emit a block scalar so non-indentation languages (TS/HTML/…) stay byte-identical.
7357+ let finalPatterns : ( { include : string } | TmPattern ) [ ] = orderedPatterns ;
7358+ if ( grammar . indent ?. blockScalar ) {
7359+ finalPatterns = [
7360+ { begin : '^(?!\\G)' , while : '^' , name : `meta.stream.${ langName } ` , patterns : orderedPatterns } ,
7361+ { begin : '\\G(?!$)' , while : '\\G' , name : `meta.stream.${ langName } ` , patterns : orderedPatterns } ,
7362+ ] ;
7363+ }
7364+
72637365 return {
72647366 $schema : 'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json' ,
72657367 name : grammarName ,
72667368 scopeName,
7267- patterns : orderedPatterns ,
7369+ patterns : finalPatterns ,
72687370 repository,
72697371 } ;
72707372}
0 commit comments