|
5 | 5 | * LICENSE file in the root directory of this source tree. |
6 | 6 | */ |
7 | 7 |
|
8 | | -const INLINE_HIGHLIGHT_REGEX = /\[\[(.*?)\]\]/g; |
| 8 | +const INLINE_HIGHLIGHT_REGEX = /\[\[.*\]\]/; |
9 | 9 |
|
10 | | -function validateInlineHighlights(meta, code) { |
11 | | - const matches = [...meta.matchAll(INLINE_HIGHLIGHT_REGEX)]; |
12 | | - if (matches.length === 0) { |
| 10 | +function isHighlightTuple(entry) { |
| 11 | + return ( |
| 12 | + Array.isArray(entry) && |
| 13 | + entry.length >= 3 && |
| 14 | + Number.isInteger(entry[0]) && |
| 15 | + Number.isInteger(entry[1]) && |
| 16 | + typeof entry[2] === 'string' |
| 17 | + ); |
| 18 | +} |
| 19 | + |
| 20 | +function parseHighlightGroups(meta) { |
| 21 | + const match = meta.match(INLINE_HIGHLIGHT_REGEX); |
| 22 | + if (match === null) { |
| 23 | + // No [[...]] inline highlight marker present (e.g. metadata-only fences |
| 24 | + // like ```js {expectedErrors: ...}) — nothing to validate here. |
13 | 25 | return []; |
14 | 26 | } |
15 | | - |
16 | | - const errors = []; |
17 | | - let highlights = []; |
18 | | - for (const match of matches) { |
19 | | - let parsed; |
20 | | - try { |
21 | | - // The fence marker [[...]] is not itself valid JSON, so wrap the |
22 | | - // captured content in brackets to parse it as an array. This makes |
23 | | - // both [1, 1, "text"] and [ [1, 1, "text"] ] (nested form) parse |
24 | | - // into a three-element highlight tuple. |
25 | | - parsed = JSON.parse(`[${match[1]}]`); |
26 | | - } catch (error) { |
27 | | - errors.push('Code highlight metadata must be valid JSON'); |
28 | | - continue; |
29 | | - } |
30 | | - if (!Array.isArray(parsed)) { |
31 | | - errors.push('Code highlight metadata must be an array'); |
32 | | - continue; |
33 | | - } |
34 | | - // A single group may either wrap the list of highlights |
35 | | - // ([[ [1, 1, "a"], [2, 3, "b"] ]] style) or be one bare |
36 | | - // three-element tuple ([1, 1, "a"] style). |
37 | | - if (parsed.length > 0 && parsed.every(entry => Array.isArray(entry))) { |
38 | | - highlights = highlights.concat(parsed); |
39 | | - } else { |
40 | | - highlights.push(parsed); |
| 27 | + let parsed; |
| 28 | + try { |
| 29 | + parsed = JSON.parse(match[0]); |
| 30 | + } catch (error) { |
| 31 | + return null; |
| 32 | + } |
| 33 | + if (!Array.isArray(parsed)) { |
| 34 | + return null; |
| 35 | + } |
| 36 | + // Normalize accepted shapes: |
| 37 | + // - list of highlight tuples: [[step, line, text], ...] |
| 38 | + // - nested single tuple: [[[step, line, text]]] |
| 39 | + // - single bare tuple: [step, line, text] |
| 40 | + if (parsed.length === 1 && Array.isArray(parsed[0])) { |
| 41 | + const inner = parsed[0]; |
| 42 | + if (isHighlightTuple(inner)) { |
| 43 | + // Nested form wrapping exactly one tuple. |
| 44 | + return [inner]; |
41 | 45 | } |
42 | 46 | } |
| 47 | + if (parsed.length > 0 && parsed.every(isHighlightTuple)) { |
| 48 | + return parsed; |
| 49 | + } |
| 50 | + return [parsed]; |
| 51 | +} |
| 52 | + |
| 53 | +function validateInlineHighlights(meta, code) { |
| 54 | + const groups = parseHighlightGroups(meta); |
| 55 | + if (groups === null) { |
| 56 | + return ['Code highlight metadata must be valid JSON']; |
| 57 | + } |
| 58 | + if (groups.length === 0) { |
| 59 | + return []; |
| 60 | + } |
| 61 | + const errors = []; |
| 62 | + const highlights = groups; |
43 | 63 |
|
44 | 64 | const lines = code.split('\n'); |
45 | 65 |
|
|
0 commit comments