Skip to content

Commit 156e3a5

Browse files
committed
fix(daemon): parse sequence mappings by unquoted colon
Detect single-line sequence mappings only when the colon is outside quotes and followed by whitespace or end-of-string. This keeps quoted trigger phrases like "1:1 figma" as scalars while preserving quoted-key mappings such as - "name": "foo".
1 parent 56b8026 commit 156e3a5

4 files changed

Lines changed: 91 additions & 10 deletions

File tree

apps/daemon/src/design-systems/frontmatter.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,13 @@ function parseYamlSubset(src: string): FrontmatterObject {
7979
continue;
8080
}
8181
}
82-
// Quoted scalars keep their colons (e.g. `- "1:1 figma"`). Only unquoted
83-
// `key: value` items are single-line mapping entries in a sequence.
84-
if (value.includes(':') && !isQuotedScalar(value)) {
82+
// Single-line sequence mappings use a colon outside quotes (e.g. `- k: v`,
83+
// `- "name": "foo"`). Colons inside quoted scalars (e.g. `- "1:1 figma"`)
84+
// must not open a mapping — skills catalogue triggers rely on that.
85+
const colonIdx = findSequenceMappingColon(value);
86+
if (colonIdx !== -1) {
8587
const obj: FrontmatterObject = {};
86-
const colonIdx = value.indexOf(':');
87-
const key = value.slice(0, colonIdx).trim();
88+
const key = unquoteScalar(value.slice(0, colonIdx).trim());
8889
const valRaw = value.slice(colonIdx + 1).trim();
8990
if (valRaw) obj[key] = coerce(valRaw);
9091
if (!Array.isArray(container)) throw new Error('frontmatter array container expected');
@@ -205,6 +206,37 @@ function isQuotedScalar(raw: string): boolean {
205206
);
206207
}
207208

209+
/** Strip surrounding quotes from a scalar; leave other text unchanged. */
210+
function unquoteScalar(raw: string): string {
211+
const v = raw.trim();
212+
return isQuotedScalar(v) ? v.slice(1, -1) : v;
213+
}
214+
215+
/**
216+
* Index of a single-line mapping separator in a sequence item, or -1.
217+
* Only colons outside quotes count, and the colon must be followed by
218+
* whitespace or end-of-string (this parser's dash-prefixed mapping subset).
219+
*/
220+
function findSequenceMappingColon(raw: string): number {
221+
let quote: '"' | "'" | null = null;
222+
for (let i = 0; i < raw.length; i++) {
223+
const ch = raw[i] ?? '';
224+
if (quote) {
225+
if (ch === quote) quote = null;
226+
continue;
227+
}
228+
if (ch === '"' || ch === "'") {
229+
quote = ch;
230+
continue;
231+
}
232+
if (ch === ':') {
233+
const next = raw[i + 1];
234+
if (next === undefined || /\s/.test(next)) return i;
235+
}
236+
}
237+
return -1;
238+
}
239+
208240
function coerce(raw: string | undefined): FrontmatterValue {
209241
if (raw === undefined) return '';
210242
let v = raw.trim();

apps/daemon/tests/frontmatter.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ describe('parseFrontmatter block sequences and arrays', () => {
100100
]);
101101
});
102102

103+
// Quoted keys must still form mappings (`- "name": "foo"`). A whole-value
104+
// isQuotedScalar gate would mis-classify this as a scalar because the item
105+
// both starts and ends with a quote.
106+
it('parses quoted-key sequence mappings', () => {
107+
expect(data('items:\n - "name": "foo"\n - \'id\': 1').items).toEqual([
108+
{ name: 'foo' },
109+
{ id: 1 },
110+
]);
111+
});
112+
103113
it('does not split inline-array elements on commas inside quotes', () => {
104114
expect(data('a: ["a,b", "c"]').a).toEqual(['a,b', 'c']);
105115
expect(data("a: ['x, y', z]").a).toEqual(['x, y', 'z']);

packages/plugin-runtime/src/parsers/frontmatter.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,13 @@ function parseYamlSubset(src: string): FrontmatterObject {
119119
continue;
120120
}
121121
}
122-
// Quoted scalars keep their colons (e.g. `- "1:1 figma"`). Only unquoted
123-
// `key: value` items are single-line mapping entries in a sequence.
124-
if (value.includes(':') && !isQuotedScalar(value)) {
122+
// Single-line sequence mappings use a colon outside quotes (e.g. `- k: v`,
123+
// `- "name": "foo"`). Colons inside quoted scalars (e.g. `- "1:1 figma"`)
124+
// must not open a mapping — skills catalogue triggers rely on that.
125+
const colonIdx = findSequenceMappingColon(value);
126+
if (colonIdx !== -1) {
125127
const obj: FrontmatterObject = {};
126-
const colonIdx = value.indexOf(':');
127-
const key = value.slice(0, colonIdx).trim();
128+
const key = unquoteScalar(value.slice(0, colonIdx).trim());
128129
const valRaw = value.slice(colonIdx + 1).trim();
129130
if (valRaw) obj[key] = coerce(valRaw);
130131
if (!Array.isArray(container)) throw new Error('frontmatter array container expected');
@@ -244,6 +245,37 @@ function isQuotedScalar(raw: string): boolean {
244245
);
245246
}
246247

248+
/** Strip surrounding quotes from a scalar; leave other text unchanged. */
249+
function unquoteScalar(raw: string): string {
250+
const v = raw.trim();
251+
return isQuotedScalar(v) ? v.slice(1, -1) : v;
252+
}
253+
254+
/**
255+
* Index of a single-line mapping separator in a sequence item, or -1.
256+
* Only colons outside quotes count, and the colon must be followed by
257+
* whitespace or end-of-string (this parser's dash-prefixed mapping subset).
258+
*/
259+
function findSequenceMappingColon(raw: string): number {
260+
let quote: '"' | "'" | null = null;
261+
for (let i = 0; i < raw.length; i++) {
262+
const ch = raw[i] ?? '';
263+
if (quote) {
264+
if (ch === quote) quote = null;
265+
continue;
266+
}
267+
if (ch === '"' || ch === "'") {
268+
quote = ch;
269+
continue;
270+
}
271+
if (ch === ':') {
272+
const next = raw[i + 1];
273+
if (next === undefined || /\s/.test(next)) return i;
274+
}
275+
}
276+
return -1;
277+
}
278+
247279
function coerce(raw: string | undefined): FrontmatterValue {
248280
if (raw === undefined) return '';
249281
const v = raw.trim();

packages/plugin-runtime/tests/parsers.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ describe('parseFrontmatter', () => {
189189
expect(data['items']).toEqual([{ k: 'v' }, { name: 'foo' }]);
190190
});
191191

192+
it('parses quoted-key sequence mappings', () => {
193+
const { data } = parseFrontmatter(
194+
'---\nitems:\n - "name": "foo"\n - \'id\': 1\n---\n',
195+
);
196+
expect(data['items']).toEqual([{ name: 'foo' }, { id: 1 }]);
197+
});
198+
192199
it('does not split inline-array elements on commas inside quotes', () => {
193200
const { data } = parseFrontmatter('---\na: ["a,b", "c"]\nb: [\'x, y\', z]\n---\n');
194201
expect(data['a']).toEqual(['a,b', 'c']);

0 commit comments

Comments
 (0)