import { findNodeAtLocation, Node, parseTree } from 'jsonc-parser'; import { isMap, isScalar, isSeq, parseDocument } from 'yaml'; export interface OffsetRange { start: number; end: number; } /** * The JSON pointer an Archify diagnostic is about. Most diagnostics carry it * in subject.path; brand diagnostics only start their message with it. */ export function diagnosticPointer(diagnostic: { message: string; subject?: { path?: string } }): string | undefined { return diagnostic.subject?.path ?? /^(\/[^\s"]+)\s/.exec(diagnostic.message)?.[1]; } /** Splits an RFC 6901 JSON pointer into path segments. */ export function pointerSegments(pointer: string): (string | number)[] { if (!pointer || pointer === '/') return []; return pointer .replace(/^\//, '') .split('/') .map((raw) => raw.replace(/~1/g, '/').replace(/~0/g, '~')) .map((segment) => (/^(0|[1-9]\d*)$/.test(segment) ? Number(segment) : segment)); } /** * Maps a JSON pointer to a text range. Objects and arrays resolve to their * property key (or opening bracket) so a "missing property" error does not * underline a whole block. Unresolvable paths fall back to the deepest * ancestor that exists. */ export function rangeForPointer(text: string, pointer: string | undefined): OffsetRange { const root = parseTree(text, [], { allowTrailingComma: true }); if (!root) return { start: 0, end: 0 }; const segments = pointerSegments(pointer ?? ''); let node: Node | undefined; for (let length = segments.length; length >= 0 && !node; length -= 1) { node = findNodeAtLocation(root, segments.slice(0, length)); } if (!node) return { start: 0, end: 0 }; if (node.type === 'object' || node.type === 'array') { const key = node.parent?.type === 'property' ? node.parent.children?.[0] : undefined; if (key) return { start: key.offset, end: key.offset + key.length }; return { start: node.offset, end: node.offset + 1 }; } return { start: node.offset, end: node.offset + node.length }; } /** The same mapping for YAML text (`.archify` files). */ export function rangeForPointerYaml(text: string, pointer: string | undefined): OffsetRange { const doc = parseDocument(text, { keepSourceTokens: false, uniqueKeys: false }); let node: unknown = doc.contents; let keyRange: [number, number] | undefined; for (const segment of pointerSegments(pointer ?? '')) { if (isMap(node)) { const pair = node.items.find((item) => isScalar(item.key) && String(item.key.value) === String(segment)); if (!pair) break; keyRange = isScalar(pair.key) && pair.key.range ? [pair.key.range[0], pair.key.range[1]] : undefined; node = pair.value; } else if (isSeq(node) && typeof segment === 'number' && segment < node.items.length) { keyRange = undefined; node = node.items[segment]; } else { break; } } if ((isMap(node) || isSeq(node)) && keyRange) return { start: keyRange[0], end: keyRange[1] }; const range = (node as { range?: [number, number, number] } | null)?.range; if (!range) return { start: 0, end: 0 }; if (isMap(node) || isSeq(node)) return { start: range[0], end: Math.min(range[0] + 1, range[1]) }; return { start: range[0], end: range[1] }; } export interface SyntaxProblem extends OffsetRange { message: string; } /** YAML syntax errors with their offsets (none for valid YAML). */ export function yamlSyntaxProblems(text: string): SyntaxProblem[] { const doc = parseDocument(text, { uniqueKeys: false }); return doc.errors.map((error) => ({ start: error.pos[0], end: Math.max(error.pos[1], error.pos[0] + 1), message: error.message.split('\n')[0], })); }