Skip to content

Commit c404452

Browse files
Highlight the remaining YAML monogram#12 items (#4/#5/#8/#10) (#21)
1 parent 8762a7b commit c404452

4 files changed

Lines changed: 1325 additions & 105 deletions

File tree

src/gen-tm.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4582,17 +4582,26 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
45824582
// JS/TS strings declare an @escape and take the escapePattern branch above, so they are
45834583
// untouched; this fires only for the otherwise-flat quoted-string token.
45844584
const { delim, escape } = tokenPatternQuoteDelimAndEscape(tok)!;
4585+
const isDoubledDelim = escape === escapeRegex(delim) + escapeRegex(delim);
4586+
// A BACKSLASH-based escape string (e.g. YAML double-quoted): the valid escapes are scoped
4587+
// constant.character.escape; any OTHER `\.` is an INVALID escape and must still be highlighted
4588+
// (monogram#12 #5 — `"quoted \' scalar"`: `\'` is not a valid YAML escape but must not read as
4589+
// plain string content). The invalid catch is listed AFTER the valid pattern (same start → the
4590+
// valid escape wins the tie; only an unrecognised `\.` falls to it). NOT added for a doubled-
4591+
// delimiter escape (`''`), where a lone `\` is literal content, not an escape.
4592+
const escapePatterns: TmPattern[] = [{ match: escape, name: `constant.character.escape.${langName}` }];
4593+
if (!isDoubledDelim) escapePatterns.push({ match: '\\\\.', name: `invalid.illegal.constant.character.escape.${langName}` });
45854594
const region: TmPattern = {
45864595
name: `${scope}.${langName}`,
45874596
begin: escapeRegex(delim),
45884597
beginCaptures: { '0': { name: `punctuation.definition.string.begin.${langName}` } },
45894598
end: escapeRegex(delim),
45904599
endCaptures: { '0': { name: `punctuation.definition.string.end.${langName}` } },
4591-
patterns: [{ match: escape, name: `constant.character.escape.${langName}` }],
4600+
patterns: escapePatterns,
45924601
};
45934602
// A doubled-delimiter escape (`''`) shares its first char with the region's `end`, so the
45944603
// escape pattern must be tried BEFORE the end (else `'a''b'` closes at the inner pair).
4595-
if (escape === escapeRegex(delim) + escapeRegex(delim)) region.applyEndPatternLast = true;
4604+
if (isDoubledDelim) region.applyEndPatternLast = true;
45964605
repository[key] = region;
45974606
topPatterns.push({ include: `#${key}` });
45984607
rememberLiteralKey(scope, key, tok.name);
@@ -4785,6 +4794,18 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
47854794
while: '\\G',
47864795
patterns: funkyBody(contentScope),
47874796
});
4797+
// The `|N`/`>N` indentation indicator (a digit with optional chomping in either order). For the
4798+
// EXPLICIT-indent block scalars (§2a‴) the digit is LITERAL (one region per digit), so the floor is
4799+
// known and the body needs no funky auto-detect: a simpler inner rule opens at the header EOL and
4800+
// paints every line the outer `\1 {N}` bound admits as block content via `contentName` (a deeper
4801+
// key-/comment-shaped body line is therefore NOT re-scoped — it is opaque block string).
4802+
const bsDigitAlt = (n: number) => `(?:${n}[-+]?|[-+]${n})`;
4803+
const bsExplicitIntroRule = (n: number) => ({
4804+
begin: `[\\t ]*(${introClass}${bsDigitAlt(n)})(?=[\\t ]*(?:#|$))([\\t ]*.*)`,
4805+
beginCaptures: { '1': { name: bsIndicator }, '2': { patterns: commentIncs } },
4806+
while: '\\G',
4807+
contentName: bsContent,
4808+
});
47884809
// Header-prefix token includes: re-scope the part of the header line BEFORE the introducer (a doc
47894810
// marker / key / `:` / anchor / tag), since the line-start indent consume swallowed the engine
47904811
// position past them. Derived from what this grammar actually emits (an unresolved include is a
@@ -4864,6 +4885,37 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
48644885
});
48654886
topPatterns.push({ include: `#${bsKey}-seq` });
48664887

4888+
// ── 2a‴. EXPLICIT-indent block scalars (`|N` / `>N`, monogram#12 #10) ──
4889+
// An explicit indentation indicator (`|5`) PINS the content indent at parent+N, OVERRIDING the
4890+
// funky body's auto-detect (which floors at the FIRST content line's indent). With `abc: |5` whose
4891+
// first body line is at column 6, auto-detect floors at 6, so a real body line at column 5
4892+
// (`# string 5`) is then SHALLOWER than the detected floor and RELEASED — re-scanned as a comment.
4893+
// The fix pins the floor to parent+N. TextMate cannot use a CAPTURED digit as a repeat count
4894+
// portably (RedCMD does, via Oniguruma `{\N}` backref-as-count + conditionals + subroutines — all
4895+
// rejected by Onigmo / GitHub-Linguist), so the only portable spelling is a region per digit with a
4896+
// LITERAL `{N}` count. Same structure as the auto-detect block scalars (forward-captured node indent
4897+
// + an inner introducer rule that opens the body at the header EOL); only two things change: the
4898+
// `while` bound is `\1 {N}` (parent+N) instead of `\1[ \t]` (parent+1), and the inner rule paints
4899+
// the body via `contentName` instead of the funky auto-detect (the floor is already known). Emitted
4900+
// for digits 1–9 in both value position (`key: |N`, nested, and doc-root `|N` / `--- |N` — `bsVp`
4901+
// admits the optional `---` / key / properties) and sequence position (`- a: |N`, whose floor adds
4902+
// the dash column via `\3` — same as `-seq`). Ranked above the auto-detect variants (scopeOrder).
4903+
for (let n = 1; n <= 9; n++) {
4904+
repository[`${bsKey}-explicit-${n}`] = emitIndentRegion({
4905+
lookahead: `(?=${bsVp}${introClass}${bsDigitAlt(n)}[\\t ]*(?:#|$))`,
4906+
cont: `\\1 {${n}}`,
4907+
patterns: [bsExplicitIntroRule(n), ...bsHeaderIncs],
4908+
});
4909+
topPatterns.push({ include: `#${bsKey}-explicit-${n}` });
4910+
repository[`${bsKey}-explicit-seq-${n}`] = emitIndentRegion({
4911+
lookahead: `(-)([ \\t]+)(?=(?:[-?][\\t ]+)*[^\\n]*?:[\\t ]+${bsProp}${introClass}${bsDigitAlt(n)}[\\t ]*(?:#|$))`,
4912+
beginCaptures: { '2': { name: `punctuation.${langName}` } },
4913+
cont: `\\1[ \\t]\\3 {${n}}`,
4914+
patterns: [bsExplicitIntroRule(n), ...bsHeaderIncs],
4915+
});
4916+
topPatterns.push({ include: `#${bsKey}-explicit-seq-${n}` });
4917+
}
4918+
48674919
// ── 2a′. Multi-line PLAIN scalar continuation (monogram#12 §6/§7) ──
48684920
// A plain scalar may FOLD across a more-indented continuation line (`key: a\n b` → "a b";
48694921
// `? e\n 42` → the key "e 42"). The parser's lexer folds these into one scalar token, but a
@@ -5067,6 +5119,31 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
50675119
}
50685120
}
50695121

5122+
// ── 2b′. Malformed directive line (monogram#12 #4) ──
5123+
// A directive owns its whole line (§6.8): `%YAML 1.2 foo` is an ILLEGAL directive (bad arity), and
5124+
// the parser rejects it — YamlDirective's arity lookahead fails and the generic Directive excludes
5125+
// the `%YAML␣` prefix, so NEITHER token matches and the trailing `foo` falls through to the plain-
5126+
// scalar tokens, which paint it as a stray `string.unquoted`. But a `%` can never BEGIN a plain
5127+
// scalar (YAML §7.3.3 — `%` is a c-indicator, excluded from ns-plain-first), so a `%`-led line the
5128+
// clean directive tokens did NOT claim is always a malformed directive, never real scalar content.
5129+
// Re-scope the whole line as an invalid directive. The indicator (`%`) is read from the directive
5130+
// tokens' leading literal (never hardcoded); ranked just BELOW the clean directives and ABOVE the
5131+
// plain scalars (scopeOrder 6.5) so it only catches what they left and beats the stray-scalar mis-
5132+
// scope. Highlight-only — the parser still rejects the line. The `^` anchor pins it to a line-start
5133+
// `%` (an indented `%` mid-line — e.g. a `key: %v` value — is left to the scalar tokens).
5134+
const directiveToks = grammar.tokens.filter(t => /(^|\.)keyword\.other\.directive(\.|$)/.test(t.scope ?? ''));
5135+
if (directiveToks.length) {
5136+
const lead = directiveToks.map(t => tokenPatternLeadingSource(t)).find((s): s is string => !!s);
5137+
const indicator = lead ? [...lead][0] : '';
5138+
if (indicator) {
5139+
repository['directive-malformed'] = {
5140+
match: `^[ \\t]*(${escapeRegex(indicator)}[^\\n]*?)[\\t ]*$`,
5141+
captures: { '1': { name: `invalid.illegal.keyword.other.directive.${langName}` } },
5142+
};
5143+
topPatterns.push({ include: '#directive-malformed' });
5144+
}
5145+
}
5146+
50705147
// ── 2c. Flow collections (`{ … }` mapping / `[ … ]` sequence) as nested begin/end regions ──
50715148
// A flat token grammar mis-scopes a flow mapping's keys (the enclosing bracket — invisible to a
50725149
// context-free token — decides whether an entry-leading scalar is a key or a sequence value).
@@ -7552,6 +7629,14 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
75527629
// (those carry a leading `-`/`?` its lookahead forbids).
75537630
// • the plain `blockscalar` is the fallback (bare `|`, `key: |`, `--- |` with an indented body).
75547631
const bsRank = grammar.indent?.blockScalar?.token.toLowerCase();
7632+
// EXPLICIT-indent block scalars (`|N`, §2a‴) must out-rank their auto-detect counterparts so a
7633+
// `|N` header takes the digit-aware floor. The sequence variant (`- a: |N`) ranks above the value
7634+
// variant: a `- …: |N` line matches BOTH (the value `bsVp` admits a leading `- `), and only the
7635+
// sequence floor adds the dash column, so it must win. Both stay below `blockscalar-key` (0.55) so a
7636+
// `? |N` explicit-key block scalar still scopes its `?` as the map key. (`-explicit-seq-` is tested
7637+
// before `-explicit-` because the seq keys also start with the value prefix.)
7638+
if (bsRank && key.startsWith(`${bsRank}-explicit-seq-`)) return 0.45;
7639+
if (bsRank && key.startsWith(`${bsRank}-explicit-`)) return 0.57;
75557640
if (bsRank && key === `${bsRank}-seq`) return 0.5;
75567641
if (key === 'blockscalar-key') return 0.55;
75577642
if (bsRank && key === `${bsRank}-doc`) return 0.58;
@@ -7614,6 +7699,11 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
76147699
if (scope.includes('constant.numeric')) return 3; // stable sort preserves DSL token order
76157700
if (scope.includes('keyword.operator') && key.startsWith('scope-')) return 4;
76167701
if (scope.includes('keyword.control')) return 5;
7702+
// A malformed-directive fallback (monogram#12 #4) is scoped keyword.other.directive, so it would
7703+
// otherwise tie the CLEAN directive tokens at 6. Rank it just below them and below constant.language
7704+
// (7) — so a well-formed directive still wins — but ABOVE the plain scalars (string.unquoted 8.8) so
7705+
// it claims a `%`-led line the clean tokens left, beating the stray-scalar mis-scope it exists to fix.
7706+
if (key === 'directive-malformed') return 6.5;
76177707
if (scope.includes('storage.') || scope.includes('keyword.other')) return 6;
76187708
if (scope.includes('constant.language')) return 7;
76197709
if (scope.includes('variable.language')) return 7.5;

test/yaml-issue12-regressions.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,31 +53,31 @@ export const cases: Issue12Case[] = [
5353

5454
{ id: '#4', title: '`%YAML 1.2 foo` — the trailing param is part of the directive line',
5555
src: '%YAML 1.2 foo\n---\n', at: 'foo', col: 0,
56-
should: (s) => isDirectiveish(s), why: 'CST emits one `directive` token `%YAML 1.2 foo`; the trailing token is directive content, not a stray plain string.unquoted scalar', bug: true },
56+
should: (s) => isDirectiveish(s), why: 'CST emits one `directive` token `%YAML 1.2 foo`; the trailing token is directive content, not a stray plain string.unquoted scalar' },
5757

5858
{ id: '#5', title: 'an escape inside a double-quoted scalar is highlighted',
5959
src: 'double: "quoted \\\' scalar"\n', at: "\\'", col: 0,
60-
should: (s) => isEscapeOrInvalid(s), why: '`\\\'` in a double-quoted scalar is an (invalid) escape; it should be constant.character.escape or invalid, not plain string', bug: true },
60+
should: (s) => isEscapeOrInvalid(s), why: '`\\\'` in a double-quoted scalar is an (invalid) escape; it should be constant.character.escape or invalid, not plain string' },
6161

6262
{ id: '#6', title: 'a `!` opening a plain-scalar CONTINUATION line is string content, not a tag',
6363
src: 'safe: a!"#$%&\'()*+,-./09:;<=>?@AZ\n !"#$%&\'()*+,-./09:;<=>?@AZ\n', at: ' !', col: 5,
64-
should: (s) => isString(s), why: 'the 2nd line folds into the one multi-line plain scalar (CST: one `scalar` token); the leading `!` is content, not storage.type.tag', bug: true },
64+
should: (s) => isString(s), why: 'the 2nd line folds into the one multi-line plain scalar (CST: one `scalar` token); the leading `!` is content, not storage.type.tag' },
6565

6666
{ id: '#7', title: '`42` inside a multi-line plain-scalar KEY is string content, not a number',
6767
src: '? a\n true\n: null\n d\n? e\n 42\n', at: ' 42', col: 2,
68-
should: (s) => notNumber(s), why: 'the explicit key is the plain scalar "e 42" (CST/AST: one scalar that resolves to the STRING "e 42"); `42` is not a numeric literal', bug: true },
68+
should: (s) => notNumber(s), why: 'the explicit key is the plain scalar "e 42" (CST/AST: one scalar that resolves to the STRING "e 42"); `42` is not a numeric literal' },
6969

7070
{ id: '#8', title: '`#...` immediately after `%YAML 1.1` is directive content, not a comment',
7171
src: '%YAML 1.1#...\n', at: '#...', col: 0,
72-
should: (s) => notComment(s), why: 'no whitespace precedes the `#`, so it is not a comment; CST emits one `directive` token `%YAML 1.1#...`', bug: true },
72+
should: (s) => notComment(s), why: 'no whitespace precedes the `#`, so it is not a comment; CST emits one `directive` token `%YAML 1.1#...`' },
7373

7474
{ id: '#9', title: '`%YAML` after document content is plain-scalar text, not a directive',
7575
src: '---\nscalar\n%YAML 1.2\n', at: '%YAML', col: 0,
76-
should: (s) => notDirective(s) && isString(s), why: 'a directive cannot appear after content; `scalar\\n%YAML 1.2` folds into one plain scalar (CST: one `scalar` token)', bug: true },
76+
should: (s) => notDirective(s) && isString(s), why: 'a directive cannot appear after content; `scalar\\n%YAML 1.2` folds into one plain scalar (CST: one `scalar` token)' },
7777

7878
{ id: '#10', title: 'a `#` line inside a `|5` block scalar body is string content, not a comment',
7979
src: 'abc: |5\n # string 6\n # string 5\n #comment 4\n #comment 3\n', at: '# string 5', col: 0,
80-
should: (s) => isBlockString(s), why: 'with explicit indent 5, the `# string 5` line (indent 5) is block-scalar body; CST puts it inside the `block-scalar`, only `#comment 4` (indent 4 < 5) ends it', bug: true },
80+
should: (s) => isBlockString(s), why: 'with explicit indent 5, the `# string 5` line (indent 5) is block-scalar body; CST puts it inside the `block-scalar`, only `#comment 4` (indent 4 < 5) ends it' },
8181
];
8282

8383
// ── runner ────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)