Skip to content

Commit 372a49f

Browse files
committed
Highlight YAML invalid escapes (#5) and glued directive '#' (#8)
Two more monogram#12 items, highlighter-only: #5 — a double-quoted scalar's INVALID escape (`"quoted \' scalar"`: `\'` is not a valid YAML escape) was left as plain string content. The derived quoted-string region now emits an `invalid.illegal.constant.character.escape` pattern after the valid-escape pattern, so an unrecognised `\.` is highlighted (the valid escapes still win the leftmost tie). Only for backslash-escape strings, not doubled- delimiter (`''`) ones, where a lone `\` is literal. #8 — `%YAML 1.1#...`: the glued `#...` (no preceding space) was scoped as a comment. Per YAML §6.6 a `#` is a comment only at line start or after whitespace, so the Comment token gains a `notPrecededBy(nonWhitespace)` guard (a portable fixed-width `(?<!\S)`). Plain scalars already keep a glued `#` as content; this stops the Comment token from claiming a glued `#` a directive left behind. Parser is unaffected (src-coverage-yaml alignment still 100%) — the lexer skips comments via the indent config, so this only changes the highlighter's Comment pattern. Also un-flag the now-fixed regression cases (#5/#6/#7/#8/#9 drop `bug:true` → hard- gated; #6/#7/#9 were stale from the earlier fixes). yaml-issue12-regressions now 8 pass / 2 known-bug (#4, #10) / 0 regression. The other six grammars regenerate byte-identical; agnostic 9/9; sanity 15/15; RedCMD Onigmo diagnostics clean. Refs #12
1 parent 8762a7b commit 372a49f

4 files changed

Lines changed: 28 additions & 10 deletions

File tree

src/gen-tm.ts

Lines changed: 11 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);

test/yaml-issue12-regressions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,23 @@ export const cases: Issue12Case[] = [
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,

yaml.tmLanguage.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@
217217
},
218218
"comment": {
219219
"name": "comment.line.number-sign.yaml",
220-
"match": "#[^\\n]*"
220+
"match": "(?<![^\\t\\n\\f\\r ])#[^\\n]*"
221221
},
222222
"dquotekey": {
223223
"name": "entity.name.tag.yaml",
@@ -280,6 +280,10 @@
280280
{
281281
"match": "\\\\(?:[0abtnvfre\"/\\\\N_LP \\t]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|\\r?\\n)",
282282
"name": "constant.character.escape.yaml"
283+
},
284+
{
285+
"match": "\\\\.",
286+
"name": "invalid.illegal.constant.character.escape.yaml"
283287
}
284288
]
285289
},

yaml.ts

Lines changed: 7 additions & 2 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
altPattern, optPattern, seq, oneOf, noneOf, range, star, plus, repeat, followedBy, notFollowedBy,
13-
precededBy, never, end,
13+
precededBy, notPrecededBy, never, end,
1414
} from './src/api.ts';
1515
import type { IndentConfig } from './src/types.ts';
1616

@@ -38,7 +38,12 @@ const hashAfterNonSpace = seq('#', precededBy(seq(nonWhitespace, '#')));
3838
const docMarkerEnd = followedBy(altPattern(oneOf('\t', ' '), '\r', '\n', end()));
3939
const DocStart = token(seq('---', docMarkerEnd), { scope: 'entity.other.document.begin' });
4040
const DocEnd = token(seq('...', docMarkerEnd), { scope: 'entity.other.document.end' });
41-
const Comment = token(seq('#', star(noneOf('\n'))), { skip: true, scope: 'comment.line.number-sign' });
41+
// A `#` is a comment indicator only at line start or AFTER whitespace (YAML §6.6); a `#` glued to a
42+
// non-space char is content, not a comment (`a#b` is a plain scalar, `%YAML 1.1#…` keeps the `#…` as
43+
// directive content — monogram#12 #8). The `notPrecededBy(nonWhitespace)` guard (a fixed-width, portable
44+
// `(?<!\S)`) enforces that. Plain scalars already keep a glued `#` via `hashAfterNonSpace`; this stops
45+
// the Comment token from claiming a glued `#` that a non-scalar token (a directive) left behind.
46+
const Comment = token(seq(notPrecededBy(nonWhitespace), '#', star(noneOf('\n'))), { skip: true, scope: 'comment.line.number-sign' });
4247
// Double-quoted scalar body. The escape set is FIXED (YAML 1.2 §5.7): a `\` may only precede one
4348
// of `0 a b t n v f r e " / \ N _ L P`, a literal space/tab, a `x`+2 / `u`+4 / `U`+8 hex escape, or
4449
// a LINE BREAK (`\`-at-EOL = line continuation). Any other `\.` (`\.`, `\'`, `\q`, `\x4`) is illegal

0 commit comments

Comments
 (0)