@@ -256,33 +256,37 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
256256 // exclusions, filtered ONCE here instead of re-tested per matcher per position.
257257 const scanMatchers = tokenMatchers . filter ( tm =>
258258 tm . name !== templateTokenName && ! markupTokenNames . has ( tm . name ) && ! indentTokenNames . has ( tm . name ) ) ;
259- // String-literal token names (the `string`-flagged tokens — quoted scalars in YAML). Used by the
260- // flow mapping-separator guard below: a quoted scalar can never run past its closing quote, so a
261- // `:` immediately after one (inside flow) is ALWAYS the mapping `key: value` separator, never the
262- // start of a plain scalar — derived from the `string` flag, not a hardcoded token name.
263- const stringTokenNames = new Set ( grammar . tokens . filter ( t => t . string ) . map ( t => t . name ) ) ;
264- // Plain-scalar token names: the tokens carrying a block-context pattern variant (`blockPattern`).
265- // In YAML these are exactly the UNQUOTED scalar family (plain / key / number / boolean-null) — the
266- // ones whose flow-vs-block forms differ because flow indicators are content in block. Used by the
267- // flow multi-line-plain FOLD post-pass: a plain scalar folded across a flow-internal newline arrives
268- // as ADJACENT plain tokens (a space-separated plain is already one token; only a NEWLINE splits it),
269- // which the post-pass re-merges. Derived from `blockPattern`, not a hardcoded token name.
270- const plainScalarTokenNames = new Set ( grammar . tokens . filter ( t => tokenBlockPatternSource ( t ) ) . map ( t => t . name ) ) ;
271- // The generic (catch-all) plain-scalar token: the LAST-declared blockPattern token. Declaration
272- // order is specific-before-general (YAML: Key, Num, BoolNull, Plain — the typed/key shapes win
273- // earlier, so the broadest string-valued plain is necessarily last). Used as the type emitted for
274- // a folded plain-scalar CONTINUATION line — a more-indented line after a plain LEAF whose leading
275- // glyph (`-`/`&`/`!`/`[`/`?`/`*`) is plain CONTENT here, not structure (so it can't be lexed by
276- // the plain head pattern, which forbids those starts). Null when no blockPattern token exists.
277- const plainContinuationTokenName = [ ...grammar . tokens ] . reverse ( ) . find ( t => tokenBlockPatternSource ( t ) ) ?. name ?? null ;
259+ // Flow mapping-separator carve-out MEMBERSHIP (IndentConfig.flowSeparatorAfterTokens). A `:` glued
260+ // (inside flow) right after one of these tokens is ALWAYS the mapping `key: value` separator, never
261+ // the start of a `:`-led plain scalar — a quoted scalar / flow-close can never run past its closer.
262+ // EXPLICIT list now (was derived from the `string` flag, which silently enlisted every string-region
263+ // token); the carve-out is OFF when the list is absent. See the flow `:` guard below.
264+ const flowSeparatorAfterTokens = new Set ( indent ?. flowSeparatorAfterTokens ?? [ ] ) ;
265+ // Plain-scalar FOLD MEMBERSHIP (IndentConfig.foldTokens). The token TYPES that participate in YAML's
266+ // plain-scalar continuation folding — in YAML the UNQUOTED scalar family (plain / key / number /
267+ // boolean-null). EXPLICIT list now (was derived from `blockPattern`, which gave folding to ANY
268+ // block-pattern token); folding is OFF when the list is absent. Used by: the block-context fold (a
269+ // deeper line after a plain leaf), the flow illegal-head continuation, and the flow multi-line merge
270+ // post-pass — a plain scalar folded across a flow-internal newline arrives as ADJACENT plain tokens
271+ // (a NEWLINE splits it), which the post-pass re-merges.
272+ const foldTokens = indent ?. foldTokens ?? [ ] ;
273+ const plainScalarTokenNames = new Set ( foldTokens ) ;
274+ // The generic (catch-all) plain-scalar token: the LAST-named fold token. Declaration order is
275+ // specific-before-general (YAML: Key, Num, BoolNull, Plain — the typed/key shapes win earlier, so
276+ // the broadest string-valued plain is necessarily last). Used as the type emitted for a folded
277+ // plain-scalar CONTINUATION line — a more-indented line after a plain LEAF whose leading glyph
278+ // (`-`/`&`/`!`/`[`/`?`/`*`) is plain CONTENT here, not structure (so it can't be lexed by the plain
279+ // head pattern, which forbids those starts). Null when no fold token is declared.
280+ const plainContinuationTokenName = foldTokens . length ? foldTokens [ foldTokens . length - 1 ] : null ;
278281 // The generic plain token's FLOW pattern (its `pattern`, not the block variant) — used by the flow
279282 // illegal-head continuation fallback: a char that no token can START here (e.g. YAML's `%`/`@`/backtick,
280283 // illegal as a plain START) is, when it follows a plain scalar inside a flow collection, mid-scalar
281284 // CONTENT. We then consume that one head char plus whatever plain BODY follows (matched by this
282285 // pattern at the next position), emit it as a plain-continuation token, and let the flow fold post-pass
283286 // merge it with the preceding scalar. Compiled once; null when no generic plain token exists.
284287 const plainFlowRe = ( ( ) => {
285- const t = [ ...grammar . tokens ] . reverse ( ) . find ( t => tokenBlockPatternSource ( t ) ) ;
288+ if ( ! plainContinuationTokenName ) return null ;
289+ const t = grammar . tokens . find ( t => t . name === plainContinuationTokenName ) ;
286290 return t ? new RegExp ( `^(?:${ tokenPatternSource ( t ) } )` ) : null ;
287291 } ) ( ) ;
288292 // Does the line content starting at `start` carry a KEY SEPARATOR — an unquoted `:` followed by
@@ -306,7 +310,22 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
306310 const kBlockScalarTok = kOf ( indent ?. blockScalar ?. token ?? null ) ;
307311 const kRawBlockTok = kOf ( indent ?. rawBlock ?. token ?? null ) ;
308312 const kPlainCont = kOf ( plainContinuationTokenName ) ;
309- const tColon = puLitOf . get ( ':' ) ?? 0 ;
313+ // The mapping KEY/VALUE separator (IndentConfig.keyValueSeparator, default `:`) — the ONE source of
314+ // truth shared with gen-tm for every "is this a mapping-key line" sniff in the lexer. `kKvSep` is its
315+ // punctuation-literal intern, for the flow-`:` carve-out push.
316+ const keyValueSep = indent ?. keyValueSeparator ?? ':' ;
317+ const kKvSep = puLitOf . get ( keyValueSep ) ?? 0 ;
318+ // Is `src` at `i` a mapping KEY separator — the `keyValueSeparator` literal followed by whitespace /
319+ // EOL / a flow indicator (`,`/`[`/`]`/`{`/`}`)? The single shared test behind every key-line sniff
320+ // (`lineHasKeySeparator`, `startsBlockStructuralNode`) so they read the separator from ONE place
321+ // (the config) rather than each hardcoding `:`. Returns the index PAST the separator on a hit (so
322+ // the caller can resume), or -1 on no match.
323+ function keySepAt ( src : string , i : number ) : number {
324+ if ( ! src . startsWith ( keyValueSep , i ) ) return - 1 ;
325+ const n = src [ i + keyValueSep . length ] ;
326+ return ( n === undefined || n === ' ' || n === '\t' || n === '\n' || n === '\r'
327+ || n === ',' || n === '[' || n === ']' || n === '{' || n === '}' ) ? i + keyValueSep . length : - 1 ;
328+ }
310329
311330 function lineHasKeySeparator ( src : string , start : number ) : boolean {
312331 for ( let i = start ; i < src . length ; i ++ ) {
@@ -327,7 +346,7 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
327346 if ( src [ i ] !== "'" ) break ; continue ;
328347 }
329348 if ( ( ch === ' ' || ch === '\t' ) && src [ i + 1 ] === '#' ) break ; // trailing comment → any sep would be earlier
330- if ( ch === ':' ) { const n = src [ i + 1 ] ; if ( n === undefined || n === ' ' || n === '\t' || n === '\n' || n === '\r' || n === ',' || n === '[' || n === ']' || n === '{' || n === '}' ) return true ; }
349+ if ( keySepAt ( src , i ) >= 0 ) return true ;
331350 }
332351 return false ;
333352 }
@@ -392,18 +411,19 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
392411 function startsBlockStructuralNode ( src : string , start : number , allowProperty = true ) : boolean {
393412 const c0 = src [ start ] ;
394413 if ( c0 === '[' || c0 === '{' || c0 === '*' ) return false ; // flow collection / alias → not indentation
395- if ( ( c0 === '-' || c0 === '?' || c0 === ':' ) && sepAfter ( src [ start + 1 ] ) ) return true ; // indicator / empty key
414+ if ( c0 !== undefined && compactIndicatorSet . has ( c0 ) && sepAfter ( src [ start + 1 ] ) ) return true ; // compact indicator (`-`/`?`)
415+ if ( src . startsWith ( keyValueSep , start ) && sepAfter ( src [ start + keyValueSep . length ] ) ) return true ; // empty key (`:` then ws/EOL)
396416 if ( ( c0 === '&' || c0 === '!' ) && allowProperty ) return true ; // node property → establishes a node here
397417 if ( c0 === '&' || c0 === '!' ) return false ; // property after `:` → inline value, legal
398- // Scalar key sniff: scan the line for an unquoted `:` followed by ws/EOL/flow-indicator (a
399- // block key separator), skipping over "…"/'…' regions and stopping at a ` #` comment / EOL.
418+ // Scalar key sniff: scan the line for an unquoted key separator followed by ws/EOL/flow-indicator,
419+ // skipping over "…"/'…' regions and stopping at a ` #` comment / EOL.
400420 for ( let i = start ; i < src . length ; i ++ ) {
401421 const ch = src [ i ] ;
402422 if ( ch === '\n' || ch === '\r' ) break ;
403423 if ( ch === '"' ) { i ++ ; while ( i < src . length && src [ i ] !== '"' && src [ i ] !== '\n' ) { if ( src [ i ] === '\\' ) i ++ ; i ++ ; } continue ; }
404424 if ( ch === "'" ) { i ++ ; while ( i < src . length && src [ i ] !== '\n' ) { if ( src [ i ] === "'" && src [ i + 1 ] !== "'" ) break ; if ( src [ i ] === "'" ) i ++ ; i ++ ; } continue ; }
405425 if ( ( ch === ' ' || ch === '\t' ) && src [ i + 1 ] === '#' ) break ; // trailing comment → key sep would be earlier
406- if ( ch === ':' ) { const n = src [ i + 1 ] ; if ( n === undefined || n === ' ' || n === '\t' || n === '\n' || n === '\r' || n === ',' || n === '[' || n === ']' || n === '{' || n === '}' ) return true ; }
426+ if ( keySepAt ( src , i ) >= 0 ) return true ;
407427 }
408428 return false ;
409429 }
@@ -424,8 +444,8 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
424444 } else break ;
425445 }
426446 if ( i >= src . length || src [ i ] === '\n' || src [ i ] === '\r' ) return false ; // property alone on the line → no nest
427- if ( ( src [ i ] === '-' || src [ i ] === '?' ) && sepAfter ( src [ i + 1 ] ) ) return true ; // nested indicator
428- return startsBlockStructuralNode ( src , i , false ) ; // a mapping key (the `:`- sniff)
447+ if ( src [ i ] !== undefined && compactIndicatorSet . has ( src [ i ] ) && sepAfter ( src [ i + 1 ] ) ) return true ; // nested compact indicator
448+ return startsBlockStructuralNode ( src , i , false ) ; // a mapping key (the key-separator sniff)
429449 }
430450
431451 // Scan from inside a template span to its next boundary: an interpolation hole
@@ -544,7 +564,11 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
544564 // The §7.4 / multi-line-flow bookkeeping is indent-only (a newline grammar has no stack).
545565 if ( flowDepth === 0 && indent ) {
546566 const prevTok = tokens [ tokens . length - 2 ] ; // the token before this just-pushed open
547- flowValueIndent = ( prevTok && prevTok . type === '' && ( prevTok . text === ':' || prevTok . text === '-' ) )
567+ // value/item position: a flow opened right after the key/value separator (map value) or a
568+ // sequence-item indicator. The `-` here is the seq-item lead specifically (NOT every
569+ // compactIndicator — `?` is an explicit KEY, not a value position); classifying it from
570+ // config is the (D) indicator-role split, deferred — see issue #44.
571+ flowValueIndent = ( prevTok && prevTok . type === '' && ( prevTok . text === keyValueSep || prevTok . text === '-' ) )
548572 ? indentStack [ indentStack . length - 1 ] : - 1 ;
549573 flowSawNewline = false ; // start tracking whether this flow spans >1 line
550574 }
@@ -776,10 +800,10 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
776800 // while `-`/`?` include them (`-\t&a x` IS an error). Block context only (flowDepth===0).
777801 if ( indent && flowDepth === 0 ) { // §6.1 tab-after-indicator error is YAML-specific
778802 const prev = tokens [ tokens . length - 1 ] ;
779- const isIndicator = prev && prev . type === '' && ( prev . text === '-' || prev . text === '?' || prev . text === ':' ) ;
803+ const isIndicator = prev && prev . type === '' && ( compactIndicatorSet . has ( prev . text ) || prev . text === keyValueSep ) ;
780804 if ( isIndicator ) {
781805 let q = pos ; while ( q < source . length && ( source [ q ] === ' ' || source [ q ] === '\t' ) ) q ++ ;
782- if ( source . slice ( pos , q ) . includes ( '\t' ) && startsBlockStructuralNode ( source , q , prev ! . text !== ':' ) ) {
806+ if ( source . slice ( pos , q ) . includes ( '\t' ) && startsBlockStructuralNode ( source , q , prev ! . text !== keyValueSep ) ) {
783807 throw new Error ( `Tab character used in indentation at offset ${ pos } ` ) ;
784808 }
785809 }
@@ -1046,14 +1070,16 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
10461070 // the separator — emit it as the `:` punctuation literal here. Gated on flow (block-context `:`
10471071 // separators are handled by the KEY-position lookaheads). yaml-test-suite 5MUD / 5T43 / 9MMW
10481072 // / C2DT / K3WX (quoted key) and the flow-collection-key cohort.
1049- // flowColonSeparator: false disables the YAML `"key":value` / ` }: value` flow
1050- // separator carve-out, for indentation grammars with `:name`-shaped tokens that
1051- // may legally follow a quoted value or a flow-close delimiter.
1052- if ( indent && indent . flowColonSeparator !== false && flowDepth > 0 && source [ pos ] === ':' ) {
1073+ // Declaring flowSeparatorAfterTokens (a non-empty list — YAML: the quoted-key tokens) ENABLES
1074+ // the carve-out; it then fires after a NAMED token OR after any flow-CLOSE delimiter (`]`/` }`,
1075+ // which structurally can't run past its closer either). An indentation grammar that declares no
1076+ // such tokens gets no carve-out at all, so a `:name`-shaped token survives after values in flow.
1077+ // The separator glyph is keyValueSeparator (default `:`).
1078+ if ( indent && flowDepth > 0 && flowSeparatorAfterTokens . size && source . startsWith ( keyValueSep , pos ) ) {
10531079 const prevTok = tokens [ tokens . length - 1 ] ;
1054- if ( prevTok && ( stringTokenNames . has ( prevTok . type ) || ( prevTok . type === '' && flowCloseSet . has ( prevTok . text ) ) ) ) {
1055- push ( mkPu ( ':' , pos , tColon ) ) ;
1056- pos += 1 ;
1080+ if ( prevTok && ( flowSeparatorAfterTokens . has ( prevTok . type ) || ( prevTok . type === '' && flowCloseSet . has ( prevTok . text ) ) ) ) {
1081+ push ( mkPu ( keyValueSep , pos , kKvSep ) ) ;
1082+ pos += keyValueSep . length ;
10571083 continue ;
10581084 }
10591085 }
@@ -1213,7 +1239,7 @@ export function createLexer(grammar: CstGrammar, intern?: LexerIntern) {
12131239 let q = i ; while ( q < source . length && source [ q ] === ' ' ) q ++ ;
12141240 return q > i && source [ q ] === '-' && sepAfter ( source [ q + 1 ] ) ;
12151241 } ;
1216- const colonPairsExplicit = wasLineLead && lit === ':' && currentLineCol === lastExplicitKeyCol ;
1242+ const colonPairsExplicit = wasLineLead && lit === keyValueSep && currentLineCol === lastExplicitKeyCol ;
12171243 const compactColon = colonPairsExplicit && dashAfter ( pos ) ;
12181244 // A line-lead `:` at its `?`'s column USES UP that pairing — the explicit entry now has its
12191245 // value, so a SECOND `: …` at the same column (`? a\n: - b\n: - c`, yaml-test-suite cousin) is
0 commit comments