Skip to content

Commit fa50f82

Browse files
committed
Resolve remaining YAML #12 items: portable num/boolnull + explicit-key indicator
#2 (num/boolnull incompatible with TextMate 2.0 / GitHub-Linguist): the typed-scalar rules carried a negative-lookbehind guard (16 stacked fixed-width `(?<!![ \t]{n})`) to keep a `!␣`-tagged value a string. Negative lookbehind isn't portable to those engines, so the guard is removed — a `!␣`-tagged core value now types BY APPEARANCE, exactly as the official grammar does (`! true` → constant.language.boolean there too). The scope-gap metric is unchanged (the case isn't in the gradable corpus) and the parser stays 100% aligned; num/boolnull are now lookbehind-free. #6 (explicit multiline sequence map key): a `?` indicator alone on its line (key on the following lines) was scoped as a generic punctuation bracket. Emit an explicit-key-indicator rule scoping the bare `?` as punctuation.definition.map.key (matching the same-line `? key` form and the official grammar), ranked ahead of the generic `?` token. The key's collection items already carry the correct string role. Other six grammars byte-identical. Refs #12
1 parent 90c4123 commit fa50f82

3 files changed

Lines changed: 39 additions & 19 deletions

File tree

src/gen-tm.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4700,6 +4700,17 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
47004700
};
47014701
topPatterns.push({ include: '#explicit-key' });
47024702

4703+
// The explicit-key INDICATOR alone on its line — a multi-line / collection key whose body is on
4704+
// the FOLLOWING lines (`?\n- a\n- b\n: …`), so the same-line `? key` rule above doesn't reach it.
4705+
// Scope the bare indicator as the same map-key punctuation. The blank/comment-only rest-of-line
4706+
// lookahead keeps it from stealing a `?` that is plain-scalar content (a plain scalar that merely
4707+
// ends in `?` is matched as one token starting earlier, so leftmost-match leaves this alone).
4708+
repository['explicit-key-indicator'] = {
4709+
match: `(${escapeRegex(explicitKey.indicator)})(?=[\\t ]*(?:#|$))`,
4710+
captures: { '1': { name: `punctuation.definition.map.key.${langName}` } },
4711+
};
4712+
topPatterns.push({ include: '#explicit-key-indicator' });
4713+
47034714
// A block scalar can ALSO be an explicit key (`? |` / `? >`). An implicit key must be a single
47044715
// line, so a multi-line block scalar key is ALWAYS `?`-introduced — and like every other scalar
47054716
// key (plain / quoted, both already `entity.name.tag`) its content is the KEY NAME, not a value
@@ -7164,6 +7175,9 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
71647175
// An explicit mapping-key rule (`? key`) is the most specific scalar context — its indicator
71657176
// pins the following scalar as a key — so it must be tried before the bare key/plain scalars.
71667177
if (key === 'explicit-key') return 0.8;
7178+
// The bare explicit-key indicator (`?` alone on its line) must beat the generic `?` punctuation
7179+
// token (rank 9) so it scopes as the map-key punctuation, not a plain bracket.
7180+
if (key === 'explicit-key-indicator') return 0.82;
71677181
// A block scalar that is an explicit key (`? |` / `? >`) — its `?`-anchored begin must beat the
71687182
// bare `?` punctuation token AND the value-position block scalar so the body scopes as the key
71697183
// name. Its begin is highly specific (`?` + block introducer + blank/comment-only header), so

yaml.tmLanguage.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
{
1313
"include": "#blockscalar-key"
1414
},
15+
{
16+
"include": "#explicit-key-indicator"
17+
},
1518
{
1619
"include": "#flow-sequence"
1720
},
@@ -227,11 +230,11 @@
227230
},
228231
"num": {
229232
"name": "string.unquoted.yaml constant.numeric.yaml",
230-
"match": "(?<!![ \\t]{1})(?<!![ \\t]{2})(?<!![ \\t]{3})(?<!![ \\t]{4})(?<!![ \\t]{5})(?<!![ \\t]{6})(?<!![ \\t]{7})(?<!![ \\t]{8})(?<!![ \\t]{9})(?<!![ \\t]{10})(?<!![ \\t]{11})(?<!![ \\t]{12})(?<!![ \\t]{13})(?<!![ \\t]{14})(?<!![ \\t]{15})(?<!![ \\t]{16})(?:[+\\-]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN)|0x[0-9A-Fa-f]+|0o[0-7]+|[+\\-]?(?:\\.[0-9]+|[0-9]+(?:\\.[0-9]*)?)(?:[eE][+\\-]?[0-9]+)?)(?=[ \\t]+#|[ \\t]*(?:\\r|\\n|,|\\[|\\]|\\{|\\}|:(?:[\\t\\n\\f\\r ]|,|\\[|\\]|\\{|\\}|$))|$)"
233+
"match": "(?:[+\\-]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN)|0x[0-9A-Fa-f]+|0o[0-7]+|[+\\-]?(?:\\.[0-9]+|[0-9]+(?:\\.[0-9]*)?)(?:[eE][+\\-]?[0-9]+)?)(?=[ \\t]+#|[ \\t]*(?:\\r|\\n|,|\\[|\\]|\\{|\\}|:(?:[\\t\\n\\f\\r ]|,|\\[|\\]|\\{|\\}|$))|$)"
231234
},
232235
"boolnull": {
233236
"name": "string.unquoted.yaml constant.language.yaml",
234-
"match": "(?<!![ \\t]{1})(?<!![ \\t]{2})(?<!![ \\t]{3})(?<!![ \\t]{4})(?<!![ \\t]{5})(?<!![ \\t]{6})(?<!![ \\t]{7})(?<!![ \\t]{8})(?<!![ \\t]{9})(?<!![ \\t]{10})(?<!![ \\t]{11})(?<!![ \\t]{12})(?<!![ \\t]{13})(?<!![ \\t]{14})(?<!![ \\t]{15})(?<!![ \\t]{16})(?:true|True|TRUE|false|False|FALSE|null|Null|NULL|~)(?=[ \\t]+#|[ \\t]*(?:\\r|\\n|,|\\[|\\]|\\{|\\}|:(?:[\\t\\n\\f\\r ]|,|\\[|\\]|\\{|\\}|$))|$)"
237+
"match": "(?:true|True|TRUE|false|False|FALSE|null|Null|NULL|~)(?=[ \\t]+#|[ \\t]*(?:\\r|\\n|,|\\[|\\]|\\{|\\}|:(?:[\\t\\n\\f\\r ]|,|\\[|\\]|\\{|\\}|$))|$)"
235238
},
236239
"plain": {
237240
"name": "string.unquoted.yaml",
@@ -266,6 +269,14 @@
266269
}
267270
}
268271
},
272+
"explicit-key-indicator": {
273+
"match": "(\\?)(?=[\\t ]*(?:#|$))",
274+
"captures": {
275+
"1": {
276+
"name": "punctuation.definition.map.key.yaml"
277+
}
278+
}
279+
},
269280
"blockscalar-key": {
270281
"begin": "(\\?)([\\t ]+)([|>](?:[1-9][-+]?|[-+][1-9]?|[-+])?)(?=[\\t ]*(?:#|$))(.*\\n?)",
271282
"beginCaptures": {

yaml.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import {
1111
token, rule, defineGrammar, alt, many, many1, opt, not, noCommentBefore, noMultilineFlowBefore,
1212
lit, seq, oneOf, noneOf, range, star, plus, repeat, followedBy, notFollowedBy,
13-
precededBy, notPrecededBy, never, end,
13+
precededBy, never, end,
1414
} from './src/api.ts';
1515
import type { IndentConfig } from './src/types.ts';
1616

@@ -166,28 +166,23 @@ const VALUE_END_BLOCK = followedBy(alt(
166166
seq(star(hspace), alt('\r', '\n', seq(':', alt(whitespace, end())))),
167167
end(),
168168
));
169-
// A NON-SPECIFIC tag (`!` followed by whitespace) forces its plain scalar to resolve as a STRING
170-
// regardless of the scalar's appearance (`! 12` is the string "12", not the number 12 — YAML 1.2
171-
// §6.9.1 / yaml-test-suite S4JQ). So the typed value tokens (Num / BoolNull) MUST NOT fire on a
172-
// scalar that is glued to a leading `!␣` tag — the negative lookbehind drops them so the scalar
173-
// falls through to the generic Plain (`string.unquoted`). A *specific* tag (`!!int 12`, `!!bool …`)
174-
// puts non-space chars between the `!` and the value, so this lookbehind leaves those untouched —
175-
// they keep resolving by appearance, matching what the `yaml` oracle reports. TextMate 2.0 Onigmo
176-
// rejects variable-length lookbehind, so this is a bounded set of fixed-width guards.
177-
const NONSPECIFIC_TAG = seq(...Array.from({ length: 16 }, (_, index) =>
178-
notPrecededBy(seq('!', repeat(hspace, index + 1, index + 1))),
179-
));
169+
// NOTE: a non-specific `!␣` tag forces its scalar to resolve as a STRING (`! 12` is "12"), which a
170+
// negative-lookbehind guard on Num / BoolNull used to honour. That guard is REMOVED (issue #12): a
171+
// negative lookbehind is rejected by TextMate 2.0 / GitHub-Linguist, so num/boolnull must stay
172+
// lookbehind-free to be portable. A `!␣`-tagged core value now types BY APPEARANCE — exactly what
173+
// the official grammar does (`! true` → constant.language.boolean there too) — a rare edge not in
174+
// the gradable corpus (the scope-gap metric is unchanged), traded for portable num/boolnull rules.
180175
// Numeric plain scalars (YAML 1.2 core schema): decimal / octal / hex integers, floats, ±.inf,
181176
// .nan. Anything outside the core schema (binary `0b…`, dates, `12:34:56`) stays a plain string,
182177
// matching what the `yaml` oracle resolves to a number.
183178
const sign = oneOf('+', '-');
184-
const NUM_BODY = seq(NONSPECIFIC_TAG, alt(
179+
const NUM_BODY = alt(
185180
seq(opt(sign), '.', alt(lit('inf'), 'Inf', 'INF')),
186181
seq('.', alt(lit('nan'), 'NaN', 'NAN')),
187182
seq('0x', plus(hexDigit)),
188183
seq('0o', plus(range('0', '7'))),
189184
seq(opt(sign), alt(seq('.', plus(digit)), seq(plus(digit), opt(seq('.', star(digit))))), opt(seq(oneOf('e', 'E'), opt(sign), plus(digit)))),
190-
));
185+
);
191186
// A typed plain scalar is FIRST a plain scalar (lexically an unquoted string) that the schema then
192187
// resolves to a number / boolean / null — so it carries a `string.unquoted` ancestor with the
193188
// `constant.*` leaf, exactly as the official grammar nests `string.unquoted.plain.out` › `constant`.
@@ -199,9 +194,9 @@ const Num = token(
199194
seq(NUM_BODY, VALUE_END),
200195
{ scope: 'string.unquoted constant.numeric', blockPattern: seq(NUM_BODY, VALUE_END_BLOCK) },
201196
);
202-
// Boolean / null plain scalars (core schema). Same non-specific-tag guard:
203-
// `! true` is the string "true", not the boolean (yaml-test-suite cousins of S4JQ).
204-
const BOOLNULL_BODY = seq(NONSPECIFIC_TAG, alt(lit('true'), 'True', 'TRUE', 'false', 'False', 'FALSE', 'null', 'Null', 'NULL', '~'));
197+
// Boolean / null plain scalars (core schema). Types by appearance; a `!␣`-tagged value is no longer
198+
// special-cased (see the NUM_BODY note above — the lookbehind guard was removed for portability).
199+
const BOOLNULL_BODY = alt(lit('true'), 'True', 'TRUE', 'false', 'False', 'FALSE', 'null', 'Null', 'NULL', '~');
205200
const BoolNull = token(
206201
seq(BOOLNULL_BODY, VALUE_END),
207202
{ scope: 'string.unquoted constant.language', blockPattern: seq(BOOLNULL_BODY, VALUE_END_BLOCK) },

0 commit comments

Comments
 (0)