From 80fa60c6b24b78e668adf79503314a1cf735bc49 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 13:45:42 -0700 Subject: [PATCH 01/18] starting work towards yaml validator in monorepo --- packages/annotated-json/package.json | 15 + packages/annotated-json/src/index.ts | 25 ++ packages/annotated-json/src/types.ts | 33 ++ packages/annotated-json/tsconfig.json | 5 + packages/mapped-string/package.json | 14 + packages/mapped-string/src/error.ts | 61 +++ packages/mapped-string/src/glb.ts | 70 +++ packages/mapped-string/src/index.ts | 27 ++ packages/mapped-string/src/located-error.ts | 33 ++ packages/mapped-string/src/mapped-text.ts | 444 ++++++++++++++++++++ packages/mapped-string/src/ranged-text.ts | 93 ++++ packages/mapped-string/src/text.ts | 285 +++++++++++++ packages/mapped-string/src/types.ts | 58 +++ packages/mapped-string/tsconfig.json | 5 + yarn.lock | 5 + 15 files changed, 1173 insertions(+) create mode 100644 packages/annotated-json/package.json create mode 100644 packages/annotated-json/src/index.ts create mode 100644 packages/annotated-json/src/types.ts create mode 100644 packages/annotated-json/tsconfig.json create mode 100644 packages/mapped-string/package.json create mode 100644 packages/mapped-string/src/error.ts create mode 100644 packages/mapped-string/src/glb.ts create mode 100644 packages/mapped-string/src/index.ts create mode 100644 packages/mapped-string/src/located-error.ts create mode 100644 packages/mapped-string/src/mapped-text.ts create mode 100644 packages/mapped-string/src/ranged-text.ts create mode 100644 packages/mapped-string/src/text.ts create mode 100644 packages/mapped-string/src/types.ts create mode 100644 packages/mapped-string/tsconfig.json diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json new file mode 100644 index 00000000..055b7175 --- /dev/null +++ b/packages/annotated-json/package.json @@ -0,0 +1,15 @@ +{ + "name": "annotated-json", + "version": "0.1.0", + "private": true, + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "mapped-string": "*", + "tsconfig": "*", + "typescript": "^5.4.2" + }, + "devDependencies": { } +} + diff --git a/packages/annotated-json/src/index.ts b/packages/annotated-json/src/index.ts new file mode 100644 index 00000000..efd5bfc7 --- /dev/null +++ b/packages/annotated-json/src/index.ts @@ -0,0 +1,25 @@ +/* + * index.ts + * + * Copyright (C) 2024 by Posit Software, PBC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export * from "./types"; \ No newline at end of file diff --git a/packages/annotated-json/src/types.ts b/packages/annotated-json/src/types.ts new file mode 100644 index 00000000..31ae18a8 --- /dev/null +++ b/packages/annotated-json/src/types.ts @@ -0,0 +1,33 @@ +/* + * types.ts + * + * Types for annotated-json. + * + * Copyright (C) 2024 by Posit Software, PBC + * + */ + +import { MappedString } from "mapped-string"; + +// https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 +export type JSONValue = + | string + | number + | boolean + | null + | JSONValue[] + | { [key: string]: JSONValue }; + + +// AnnotatedParse annotates a JSONValue with textual spans and +// components +export interface AnnotatedParse { + start: number; + end: number; + result: JSONValue; + kind: string; + source: MappedString; + components: AnnotatedParse[]; + + errors?: { start: number; end: number; message: string }[]; // this field is only populated at the top level +} \ No newline at end of file diff --git a/packages/annotated-json/tsconfig.json b/packages/annotated-json/tsconfig.json new file mode 100644 index 00000000..aad794c2 --- /dev/null +++ b/packages/annotated-json/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["node_modules"], + "extends": "tsconfig/base.json", + "include": ["src"] +} diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json new file mode 100644 index 00000000..5ad19600 --- /dev/null +++ b/packages/mapped-string/package.json @@ -0,0 +1,14 @@ +{ + "name": "mapped-string", + "version": "0.1.0", + "private": true, + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "tsconfig": "*", + "typescript": "^5.4.2" + }, + "devDependencies": { } +} + diff --git a/packages/mapped-string/src/error.ts b/packages/mapped-string/src/error.ts new file mode 100644 index 00000000..ae6e6018 --- /dev/null +++ b/packages/mapped-string/src/error.ts @@ -0,0 +1,61 @@ +/* + * error.ts + * + * Copyright (C) 2020-2024 Posit Software, PBC + */ + +export class InternalError extends Error { + constructor( + message: string, + printName = true, + printStack = true, + ) { + super(message); + this.name = "Internal Error"; + this.printName = printName; + this.printStack = printStack; + } + + public readonly printName: boolean; + public readonly printStack: boolean; +} + +export class UnreachableError extends InternalError { + constructor() { + super("Unreachable code was reached.", true, true); + } +} + +export class ErrorEx extends Error { + constructor( + name: string, + message: string, + printName = true, + printStack = true, + ) { + super(message); + this.name = name; + this.printName = printName; + this.printStack = printStack; + } + + public readonly printName: boolean; + public readonly printStack: boolean; +} + +export function asErrorEx(e: unknown) { + if (e instanceof ErrorEx) { + return e; + } else if (e instanceof Error) { + // amend this error rather than creating a new ErrorEx + // so that the stack trace survives + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (e as any).printName = e.name !== "Error"; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (e as any).printStack = !!e.message; + return e as ErrorEx; + } else { + return new ErrorEx("Error", String(e), false, true); + } +} diff --git a/packages/mapped-string/src/glb.ts b/packages/mapped-string/src/glb.ts new file mode 100644 index 00000000..6a34a59c --- /dev/null +++ b/packages/mapped-string/src/glb.ts @@ -0,0 +1,70 @@ +/* +* binary-search.ts +* +* Copyright (C) 2021-2024 Posit Software, PBC +* +*/ + +export function glb( + array: T[], + value: U, + compare?: (a: U, b: T) => number, +) { + compare = compare || + ((a: unknown, b: unknown) => (a as number) - (b as number)); + if (array.length === 0) { + return -1; + } + if (array.length === 1) { + if (compare(value, array[0]) < 0) { + return -1; + } else { + return 0; + } + } + + let left = 0; + let right = array.length - 1; + const vLeft = array[left], vRight = array[right]; + + if (compare(value, vRight) >= 0) { + // pre: value >= vRight + return right; + } + if (compare(value, vLeft) < 0) { + // pre: value < vLeft + return -1; + } + + // pre: compare(value, vRight) === -1 => value < vRight + // pre: compare(value, vLeft) === {0, 1} => compare(vLeft, value) === {-1, 0} => vLeft <= value + // pre: vLeft <= value < vRight + + while (right - left > 1) { + // pre: right - left > 1 => ((right - left) >> 1) > 0 + // pre: vLeft <= value < vRight (from before while start and end of while loop) + + const center = left + ((right - left) >> 1); + const vCenter = array[center]; + const cmp = compare(value, vCenter); + + if (cmp < 0) { + right = center; + // vRight = vCenter + // pre: value < vCenter + // pre: value < vRight + } else if (cmp === 0) { + // pre: value === center => center <= value + left = center; + // vLeft = vCenter + // pre: vLeft <= value + } else { + // pre: cmp > 0 + // pre: value > center => center < value => center <= value + left = center; + // pre: vLeft <= value + } + // pre: vLeft <= value < vRight + } + return left; +} diff --git a/packages/mapped-string/src/index.ts b/packages/mapped-string/src/index.ts new file mode 100644 index 00000000..7c34a002 --- /dev/null +++ b/packages/mapped-string/src/index.ts @@ -0,0 +1,27 @@ +/* + * index.ts + * + * Copyright (C) 2024 by Posit Software, PBC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export * from "./ranged-text"; +export * from "./mapped-text"; +export * from "./types"; \ No newline at end of file diff --git a/packages/mapped-string/src/located-error.ts b/packages/mapped-string/src/located-error.ts new file mode 100644 index 00000000..5791f553 --- /dev/null +++ b/packages/mapped-string/src/located-error.ts @@ -0,0 +1,33 @@ +/* + * located-error.ts + * + * Copyright (C) 2020-2024 Posit Software, PBC + */ + +import { mappedIndexToLineCol, MappedString } from "./mapped-text"; + +export class LocatedError extends Error { + constructor( + name: string, + message: string, + source: MappedString, + position = 0, + printName = true, + printStack = true, + ) { + const fileName = source.map(position)?.originalString?.fileName; + if (fileName) { + const { line, column } = mappedIndexToLineCol(source)(position); + message = `In file ${fileName} (${line + 1}:${column + 1}): +${message}`; + } + + super(message); + this.name = name; + this.printName = printName; + this.printStack = printStack; + } + + public readonly printName: boolean; + public readonly printStack: boolean; +} diff --git a/packages/mapped-string/src/mapped-text.ts b/packages/mapped-string/src/mapped-text.ts new file mode 100644 index 00000000..758010c8 --- /dev/null +++ b/packages/mapped-string/src/mapped-text.ts @@ -0,0 +1,444 @@ +/** + * mapped-text.ts + * + * Copyright (C) 2021-2024 Posit Software, PBC + */ + +import { glb } from "./glb"; + +import { rangedLines } from "./ranged-text"; + +import { + indexToLineCol as unmappedIndexToLineCol, + lineBreakPositions, + matchAll, +} from "./text"; + +import { + EitherString, + MappedString, + Range, + RangedSubstring, + StringChunk, + StringMapResult, +} from "./types"; +import { InternalError } from "./error"; + +export type { + EitherString, + MappedString, + Range, + RangedSubstring, + StringChunk, +} from "./types"; + +/** + * returns a substring of the mapped string, together with associated maps + * + * @param source + * @param start index for start of substring + * @param end index for end of substring (optional) + * @returns mapped string + */ +export function mappedSubstring( + source: EitherString, + start: number, + end?: number, +): MappedString { + if (typeof source === "string") { + source = asMappedString(source); + } + const value = source.value.substring(start, end); + // typescript doesn't see type stability across closures, + // so we hold its hand a little here + const mappedSource: MappedString = source; + return { + value, + fileName: mappedSource.fileName, + map: (index: number, closest?: boolean) => { + if (closest) { + index = Math.max(0, Math.min(value.length, index - 1)); + } + // we need to special-case a zero-offset lookup from an empty string, + // since those are necessary during error resolution of empty YAML values. + if (index === 0 && index === value.length) { + return mappedSource.map(index + start, closest); + } + if (index < 0 || index >= value.length) { + return undefined; + } + return mappedSource.map(index + start, closest); + }, + }; +} + +/** +mappedString provides a mechanism for maintaining offset information +through substrings. This comes up often in quarto, where we often pull +a part of a larger string, send that to an interpreter, compiler or +validator, and then want to report error information with respect to +line information in the first string. + +You construct a mappedString from a list of substring ranges of an +original string (or unmappable "new" substrings), which are +concatenated into the result in the field `value`. + +In the field `fileName`, we (optionally) keep a filename, strictly as +metadata for error reporting. + +In addition to this new string, mappedString provides `map` that sends offsets from this new +string into offsets of the original "base" mappedString. If closest=true, +`map` attempts to avoid undefined results by +returning the closest smaller result that is valid in case it's called +with a value that has no inverse (such as an out-of-bounds access). + +If you pass a MappedString as the input to this function, the result's +map will walk the inverse maps all the way to the base mappedString +(constructed from asMappedString). + +This provides a natural composition for mapped strings. +*/ + +export function mappedString( + source: EitherString, + pieces: StringChunk[], + fileName?: string, +): MappedString { + + if (typeof source === "string") { + source = asMappedString(source, fileName); + } + + const mappedPieces = pieces.map((piece) => { + if (typeof piece === "string") { + return asMappedString(piece); + } else if ((piece as MappedString).value !== undefined) { + return piece as MappedString; + } else { + const { start, end } = piece as Range; + return mappedSubstring(source, start, end); + } + }); + return mappedConcat(mappedPieces); +} + +export function asMappedString( + str: EitherString, + fileName?: string, +): MappedString { + if (typeof str === "string") { + return { + value: str, + fileName, + map: function (index: number, closest?: boolean): StringMapResult { + if (closest) { + index = Math.min(str.length - 1, Math.max(0, index)); + } + if (index < 0 || index >= str.length) { + return undefined; + } + return { + index, + originalString: this, + }; + }, + }; + } else if (fileName !== undefined) { + throw new InternalError( + "can't change the fileName of an existing MappedString", + ); + } else { + return str; + } +} + +// Every mapped string parameter should have the same originalString and fileName. +// If none of the parameters are mappedstring, this returns a fresh +// MappedString +export function mappedConcat(strings: EitherString[]): MappedString { + if (strings.length === 0) { + return { + value: "", + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + map: (_index: number, _closest?: boolean) => undefined, + }; + } + if (strings.every((s) => typeof s === "string")) { + return asMappedString(strings.join("")); + } + + const mappedStrings = strings.map((s) => { + if (typeof s === "string") { + return asMappedString(s); + } else return s; + }); + let currentOffset = 0; + const offsets: number[] = [0]; + for (const s of mappedStrings) { + currentOffset += s.value.length; + offsets.push(currentOffset); + } + const value = mappedStrings.map((s) => s.value).join(""); + + return { + value, + map: (offset: number, closest?: boolean): StringMapResult => { + if (closest) { + offset = Math.max(0, Math.min(offset, value.length - 1)); + } + // we need to special-case an offset-0 lookup into an empty mapped string + // since those are necessary during error resolution of empty YAML values. + if (offset === 0 && offset == value.length && mappedStrings.length) { + return mappedStrings[0].map(0, closest); + } + if (offset < 0 || offset >= value.length) { + return undefined; + } + const ix = glb(offsets, offset); + const v = mappedStrings[ix]; + return v.map(offset - offsets[ix]); + }, + }; +} + +// mapped version of text.ts:indexToLineCol +export function mappedIndexToLineCol(eitherText: EitherString) { + const text = asMappedString(eitherText); + + return function (offset: number) { + const mapResult = text.map(offset, true); + if (mapResult === undefined) { + throw new InternalError("bad offset in mappedIndexRowCol"); + } + const { index, originalString } = mapResult; + + return unmappedIndexToLineCol(originalString.value)(index); + }; +} + +// mapped version of text.ts:normalizeNewlines +export function mappedNormalizeNewlines( + eitherText: EitherString, +): MappedString { + const text = asMappedString(eitherText); + + // here we search for \r\n, and skip the \r's. that's slightly + // different from the other implementation but the observable + // behavior on .value is the same. + + let start = 0; + const chunks: Range[] = []; + for (const offset of lineBreakPositions(text.value)) { + if (text.value[offset] !== "\r") { + continue; + } + + // we know this is an \r\n, so we handle it + chunks.push({ start, end: offset }); // string contents + chunks.push({ start: offset + 1, end: offset + 2 }); // \n part of \r\n + start = offset + 2; + } + if (start !== text.value.length) { + chunks.push({ start, end: text.value.length }); + } + return mappedString(text, chunks); +} + +// skipRegexpAll(s, r) is a mapped version of s.replaceAll(r, "") +export function skipRegexpAll( + eitherText: EitherString, + re: RegExp, +): MappedString { + const text = asMappedString(eitherText); + + let start = 0; + const chunks: Range[] = []; + for (const match of matchAll(text.value, re)) { + chunks.push({ start, end: match.index }); + start = match[0].length + match.index; + } + if (start !== text.value.length) { + chunks.push({ start, end: text.value.length }); + } + return mappedString(text, chunks); +} + +// skipRegexp(s, r) is a mapped version of s.replace(r, "") +export function skipRegexp(eitherText: EitherString, re: RegExp): MappedString { + const text = asMappedString(eitherText); + const m = text.value.match(re); + + if (m) { + return mappedString(text, [ + { start: 0, end: m.index! }, + { start: m.index! + m[0].length, end: text.value.length }, + ]); + } else { + return text; + } +} + +/** + * join an array of EitherStrings into a single MappedString. This + * is effectively the EitherString version of Array.prototype.join. + */ +export function join(mappedStrs: EitherString[], sep: string): MappedString { + const innerStrings: MappedString[] = []; + const mappedSep = asMappedString(sep); + for (let i = 0; i < mappedStrs.length; ++i) { + const mappedStr = mappedStrs[i]; + if (typeof mappedStr === "string") { + innerStrings.push(asMappedString(mappedStr)); + } else { + innerStrings.push(mappedStr); + } + if (i < mappedStrs.length) { + innerStrings.push(mappedSep); + } + } + return mappedConcat(innerStrings); +} + +export function mappedLines( + str: MappedString, + keepNewLines = false, +): MappedString[] { + const lines = rangedLines(str.value, keepNewLines); + return lines.map((v: RangedSubstring) => mappedString(str, [v.range])); +} + +export function mappedReplace( + str: MappedString, + target: string | RegExp, + replacement: EitherString, +): MappedString { + if (typeof target === "string") { + const index = str.value.indexOf(target); + if (index === -1) { + return str; + } + return mappedConcat([ + mappedString(str, [{ start: 0, end: index }]), + asMappedString(replacement), + mappedString(str, [{ + start: index + target.length, + end: str.value.length, + }]), + ]); + } + + if (!target.global) { + const result = target.exec(str.value); + if (!result) { + return str; + } + return mappedConcat([ + mappedSubstring(str, 0, target.lastIndex), + asMappedString(replacement), + mappedSubstring( + str, + target.lastIndex + result[0].length, + str.value.length, + ), + ]); + } + + let result = target.exec(str.value); + if (!result) { + return str; + } + let currentRange = 0; + const pieces: MappedString[] = []; + while (result) { + pieces.push( + mappedSubstring(str, currentRange, target.lastIndex), + ); + pieces.push(asMappedString(replacement)); + currentRange = target.lastIndex + result[0].length; + + result = target.exec(str.value); + } + pieces.push( + mappedSubstring(str, currentRange, str.value.length), + ); + + return mappedConcat(pieces); +} + +/** + * breakOnDelimiter() behaves like split(), except that it: + * + * - operates on MappedStrings + * - returns an array of MappedStrings + * - keeps the delimiters inside the string's results by default. This last + * quirk is often useful + */ +export function breakOnDelimiter( + string: MappedString, + delimiter: string, + keepDelimiters = true, +): MappedString[] { + let currentPosition = 0; + let r = string.value.indexOf(delimiter, currentPosition); + const substrings: MappedString[] = []; + while (r !== -1) { + const end = keepDelimiters ? r + delimiter.length : r; + substrings.push(mappedSubstring(string, currentPosition, end)); + currentPosition = r + delimiter.length; + r = string.value.indexOf(delimiter, currentPosition); + } + return substrings; +} + +function findSpaceStart(string: MappedString): number { + const result = string.value.match(/^\s+/); + if (result === null || result.length === 0) { + return 0; + } + return result[0].length; +} + +function findSpaceEnd(string: MappedString): number { + const result = string.value.match(/\s+$/); + if (result === null || result.length === 0) { + return 0; + } + return result[0].length; +} + +/** + * mappedTrim(): MappedString version of String.trim() + */ +export function mappedTrim(string: MappedString): MappedString { + const start = findSpaceStart(string); + const end = findSpaceEnd(string); + if (start === 0 && end === 0) { + return string; + } + if (start > string.value.length - end) { + return mappedSubstring(string, 0, 0); + } + return mappedSubstring(string, start, string.value.length - end); +} + +/** + * mappedTrimStart(): MappedString version of String.trimStart() + */ +export function mappedTrimStart(string: MappedString): MappedString { + const start = findSpaceStart(string); + if (start === 0) { + return string; + } + return mappedSubstring(string, start, string.value.length); +} + +/** + * mappedTrimEnd(): MappedString version of String.trimEnd() + */ +export function mappedTrimEnd(string: MappedString): MappedString { + const end = findSpaceEnd(string); + if (end === 0) { + return string; + } + return mappedSubstring(string, 0, string.value.length - end); +} diff --git a/packages/mapped-string/src/ranged-text.ts b/packages/mapped-string/src/ranged-text.ts new file mode 100644 index 00000000..b759a648 --- /dev/null +++ b/packages/mapped-string/src/ranged-text.ts @@ -0,0 +1,93 @@ +/* +* ranged-text.ts +* +* Copyright (C) 2021-2024 Posit Software, PBC +* +*/ + +import { RangedSubstring } from "./types"; +export type { Range, RangedSubstring } from "./types"; + +export function rangedSubstring( + src: string, + start: number, + end = -1, +): RangedSubstring { + if (end === -1) { + end = src.length; + } + + const substring = src.substring(start, end); + return { + substring, + range: { start, end }, + }; +} + +function matchAll(str: string, regex: RegExp) { + let match; + regex = new RegExp(regex); // create new to guarantee freshness wrt exec + const result = []; + while ((match = regex.exec(str)) != null) { + result.push(match); + } + return result; +} + +// RangedSubstring version of lines(), but includes the option to +// carry newlines with it, since that's sometimes useful +export function rangedLines( + text: string, + includeNewLines = false, +): RangedSubstring[] { + const regex = /\r?\n/g; + const result: RangedSubstring[] = []; + + let startOffset = 0; + // NB can't use String.prototype.matchAll here because this is + // getting sent into the IDE which runs an older version of the js + // stdlib without String.prototype.matchAll + + if (!includeNewLines) { + for (const r of matchAll(text, regex)) { + result.push({ + substring: text.substring(startOffset, r.index!), + range: { + start: startOffset, + end: r.index!, + }, + }); + startOffset = r.index! + r[0].length; + } + result.push({ + substring: text.substring(startOffset, text.length), + range: { + start: startOffset, + end: text.length, + }, + }); + return result; + } else { + const matches = matchAll(text, regex); + let prevOffset = 0; + for (const r of matches) { + const stringEnd = r.index! + r[0].length; + result.push({ + substring: text.substring(prevOffset, stringEnd), + range: { + start: prevOffset, + end: stringEnd, + }, + }); + prevOffset = stringEnd; + } + result.push({ + substring: text.substring(prevOffset, text.length), + range: { + start: prevOffset, + end: text.length, + }, + }); + return result; + } +} diff --git a/packages/mapped-string/src/text.ts b/packages/mapped-string/src/text.ts new file mode 100644 index 00000000..3e50b835 --- /dev/null +++ b/packages/mapped-string/src/text.ts @@ -0,0 +1,285 @@ +/* + * text.ts + * + * Copyright (C) 2021-2024 Posit Software, PBC + */ + +import { glb } from "./glb"; +import { InternalError } from "./error"; + +export function lines(text: string): string[] { + return text.split(/\r?\n/); +} + +export function normalizeNewlines(text: string) { + return lines(text).join("\n"); +} + +export function trimEmptyLines( + lines: string[], + trim: "leading" | "trailing" | "all" = "all", +) { + // trim leading lines + if (trim === "all" || trim === "leading") { + const firstNonEmpty = lines.findIndex((line) => line.trim().length > 0); + if (firstNonEmpty === -1) { + return []; + } + lines = lines.slice(firstNonEmpty); + } + + // trim trailing lines + if (trim === "all" || trim === "trailing") { + let lastNonEmpty = -1; + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].trim().length > 0) { + lastNonEmpty = i; + break; + } + } + if (lastNonEmpty > -1) { + lines = lines.slice(0, lastNonEmpty + 1); + } + } + + return lines; +} + +// NB we can't use JS matchAll or replaceAll here because we need to support old +// Chromium in the IDE +// +// NB this mutates the regexp. +export function* matchAll(text: string, regexp: RegExp) { + if (!regexp.global) { + throw new Error("matchAll requires global regexps"); + } + let match; + while ((match = regexp.exec(text)) !== null) { + yield match; + } +} + +export function* lineOffsets(text: string) { + yield 0; + for (const match of matchAll(text, /\r?\n/g)) { + yield match.index + match[0].length; + } +} + +export function* lineBreakPositions(text: string) { + for (const match of matchAll(text, /\r?\n/g)) { + yield match.index; + } +} + +export function indexToLineCol(text: string) { + const offsets = Array.from(lineOffsets(text)); + return function (offset: number) { + if (offset === 0) { + return { + line: 0, + column: 0, + }; + } + + const startIndex = glb(offsets, offset); + return { + line: startIndex, + column: offset - offsets[startIndex], + }; + }; +} + +export function lineColToIndex(text: string) { + const offsets = Array.from(lineOffsets(text)); + return function (position: { line: number; column: number }) { + return offsets[position.line] + position.column; + }; +} + +// O(n1 * n2) naive edit string distance, don't use this on big texts! +export function editDistance(w1: string, w2: string): number { + const cost = (c: string): number => { + if ("_-".indexOf(c) !== -1) { + return 1; + } + return 10; + }; + const cost2 = (c1: string, c2: string): number => { + if (c1 === c2) { + return 0; + } + if ("_-".indexOf(c1) !== -1 && "_-".indexOf(c2) !== -1) { + return 1; + } + if (c1.toLocaleLowerCase() === c2.toLocaleLowerCase()) { + return 1; + } + const cc1 = c1.charCodeAt(0); + const cc2 = c2.charCodeAt(0); + + if (cc1 >= 48 && cc1 <= 57 && cc2 >= 48 && cc2 <= 57) { + return 1; + } + + return 10; + }; + + const s1 = w1.length + 1; + const s2 = w2.length + 1; + const v = new Int32Array(s1 * s2); + for (let i = 0; i < s1; ++i) { + for (let j = 0; j < s2; ++j) { + if (i === 0 && j === 0) { + continue; + } else if (i === 0) { + v[i * s2 + j] = v[i * s2 + (j - 1)] + cost(w2[j - 1]); + } else if (j === 0) { + v[i * s2 + j] = v[(i - 1) * s2 + j] + cost(w1[i - 1]); + } else { + v[i * s2 + j] = Math.min( + v[(i - 1) * s2 + (j - 1)] + cost2(w1[i - 1], w2[j - 1]), + v[i * s2 + (j - 1)] + cost(w2[j - 1]), + v[(i - 1) * s2 + j] + cost(w1[i - 1]), + ); + } + } + } + + return v[(w1.length + 1) * (w2.length + 1) - 1]; +} + +export type CaseConvention = + | "camelCase" + | "capitalizationCase" + | "underscore_case" + | "snake_case" + | "dash-case" + | "kebab-case"; + +export function detectCaseConvention( + key: string, +): CaseConvention | undefined { + if (key.toLocaleLowerCase() !== key) { + return "capitalizationCase"; + } + const underscoreIndex = key.indexOf("_"); + if ( + underscoreIndex !== -1 && + underscoreIndex !== 0 && + underscoreIndex !== key.length - 1 + ) { + return "underscore_case"; + } + const dashIndex = key.indexOf("-"); + if ( + dashIndex !== -1 && + dashIndex !== 0 && + dashIndex !== key.length - 1 + ) { + return "dash-case"; + } + return undefined; +} + +export function resolveCaseConventionRegex( + keys: string[], + conventions?: CaseConvention[], +): { + pattern?: string; + list: string[]; +} { + if (conventions !== undefined) { + if (conventions.length === 0) { + throw new InternalError( + "resolveCaseConventionRegex requires nonempty `conventions`", + ); + } + // conventions were specified, we use them + return { + pattern: conventions.map((c) => `(${c})`).join("|"), + list: conventions, + }; + } + + // no conventions were specified, we sniff all keys to disallow near-misses + const disallowedNearMisses: string[] = []; + const keySet = new Set(keys); + + const addNearMiss = (value: string) => { + if (!keySet.has(value)) { + disallowedNearMisses.push(value); + } + }; + + const foundConventions: Set = new Set(); + for (const key of keys) { + const found = detectCaseConvention(key); + if (found) { + foundConventions.add(found); + } + switch (found) { + case "capitalizationCase": + addNearMiss(toUnderscoreCase(key)); + addNearMiss(toDashCase(key)); + break; + case "dash-case": + addNearMiss(toUnderscoreCase(key)); + addNearMiss(toCapitalizationCase(key)); + break; + case "underscore_case": + addNearMiss(toDashCase(key)); + addNearMiss(toCapitalizationCase(key)); + break; + } + } + + if (foundConventions.size === 0) { + // if no evidence of any keys was found, return undefined so + // that no required names regex is set. + return { + pattern: undefined, + list: [], + }; + } + + return { + pattern: `(?!(${disallowedNearMisses.map((c) => `^${c}$`).join("|")}))`, + list: Array.from(foundConventions), + }; +} + +export function toDashCase(str: string): string { + return toUnderscoreCase(str).replace(/_/g, "-"); +} + +export function toUnderscoreCase(str: string): string { + return str.replace( + /([A-Z]+)/g, + (_match: string, p1: string) => `-${p1}`, + ).replace(/-/g, "_").split("_").filter((x) => x.length).join("_") + .toLocaleLowerCase(); +} + +export function toCapitalizationCase(str: string): string { + return toUnderscoreCase(str).replace( + /_(.)/g, + (_match: string, p1: string) => p1.toLocaleUpperCase(), + ); +} + +export function normalizeCaseConvention(str: string): CaseConvention { + const map: Record = { + "capitalizationCase": "capitalizationCase", + "camelCase": "capitalizationCase", + "underscore_case": "underscore_case", + "snake_case": "underscore_case", + "dash-case": "dash-case", + "kebab-case": "dash-case", + }; + const result = map[str]; + if (result === undefined) { + throw new InternalError(`${str} is not a valid case convention`); + } + return result; +} diff --git a/packages/mapped-string/src/types.ts b/packages/mapped-string/src/types.ts new file mode 100644 index 00000000..2c0b49db --- /dev/null +++ b/packages/mapped-string/src/types.ts @@ -0,0 +1,58 @@ +/* +* text-types.ts +* +* Copyright (C) 2021-2024 Posit Software, PBC +* +*/ + +export interface Range { + start: number; + end: number; +} + +// A ranged substring is simply a substring of some string plus the +// positional information. It's used to carry positional information of +// source code as it's processed through the system. +// +// The defining property is: +// +// const rangedSub = rangedSubstring(src, start, end); +// assert(rangedSub === src.substring(rangedSub.range.start, rangedSub.range.end)); +export interface RangedSubstring { + readonly substring: string; + readonly range: Range; +} + +export type StringMapResult = { + index: number; + originalString: MappedString; +} | undefined; + +export interface MappedString { + readonly value: string; + readonly fileName?: string; + readonly map: (index: number, closest?: boolean) => StringMapResult; +} + +export type EitherString = string | MappedString; +export type StringChunk = string | MappedString | Range; + +export interface ErrorLocation { + start: { + line: number; + column: number; + }; + end: { + line: number; + column: number; + }; +} + +export interface TidyverseError { + heading: string; + error: string[]; + info: Record; // use tag for infos to only display one error of each tag + fileName?: string; + location?: ErrorLocation; + sourceContext?: string; +} diff --git a/packages/mapped-string/tsconfig.json b/packages/mapped-string/tsconfig.json new file mode 100644 index 00000000..aad794c2 --- /dev/null +++ b/packages/mapped-string/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["node_modules"], + "extends": "tsconfig/base.json", + "include": ["src"] +} diff --git a/yarn.lock b/yarn.lock index 1661c7d7..220b6b81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7892,6 +7892,11 @@ typescript@^4.5.2, typescript@^4.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^5.4.2: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + typo-js@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.2.tgz#340484d81fe518e77c81a5a770162b14492f183b" From 093a5b8007ad5ef8f87a3a1e9352e7eb9cb6ec7e Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 14:23:26 -0700 Subject: [PATCH 02/18] more partial work towards external yaml annotated parser --- packages/annotated-json/external/js-yaml.js | 3312 +++++++++++++++++ packages/annotated-json/package.json | 2 + packages/annotated-json/src/annotated-yaml.ts | 727 ++++ packages/annotated-json/src/glb.ts | 70 + .../src/js-yaml-quarto-schema.ts | 38 + packages/annotated-json/src/text.ts | 57 + packages/annotated-json/tsconfig.json | 5 +- packages/mapped-string/package.json | 3 +- packages/mapped-string/src/error.ts | 129 + packages/mapped-string/src/index.ts | 3 +- packages/tidyverse-errors/package.json | 15 + packages/tidyverse-errors/src/errors.ts | 56 + packages/tidyverse-errors/src/index.ts | 25 + packages/tidyverse-errors/tsconfig.json | 8 + 14 files changed, 4447 insertions(+), 3 deletions(-) create mode 100644 packages/annotated-json/external/js-yaml.js create mode 100644 packages/annotated-json/src/annotated-yaml.ts create mode 100644 packages/annotated-json/src/glb.ts create mode 100644 packages/annotated-json/src/js-yaml-quarto-schema.ts create mode 100644 packages/annotated-json/src/text.ts create mode 100644 packages/tidyverse-errors/package.json create mode 100644 packages/tidyverse-errors/src/errors.ts create mode 100644 packages/tidyverse-errors/src/index.ts create mode 100644 packages/tidyverse-errors/tsconfig.json diff --git a/packages/annotated-json/external/js-yaml.js b/packages/annotated-json/external/js-yaml.js new file mode 100644 index 00000000..01e26b21 --- /dev/null +++ b/packages/annotated-json/external/js-yaml.js @@ -0,0 +1,3312 @@ +// eslint-disable + +/** NB: we needed to hack our way around the fact that js-yaml doesn't export some entry points + * that are necessary to define custom schema. We added those exports by hand on the output of esbuild. + * + * FIXME This needs a better solution. + * + */ + +/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */ +function isNothing(subject) { + return typeof subject === "undefined" || subject === null; +} +function isObject(subject) { + return typeof subject === "object" && subject !== null; +} +function toArray(sequence) { + if (Array.isArray(sequence)) return sequence; + else if (isNothing(sequence)) return []; + return [sequence]; +} +function extend(target, source) { + var index, length, key, sourceKeys; + if (source) { + sourceKeys = Object.keys(source); + for (index = 0, length = sourceKeys.length; index < length; index += 1) { + key = sourceKeys[index]; + target[key] = source[key]; + } + } + return target; +} +function repeat(string, count) { + var result = "", + cycle; + for (cycle = 0; cycle < count; cycle += 1) { + result += string; + } + return result; +} +function isNegativeZero(number) { + return number === 0 && Number.NEGATIVE_INFINITY === 1 / number; +} +var isNothing_1 = isNothing; +var isObject_1 = isObject; +var toArray_1 = toArray; +var repeat_1 = repeat; +var isNegativeZero_1 = isNegativeZero; +var extend_1 = extend; +var common = { + isNothing: isNothing_1, + isObject: isObject_1, + toArray: toArray_1, + repeat: repeat_1, + isNegativeZero: isNegativeZero_1, + extend: extend_1, +}; +function formatError(exception2, compact) { + var where = "", + message = exception2.reason || "(unknown reason)"; + if (!exception2.mark) return message; + if (exception2.mark.name) { + where += 'in "' + exception2.mark.name + '" '; + } + where += + "(" + (exception2.mark.line + 1) + ":" + (exception2.mark.column + 1) + ")"; + if (!compact && exception2.mark.snippet) { + where += "\n\n" + exception2.mark.snippet; + } + return message + " " + where; +} +function YAMLException$1(reason, mark) { + Error.call(this); + this.name = "YAMLException"; + this.reason = reason; + this.mark = mark; + this.message = formatError(this, false); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error().stack || ""; + } +} +YAMLException$1.prototype = Object.create(Error.prototype); +YAMLException$1.prototype.constructor = YAMLException$1; +YAMLException$1.prototype.toString = function toString(compact) { + return this.name + ": " + formatError(this, compact); +}; +var exception = YAMLException$1; +function getLine(buffer, lineStart, lineEnd, position, maxLineLength) { + var head = ""; + var tail = ""; + var maxHalfLength = Math.floor(maxLineLength / 2) - 1; + if (position - lineStart > maxHalfLength) { + head = " ... "; + lineStart = position - maxHalfLength + head.length; + } + if (lineEnd - position > maxHalfLength) { + tail = " ..."; + lineEnd = position + maxHalfLength - tail.length; + } + return { + str: + head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "\u2192") + tail, + pos: position - lineStart + head.length, + }; +} +function padStart(string, max) { + return common.repeat(" ", max - string.length) + string; +} +function makeSnippet(mark, options) { + options = Object.create(options || null); + if (!mark.buffer) return null; + if (!options.maxLength) options.maxLength = 79; + if (typeof options.indent !== "number") options.indent = 1; + if (typeof options.linesBefore !== "number") options.linesBefore = 3; + if (typeof options.linesAfter !== "number") options.linesAfter = 2; + var re = /\r?\n|\r|\0/g; + var lineStarts = [0]; + var lineEnds = []; + var match; + var foundLineNo = -1; + while ((match = re.exec(mark.buffer))) { + lineEnds.push(match.index); + lineStarts.push(match.index + match[0].length); + if (mark.position <= match.index && foundLineNo < 0) { + foundLineNo = lineStarts.length - 2; + } + } + if (foundLineNo < 0) foundLineNo = lineStarts.length - 1; + var result = "", + i, + line; + var lineNoLength = Math.min( + mark.line + options.linesAfter, + lineEnds.length + ).toString().length; + var maxLineLength = options.maxLength - (options.indent + lineNoLength + 3); + for (i = 1; i <= options.linesBefore; i++) { + if (foundLineNo - i < 0) break; + line = getLine( + mark.buffer, + lineStarts[foundLineNo - i], + lineEnds[foundLineNo - i], + mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]), + maxLineLength + ); + result = + common.repeat(" ", options.indent) + + padStart((mark.line - i + 1).toString(), lineNoLength) + + " | " + + line.str + + "\n" + + result; + } + line = getLine( + mark.buffer, + lineStarts[foundLineNo], + lineEnds[foundLineNo], + mark.position, + maxLineLength + ); + result += + common.repeat(" ", options.indent) + + padStart((mark.line + 1).toString(), lineNoLength) + + " | " + + line.str + + "\n"; + result += + common.repeat("-", options.indent + lineNoLength + 3 + line.pos) + "^\n"; + for (i = 1; i <= options.linesAfter; i++) { + if (foundLineNo + i >= lineEnds.length) break; + line = getLine( + mark.buffer, + lineStarts[foundLineNo + i], + lineEnds[foundLineNo + i], + mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]), + maxLineLength + ); + result += + common.repeat(" ", options.indent) + + padStart((mark.line + i + 1).toString(), lineNoLength) + + " | " + + line.str + + "\n"; + } + return result.replace(/\n$/, ""); +} +var snippet = makeSnippet; +var TYPE_CONSTRUCTOR_OPTIONS = [ + "kind", + "multi", + "resolve", + "construct", + "instanceOf", + "predicate", + "represent", + "representName", + "defaultStyle", + "styleAliases", +]; +var YAML_NODE_KINDS = ["scalar", "sequence", "mapping"]; +function compileStyleAliases(map2) { + var result = {}; + if (map2 !== null) { + Object.keys(map2).forEach(function (style) { + map2[style].forEach(function (alias) { + result[String(alias)] = style; + }); + }); + } + return result; +} +function Type$1(tag, options) { + options = options || {}; + Object.keys(options).forEach(function (name) { + if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) { + throw new exception( + 'Unknown option "' + + name + + '" is met in definition of "' + + tag + + '" YAML type.' + ); + } + }); + this.options = options; + this.tag = tag; + this.kind = options["kind"] || null; + this.resolve = + options["resolve"] || + function () { + return true; + }; + this.construct = + options["construct"] || + function (data) { + return data; + }; + this.instanceOf = options["instanceOf"] || null; + this.predicate = options["predicate"] || null; + this.represent = options["represent"] || null; + this.representName = options["representName"] || null; + this.defaultStyle = options["defaultStyle"] || null; + this.multi = options["multi"] || false; + this.styleAliases = compileStyleAliases(options["styleAliases"] || null); + if (YAML_NODE_KINDS.indexOf(this.kind) === -1) { + throw new exception( + 'Unknown kind "' + + this.kind + + '" is specified for "' + + tag + + '" YAML type.' + ); + } +} +var type = Type$1; +function compileList(schema2, name) { + var result = []; + schema2[name].forEach(function (currentType) { + var newIndex = result.length; + result.forEach(function (previousType, previousIndex) { + if ( + previousType.tag === currentType.tag && + previousType.kind === currentType.kind && + previousType.multi === currentType.multi + ) { + newIndex = previousIndex; + } + }); + result[newIndex] = currentType; + }); + return result; +} +function compileMap() { + var result = { + scalar: {}, + sequence: {}, + mapping: {}, + fallback: {}, + multi: { + scalar: [], + sequence: [], + mapping: [], + fallback: [], + }, + }, + index, + length; + function collectType(type2) { + if (type2.multi) { + result.multi[type2.kind].push(type2); + result.multi["fallback"].push(type2); + } else { + result[type2.kind][type2.tag] = result["fallback"][type2.tag] = type2; + } + } + for (index = 0, length = arguments.length; index < length; index += 1) { + arguments[index].forEach(collectType); + } + return result; +} +function Schema$1(definition) { + return this.extend(definition); +} +Schema$1.prototype.extend = function extend2(definition) { + var implicit = []; + var explicit = []; + if (definition instanceof type) { + explicit.push(definition); + } else if (Array.isArray(definition)) { + explicit = explicit.concat(definition); + } else if ( + definition && + (Array.isArray(definition.implicit) || Array.isArray(definition.explicit)) + ) { + if (definition.implicit) implicit = implicit.concat(definition.implicit); + if (definition.explicit) explicit = explicit.concat(definition.explicit); + } else { + throw new exception( + "Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })" + ); + } + implicit.forEach(function (type$1) { + if (!(type$1 instanceof type)) { + throw new exception( + "Specified list of YAML types (or a single Type object) contains a non-Type object." + ); + } + if (type$1.loadKind && type$1.loadKind !== "scalar") { + throw new exception( + "There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported." + ); + } + if (type$1.multi) { + throw new exception( + "There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit." + ); + } + }); + explicit.forEach(function (type$1) { + if (!(type$1 instanceof type)) { + throw new exception( + "Specified list of YAML types (or a single Type object) contains a non-Type object." + ); + } + }); + var result = Object.create(Schema$1.prototype); + result.implicit = (this.implicit || []).concat(implicit); + result.explicit = (this.explicit || []).concat(explicit); + result.compiledImplicit = compileList(result, "implicit"); + result.compiledExplicit = compileList(result, "explicit"); + result.compiledTypeMap = compileMap( + result.compiledImplicit, + result.compiledExplicit + ); + return result; +}; +var schema = Schema$1; +var str = new type("tag:yaml.org,2002:str", { + kind: "scalar", + construct: function (data) { + return data !== null ? data : ""; + }, +}); +var seq = new type("tag:yaml.org,2002:seq", { + kind: "sequence", + construct: function (data) { + return data !== null ? data : []; + }, +}); +var map = new type("tag:yaml.org,2002:map", { + kind: "mapping", + construct: function (data) { + return data !== null ? data : {}; + }, +}); +var failsafe = new schema({ + explicit: [str, seq, map], +}); +function resolveYamlNull(data) { + if (data === null) return true; + var max = data.length; + return ( + (max === 1 && data === "~") || + (max === 4 && (data === "null" || data === "Null" || data === "NULL")) + ); +} +function constructYamlNull() { + return null; +} +function isNull(object) { + return object === null; +} +var _null = new type("tag:yaml.org,2002:null", { + kind: "scalar", + resolve: resolveYamlNull, + construct: constructYamlNull, + predicate: isNull, + represent: { + canonical: function () { + return "~"; + }, + lowercase: function () { + return "null"; + }, + uppercase: function () { + return "NULL"; + }, + camelcase: function () { + return "Null"; + }, + empty: function () { + return ""; + }, + }, + defaultStyle: "lowercase", +}); +function resolveYamlBoolean(data) { + if (data === null) return false; + var max = data.length; + return ( + (max === 4 && (data === "true" || data === "True" || data === "TRUE")) || + (max === 5 && (data === "false" || data === "False" || data === "FALSE")) + ); +} +function constructYamlBoolean(data) { + return data === "true" || data === "True" || data === "TRUE"; +} +function isBoolean(object) { + return Object.prototype.toString.call(object) === "[object Boolean]"; +} +var bool = new type("tag:yaml.org,2002:bool", { + kind: "scalar", + resolve: resolveYamlBoolean, + construct: constructYamlBoolean, + predicate: isBoolean, + represent: { + lowercase: function (object) { + return object ? "true" : "false"; + }, + uppercase: function (object) { + return object ? "TRUE" : "FALSE"; + }, + camelcase: function (object) { + return object ? "True" : "False"; + }, + }, + defaultStyle: "lowercase", +}); +function isHexCode(c) { + return (48 <= c && c <= 57) || (65 <= c && c <= 70) || (97 <= c && c <= 102); +} +function isOctCode(c) { + return 48 <= c && c <= 55; +} +function isDecCode(c) { + return 48 <= c && c <= 57; +} +function resolveYamlInteger(data) { + if (data === null) return false; + var max = data.length, + index = 0, + hasDigits = false, + ch; + if (!max) return false; + ch = data[index]; + if (ch === "-" || ch === "+") { + ch = data[++index]; + } + if (ch === "0") { + if (index + 1 === max) return true; + ch = data[++index]; + if (ch === "b") { + index++; + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (ch !== "0" && ch !== "1") return false; + hasDigits = true; + } + return hasDigits && ch !== "_"; + } + if (ch === "x") { + index++; + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (!isHexCode(data.charCodeAt(index))) return false; + hasDigits = true; + } + return hasDigits && ch !== "_"; + } + if (ch === "o") { + index++; + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (!isOctCode(data.charCodeAt(index))) return false; + hasDigits = true; + } + return hasDigits && ch !== "_"; + } + } + if (ch === "_") return false; + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (!isDecCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + if (!hasDigits || ch === "_") return false; + return true; +} +function constructYamlInteger(data) { + var value = data, + sign = 1, + ch; + if (value.indexOf("_") !== -1) { + value = value.replace(/_/g, ""); + } + ch = value[0]; + if (ch === "-" || ch === "+") { + if (ch === "-") sign = -1; + value = value.slice(1); + ch = value[0]; + } + if (value === "0") return 0; + if (ch === "0") { + if (value[1] === "b") return sign * parseInt(value.slice(2), 2); + if (value[1] === "x") return sign * parseInt(value.slice(2), 16); + if (value[1] === "o") return sign * parseInt(value.slice(2), 8); + } + return sign * parseInt(value, 10); +} +function isInteger(object) { + return ( + Object.prototype.toString.call(object) === "[object Number]" && + object % 1 === 0 && + !common.isNegativeZero(object) + ); +} +var int = new type("tag:yaml.org,2002:int", { + kind: "scalar", + resolve: resolveYamlInteger, + construct: constructYamlInteger, + predicate: isInteger, + represent: { + binary: function (obj) { + return obj >= 0 + ? "0b" + obj.toString(2) + : "-0b" + obj.toString(2).slice(1); + }, + octal: function (obj) { + return obj >= 0 + ? "0o" + obj.toString(8) + : "-0o" + obj.toString(8).slice(1); + }, + decimal: function (obj) { + return obj.toString(10); + }, + hexadecimal: function (obj) { + return obj >= 0 + ? "0x" + obj.toString(16).toUpperCase() + : "-0x" + obj.toString(16).toUpperCase().slice(1); + }, + }, + defaultStyle: "decimal", + styleAliases: { + binary: [2, "bin"], + octal: [8, "oct"], + decimal: [10, "dec"], + hexadecimal: [16, "hex"], + }, +}); +var YAML_FLOAT_PATTERN = new RegExp( + "^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$" +); +function resolveYamlFloat(data) { + if (data === null) return false; + if (!YAML_FLOAT_PATTERN.test(data) || data[data.length - 1] === "_") { + return false; + } + return true; +} +function constructYamlFloat(data) { + var value, sign; + value = data.replace(/_/g, "").toLowerCase(); + sign = value[0] === "-" ? -1 : 1; + if ("+-".indexOf(value[0]) >= 0) { + value = value.slice(1); + } + if (value === ".inf") { + return sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + } else if (value === ".nan") { + return NaN; + } + return sign * parseFloat(value, 10); +} +var SCIENTIFIC_WITHOUT_DOT = /^[-+]?[0-9]+e/; +function representYamlFloat(object, style) { + var res; + if (isNaN(object)) { + switch (style) { + case "lowercase": + return ".nan"; + case "uppercase": + return ".NAN"; + case "camelcase": + return ".NaN"; + } + } else if (Number.POSITIVE_INFINITY === object) { + switch (style) { + case "lowercase": + return ".inf"; + case "uppercase": + return ".INF"; + case "camelcase": + return ".Inf"; + } + } else if (Number.NEGATIVE_INFINITY === object) { + switch (style) { + case "lowercase": + return "-.inf"; + case "uppercase": + return "-.INF"; + case "camelcase": + return "-.Inf"; + } + } else if (common.isNegativeZero(object)) { + return "-0.0"; + } + res = object.toString(10); + return SCIENTIFIC_WITHOUT_DOT.test(res) ? res.replace("e", ".e") : res; +} +function isFloat(object) { + return ( + Object.prototype.toString.call(object) === "[object Number]" && + (object % 1 !== 0 || common.isNegativeZero(object)) + ); +} +var float = new type("tag:yaml.org,2002:float", { + kind: "scalar", + resolve: resolveYamlFloat, + construct: constructYamlFloat, + predicate: isFloat, + represent: representYamlFloat, + defaultStyle: "lowercase", +}); +var json = failsafe.extend({ + implicit: [_null, bool, int, float], +}); +var core = json; +var YAML_DATE_REGEXP = new RegExp( + "^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$" +); +var YAML_TIMESTAMP_REGEXP = new RegExp( + "^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$" +); +function resolveYamlTimestamp(data) { + if (data === null) return false; + if (YAML_DATE_REGEXP.exec(data) !== null) return true; + if (YAML_TIMESTAMP_REGEXP.exec(data) !== null) return true; + return false; +} +function constructYamlTimestamp(data) { + var match, + year, + month, + day, + hour, + minute, + second, + fraction = 0, + delta = null, + tz_hour, + tz_minute, + date; + match = YAML_DATE_REGEXP.exec(data); + if (match === null) match = YAML_TIMESTAMP_REGEXP.exec(data); + if (match === null) throw new Error("Date resolve error"); + year = +match[1]; + month = +match[2] - 1; + day = +match[3]; + if (!match[4]) { + return new Date(Date.UTC(year, month, day)); + } + hour = +match[4]; + minute = +match[5]; + second = +match[6]; + if (match[7]) { + fraction = match[7].slice(0, 3); + while (fraction.length < 3) { + fraction += "0"; + } + fraction = +fraction; + } + if (match[9]) { + tz_hour = +match[10]; + tz_minute = +(match[11] || 0); + delta = (tz_hour * 60 + tz_minute) * 6e4; + if (match[9] === "-") delta = -delta; + } + date = new Date(Date.UTC(year, month, day, hour, minute, second, fraction)); + if (delta) date.setTime(date.getTime() - delta); + return date; +} +function representYamlTimestamp(object) { + return object.toISOString(); +} +var timestamp = new type("tag:yaml.org,2002:timestamp", { + kind: "scalar", + resolve: resolveYamlTimestamp, + construct: constructYamlTimestamp, + instanceOf: Date, + represent: representYamlTimestamp, +}); +function resolveYamlMerge(data) { + return data === "<<" || data === null; +} +var merge = new type("tag:yaml.org,2002:merge", { + kind: "scalar", + resolve: resolveYamlMerge, +}); +var BASE64_MAP = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r"; +function resolveYamlBinary(data) { + if (data === null) return false; + var code, + idx, + bitlen = 0, + max = data.length, + map2 = BASE64_MAP; + for (idx = 0; idx < max; idx++) { + code = map2.indexOf(data.charAt(idx)); + if (code > 64) continue; + if (code < 0) return false; + bitlen += 6; + } + return bitlen % 8 === 0; +} +function constructYamlBinary(data) { + var idx, + tailbits, + input = data.replace(/[\r\n=]/g, ""), + max = input.length, + map2 = BASE64_MAP, + bits = 0, + result = []; + for (idx = 0; idx < max; idx++) { + if (idx % 4 === 0 && idx) { + result.push((bits >> 16) & 255); + result.push((bits >> 8) & 255); + result.push(bits & 255); + } + bits = (bits << 6) | map2.indexOf(input.charAt(idx)); + } + tailbits = (max % 4) * 6; + if (tailbits === 0) { + result.push((bits >> 16) & 255); + result.push((bits >> 8) & 255); + result.push(bits & 255); + } else if (tailbits === 18) { + result.push((bits >> 10) & 255); + result.push((bits >> 2) & 255); + } else if (tailbits === 12) { + result.push((bits >> 4) & 255); + } + return new Uint8Array(result); +} +function representYamlBinary(object) { + var result = "", + bits = 0, + idx, + tail, + max = object.length, + map2 = BASE64_MAP; + for (idx = 0; idx < max; idx++) { + if (idx % 3 === 0 && idx) { + result += map2[(bits >> 18) & 63]; + result += map2[(bits >> 12) & 63]; + result += map2[(bits >> 6) & 63]; + result += map2[bits & 63]; + } + bits = (bits << 8) + object[idx]; + } + tail = max % 3; + if (tail === 0) { + result += map2[(bits >> 18) & 63]; + result += map2[(bits >> 12) & 63]; + result += map2[(bits >> 6) & 63]; + result += map2[bits & 63]; + } else if (tail === 2) { + result += map2[(bits >> 10) & 63]; + result += map2[(bits >> 4) & 63]; + result += map2[(bits << 2) & 63]; + result += map2[64]; + } else if (tail === 1) { + result += map2[(bits >> 2) & 63]; + result += map2[(bits << 4) & 63]; + result += map2[64]; + result += map2[64]; + } + return result; +} +function isBinary(obj) { + return Object.prototype.toString.call(obj) === "[object Uint8Array]"; +} +var binary = new type("tag:yaml.org,2002:binary", { + kind: "scalar", + resolve: resolveYamlBinary, + construct: constructYamlBinary, + predicate: isBinary, + represent: representYamlBinary, +}); +var _hasOwnProperty$3 = Object.prototype.hasOwnProperty; +var _toString$2 = Object.prototype.toString; +function resolveYamlOmap(data) { + if (data === null) return true; + var objectKeys = [], + index, + length, + pair, + pairKey, + pairHasKey, + object = data; + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + pairHasKey = false; + if (_toString$2.call(pair) !== "[object Object]") return false; + for (pairKey in pair) { + if (_hasOwnProperty$3.call(pair, pairKey)) { + if (!pairHasKey) pairHasKey = true; + else return false; + } + } + if (!pairHasKey) return false; + if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey); + else return false; + } + return true; +} +function constructYamlOmap(data) { + return data !== null ? data : []; +} +var omap = new type("tag:yaml.org,2002:omap", { + kind: "sequence", + resolve: resolveYamlOmap, + construct: constructYamlOmap, +}); +var _toString$1 = Object.prototype.toString; +function resolveYamlPairs(data) { + if (data === null) return true; + var index, + length, + pair, + keys, + result, + object = data; + result = new Array(object.length); + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + if (_toString$1.call(pair) !== "[object Object]") return false; + keys = Object.keys(pair); + if (keys.length !== 1) return false; + result[index] = [keys[0], pair[keys[0]]]; + } + return true; +} +function constructYamlPairs(data) { + if (data === null) return []; + var index, + length, + pair, + keys, + result, + object = data; + result = new Array(object.length); + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + keys = Object.keys(pair); + result[index] = [keys[0], pair[keys[0]]]; + } + return result; +} +var pairs = new type("tag:yaml.org,2002:pairs", { + kind: "sequence", + resolve: resolveYamlPairs, + construct: constructYamlPairs, +}); +var _hasOwnProperty$2 = Object.prototype.hasOwnProperty; +function resolveYamlSet(data) { + if (data === null) return true; + var key, + object = data; + for (key in object) { + if (_hasOwnProperty$2.call(object, key)) { + if (object[key] !== null) return false; + } + } + return true; +} +function constructYamlSet(data) { + return data !== null ? data : {}; +} +var set = new type("tag:yaml.org,2002:set", { + kind: "mapping", + resolve: resolveYamlSet, + construct: constructYamlSet, +}); +var _default = core.extend({ + implicit: [timestamp, merge], + explicit: [binary, omap, pairs, set], +}); +var _hasOwnProperty$1 = Object.prototype.hasOwnProperty; +var CONTEXT_FLOW_IN = 1; +var CONTEXT_FLOW_OUT = 2; +var CONTEXT_BLOCK_IN = 3; +var CONTEXT_BLOCK_OUT = 4; +var CHOMPING_CLIP = 1; +var CHOMPING_STRIP = 2; +var CHOMPING_KEEP = 3; +var PATTERN_NON_PRINTABLE = + // eslint-disable-next-line no-control-regex + /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; +var PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/; +var PATTERN_FLOW_INDICATORS = /[,\[\]\{\}]/; +var PATTERN_TAG_HANDLE = /^(?:!|!!|![a-z\-]+!)$/i; +var PATTERN_TAG_URI = + /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; +function _class(obj) { + return Object.prototype.toString.call(obj); +} +function is_EOL(c) { + return c === 10 || c === 13; +} +function is_WHITE_SPACE(c) { + return c === 9 || c === 32; +} +function is_WS_OR_EOL(c) { + return c === 9 || c === 32 || c === 10 || c === 13; +} +function is_FLOW_INDICATOR(c) { + return c === 44 || c === 91 || c === 93 || c === 123 || c === 125; +} +function fromHexCode(c) { + var lc; + if (48 <= c && c <= 57) { + return c - 48; + } + lc = c | 32; + if (97 <= lc && lc <= 102) { + return lc - 97 + 10; + } + return -1; +} +function escapedHexLen(c) { + if (c === 120) { + return 2; + } + if (c === 117) { + return 4; + } + if (c === 85) { + return 8; + } + return 0; +} +function fromDecimalCode(c) { + if (48 <= c && c <= 57) { + return c - 48; + } + return -1; +} +function simpleEscapeSequence(c) { + return c === 48 + ? "\0" + : c === 97 + ? "\x07" + : c === 98 + ? "\b" + : c === 116 + ? " " + : c === 9 + ? " " + : c === 110 + ? "\n" + : c === 118 + ? "\v" + : c === 102 + ? "\f" + : c === 114 + ? "\r" + : c === 101 + ? "" + : c === 32 + ? " " + : c === 34 + ? '"' + : c === 47 + ? "/" + : c === 92 + ? "\\" + : c === 78 + ? "\x85" + : c === 95 + ? "\xA0" + : c === 76 + ? "\u2028" + : c === 80 + ? "\u2029" + : ""; +} +function charFromCodepoint(c) { + if (c <= 65535) { + return String.fromCharCode(c); + } + return String.fromCharCode( + ((c - 65536) >> 10) + 55296, + ((c - 65536) & 1023) + 56320 + ); +} +var simpleEscapeCheck = new Array(256); +var simpleEscapeMap = new Array(256); +for (var i = 0; i < 256; i++) { + simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0; + simpleEscapeMap[i] = simpleEscapeSequence(i); +} +function State$1(input, options) { + this.input = input; + this.filename = options["filename"] || null; + this.schema = options["schema"] || _default; + this.onWarning = options["onWarning"] || null; + this.legacy = options["legacy"] || false; + this.json = options["json"] || false; + this.listener = options["listener"] || null; + this.implicitTypes = this.schema.compiledImplicit; + this.typeMap = this.schema.compiledTypeMap; + this.length = input.length; + this.position = 0; + this.line = 0; + this.lineStart = 0; + this.lineIndent = 0; + this.firstTabInLine = -1; + this.documents = []; +} +function generateError(state, message) { + var mark = { + name: state.filename, + buffer: state.input.slice(0, -1), + position: state.position, + line: state.line, + column: state.position - state.lineStart, + }; + mark.snippet = snippet(mark); + return new exception(message, mark); +} +function throwError(state, message) { + throw generateError(state, message); +} +function throwWarning(state, message) { + if (state.onWarning) { + state.onWarning.call(null, generateError(state, message)); + } +} +var directiveHandlers = { + YAML: function handleYamlDirective(state, name, args) { + var match, major, minor; + if (state.version !== null) { + throwError(state, "duplication of %YAML directive"); + } + if (args.length !== 1) { + throwError(state, "YAML directive accepts exactly one argument"); + } + match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); + if (match === null) { + throwError(state, "ill-formed argument of the YAML directive"); + } + major = parseInt(match[1], 10); + minor = parseInt(match[2], 10); + if (major !== 1) { + throwError(state, "unacceptable YAML version of the document"); + } + state.version = args[0]; + state.checkLineBreaks = minor < 2; + if (minor !== 1 && minor !== 2) { + throwWarning(state, "unsupported YAML version of the document"); + } + }, + TAG: function handleTagDirective(state, name, args) { + var handle, prefix; + if (args.length !== 2) { + throwError(state, "TAG directive accepts exactly two arguments"); + } + handle = args[0]; + prefix = args[1]; + if (!PATTERN_TAG_HANDLE.test(handle)) { + throwError( + state, + "ill-formed tag handle (first argument) of the TAG directive" + ); + } + if (_hasOwnProperty$1.call(state.tagMap, handle)) { + throwError( + state, + 'there is a previously declared suffix for "' + handle + '" tag handle' + ); + } + if (!PATTERN_TAG_URI.test(prefix)) { + throwError( + state, + "ill-formed tag prefix (second argument) of the TAG directive" + ); + } + try { + prefix = decodeURIComponent(prefix); + } catch (err) { + throwError(state, "tag prefix is malformed: " + prefix); + } + state.tagMap[handle] = prefix; + }, +}; +function captureSegment(state, start, end, checkJson) { + var _position, _length, _character, _result; + if (start < end) { + _result = state.input.slice(start, end); + if (checkJson) { + for ( + _position = 0, _length = _result.length; + _position < _length; + _position += 1 + ) { + _character = _result.charCodeAt(_position); + if ( + !(_character === 9 || (32 <= _character && _character <= 1114111)) + ) { + throwError(state, "expected valid JSON character"); + } + } + } else if (PATTERN_NON_PRINTABLE.test(_result)) { + throwError(state, "the stream contains non-printable characters"); + } + state.result += _result; + } +} +function mergeMappings(state, destination, source, overridableKeys) { + var sourceKeys, key, index, quantity; + if (!common.isObject(source)) { + throwError( + state, + "cannot merge mappings; the provided source object is unacceptable" + ); + } + sourceKeys = Object.keys(source); + for (index = 0, quantity = sourceKeys.length; index < quantity; index += 1) { + key = sourceKeys[index]; + if (!_hasOwnProperty$1.call(destination, key)) { + destination[key] = source[key]; + overridableKeys[key] = true; + } + } +} +function storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + valueNode, + startLine, + startLineStart, + startPos +) { + var index, quantity; + if (Array.isArray(keyNode)) { + keyNode = Array.prototype.slice.call(keyNode); + for (index = 0, quantity = keyNode.length; index < quantity; index += 1) { + if (Array.isArray(keyNode[index])) { + throwError(state, "nested arrays are not supported inside keys"); + } + if ( + typeof keyNode === "object" && + _class(keyNode[index]) === "[object Object]" + ) { + keyNode[index] = "[object Object]"; + } + } + } + if (typeof keyNode === "object" && _class(keyNode) === "[object Object]") { + keyNode = "[object Object]"; + } + keyNode = String(keyNode); + if (_result === null) { + _result = {}; + } + if (keyTag === "tag:yaml.org,2002:merge") { + if (Array.isArray(valueNode)) { + for ( + index = 0, quantity = valueNode.length; + index < quantity; + index += 1 + ) { + mergeMappings(state, _result, valueNode[index], overridableKeys); + } + } else { + mergeMappings(state, _result, valueNode, overridableKeys); + } + } else { + if ( + !state.json && + !_hasOwnProperty$1.call(overridableKeys, keyNode) && + _hasOwnProperty$1.call(_result, keyNode) + ) { + state.line = startLine || state.line; + state.lineStart = startLineStart || state.lineStart; + state.position = startPos || state.position; + throwError(state, "duplicated mapping key"); + } + if (keyNode === "__proto__") { + Object.defineProperty(_result, keyNode, { + configurable: true, + enumerable: true, + writable: true, + value: valueNode, + }); + } else { + _result[keyNode] = valueNode; + } + delete overridableKeys[keyNode]; + } + return _result; +} +function readLineBreak(state) { + var ch; + ch = state.input.charCodeAt(state.position); + if (ch === 10) { + state.position++; + } else if (ch === 13) { + state.position++; + if (state.input.charCodeAt(state.position) === 10) { + state.position++; + } + } else { + throwError(state, "a line break is expected"); + } + state.line += 1; + state.lineStart = state.position; + state.firstTabInLine = -1; +} +function skipSeparationSpace(state, allowComments, checkIndent) { + var lineBreaks = 0, + ch = state.input.charCodeAt(state.position); + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + if (ch === 9 && state.firstTabInLine === -1) { + state.firstTabInLine = state.position; + } + ch = state.input.charCodeAt(++state.position); + } + if (allowComments && ch === 35) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 10 && ch !== 13 && ch !== 0); + } + if (is_EOL(ch)) { + readLineBreak(state); + ch = state.input.charCodeAt(state.position); + lineBreaks++; + state.lineIndent = 0; + while (ch === 32) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + } else { + break; + } + } + if ( + checkIndent !== -1 && + lineBreaks !== 0 && + state.lineIndent < checkIndent + ) { + throwWarning(state, "deficient indentation"); + } + return lineBreaks; +} +function testDocumentSeparator(state) { + var _position = state.position, + ch; + ch = state.input.charCodeAt(_position); + if ( + (ch === 45 || ch === 46) && + ch === state.input.charCodeAt(_position + 1) && + ch === state.input.charCodeAt(_position + 2) + ) { + _position += 3; + ch = state.input.charCodeAt(_position); + if (ch === 0 || is_WS_OR_EOL(ch)) { + return true; + } + } + return false; +} +function writeFoldedLines(state, count) { + if (count === 1) { + state.result += " "; + } else if (count > 1) { + state.result += common.repeat("\n", count - 1); + } +} +function readPlainScalar(state, nodeIndent, withinFlowCollection) { + var preceding, + following, + captureStart, + captureEnd, + hasPendingContent, + _line, + _lineStart, + _lineIndent, + _kind = state.kind, + _result = state.result, + ch; + ch = state.input.charCodeAt(state.position); + if ( + is_WS_OR_EOL(ch) || + is_FLOW_INDICATOR(ch) || + ch === 35 || + ch === 38 || + ch === 42 || + ch === 33 || + ch === 124 || + ch === 62 || + ch === 39 || + ch === 34 || + ch === 37 || + ch === 64 || + ch === 96 + ) { + return false; + } + if (ch === 63 || ch === 45) { + following = state.input.charCodeAt(state.position + 1); + if ( + is_WS_OR_EOL(following) || + (withinFlowCollection && is_FLOW_INDICATOR(following)) + ) { + return false; + } + } + state.kind = "scalar"; + state.result = ""; + captureStart = captureEnd = state.position; + hasPendingContent = false; + while (ch !== 0) { + if (ch === 58) { + following = state.input.charCodeAt(state.position + 1); + if ( + is_WS_OR_EOL(following) || + (withinFlowCollection && is_FLOW_INDICATOR(following)) + ) { + break; + } + } else if (ch === 35) { + preceding = state.input.charCodeAt(state.position - 1); + if (is_WS_OR_EOL(preceding)) { + break; + } + } else if ( + (state.position === state.lineStart && testDocumentSeparator(state)) || + (withinFlowCollection && is_FLOW_INDICATOR(ch)) + ) { + break; + } else if (is_EOL(ch)) { + _line = state.line; + _lineStart = state.lineStart; + _lineIndent = state.lineIndent; + skipSeparationSpace(state, false, -1); + if (state.lineIndent >= nodeIndent) { + hasPendingContent = true; + ch = state.input.charCodeAt(state.position); + continue; + } else { + state.position = captureEnd; + state.line = _line; + state.lineStart = _lineStart; + state.lineIndent = _lineIndent; + break; + } + } + if (hasPendingContent) { + captureSegment(state, captureStart, captureEnd, false); + writeFoldedLines(state, state.line - _line); + captureStart = captureEnd = state.position; + hasPendingContent = false; + } + if (!is_WHITE_SPACE(ch)) { + captureEnd = state.position + 1; + } + ch = state.input.charCodeAt(++state.position); + } + captureSegment(state, captureStart, captureEnd, false); + if (state.result) { + return true; + } + state.kind = _kind; + state.result = _result; + return false; +} +function readSingleQuotedScalar(state, nodeIndent) { + var ch, captureStart, captureEnd; + ch = state.input.charCodeAt(state.position); + if (ch !== 39) { + return false; + } + state.kind = "scalar"; + state.result = ""; + state.position++; + captureStart = captureEnd = state.position; + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 39) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + if (ch === 39) { + captureStart = state.position; + state.position++; + captureEnd = state.position; + } else { + return true; + } + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + } else if ( + state.position === state.lineStart && + testDocumentSeparator(state) + ) { + throwError( + state, + "unexpected end of the document within a single quoted scalar" + ); + } else { + state.position++; + captureEnd = state.position; + } + } + throwError( + state, + "unexpected end of the stream within a single quoted scalar" + ); +} +function readDoubleQuotedScalar(state, nodeIndent) { + var captureStart, captureEnd, hexLength, hexResult, tmp, ch; + ch = state.input.charCodeAt(state.position); + if (ch !== 34) { + return false; + } + state.kind = "scalar"; + state.result = ""; + state.position++; + captureStart = captureEnd = state.position; + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 34) { + captureSegment(state, captureStart, state.position, true); + state.position++; + return true; + } else if (ch === 92) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + if (is_EOL(ch)) { + skipSeparationSpace(state, false, nodeIndent); + } else if (ch < 256 && simpleEscapeCheck[ch]) { + state.result += simpleEscapeMap[ch]; + state.position++; + } else if ((tmp = escapedHexLen(ch)) > 0) { + hexLength = tmp; + hexResult = 0; + for (; hexLength > 0; hexLength--) { + ch = state.input.charCodeAt(++state.position); + if ((tmp = fromHexCode(ch)) >= 0) { + hexResult = (hexResult << 4) + tmp; + } else { + throwError(state, "expected hexadecimal character"); + } + } + state.result += charFromCodepoint(hexResult); + state.position++; + } else { + throwError(state, "unknown escape sequence"); + } + captureStart = captureEnd = state.position; + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + } else if ( + state.position === state.lineStart && + testDocumentSeparator(state) + ) { + throwError( + state, + "unexpected end of the document within a double quoted scalar" + ); + } else { + state.position++; + captureEnd = state.position; + } + } + throwError( + state, + "unexpected end of the stream within a double quoted scalar" + ); +} +function readFlowCollection(state, nodeIndent) { + var readNext = true, + _line, + _lineStart, + _pos, + _tag = state.tag, + _result, + _anchor = state.anchor, + following, + terminator, + isPair, + isExplicitPair, + isMapping, + overridableKeys = Object.create(null), + keyNode, + keyTag, + valueNode, + ch; + ch = state.input.charCodeAt(state.position); + if (ch === 91) { + terminator = 93; + isMapping = false; + _result = []; + } else if (ch === 123) { + terminator = 125; + isMapping = true; + _result = {}; + } else { + return false; + } + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + ch = state.input.charCodeAt(++state.position); + while (ch !== 0) { + skipSeparationSpace(state, true, nodeIndent); + ch = state.input.charCodeAt(state.position); + if (ch === terminator) { + state.position++; + state.tag = _tag; + state.anchor = _anchor; + state.kind = isMapping ? "mapping" : "sequence"; + state.result = _result; + return true; + } else if (!readNext) { + throwError(state, "missed comma between flow collection entries"); + } else if (ch === 44) { + throwError(state, "expected the node content, but found ','"); + } + keyTag = keyNode = valueNode = null; + isPair = isExplicitPair = false; + if (ch === 63) { + following = state.input.charCodeAt(state.position + 1); + if (is_WS_OR_EOL(following)) { + isPair = isExplicitPair = true; + state.position++; + skipSeparationSpace(state, true, nodeIndent); + } + } + _line = state.line; + _lineStart = state.lineStart; + _pos = state.position; + composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); + keyTag = state.tag; + keyNode = state.result; + skipSeparationSpace(state, true, nodeIndent); + ch = state.input.charCodeAt(state.position); + if ((isExplicitPair || state.line === _line) && ch === 58) { + isPair = true; + ch = state.input.charCodeAt(++state.position); + skipSeparationSpace(state, true, nodeIndent); + composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); + valueNode = state.result; + } + if (isMapping) { + storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + valueNode, + _line, + _lineStart, + _pos + ); + } else if (isPair) { + _result.push( + storeMappingPair( + state, + null, + overridableKeys, + keyTag, + keyNode, + valueNode, + _line, + _lineStart, + _pos + ) + ); + } else { + _result.push(keyNode); + } + skipSeparationSpace(state, true, nodeIndent); + ch = state.input.charCodeAt(state.position); + if (ch === 44) { + readNext = true; + ch = state.input.charCodeAt(++state.position); + } else { + readNext = false; + } + } + throwError(state, "unexpected end of the stream within a flow collection"); +} +function readBlockScalar(state, nodeIndent) { + var captureStart, + folding, + chomping = CHOMPING_CLIP, + didReadContent = false, + detectedIndent = false, + textIndent = nodeIndent, + emptyLines = 0, + atMoreIndented = false, + tmp, + ch; + ch = state.input.charCodeAt(state.position); + if (ch === 124) { + folding = false; + } else if (ch === 62) { + folding = true; + } else { + return false; + } + state.kind = "scalar"; + state.result = ""; + while (ch !== 0) { + ch = state.input.charCodeAt(++state.position); + if (ch === 43 || ch === 45) { + if (CHOMPING_CLIP === chomping) { + chomping = ch === 43 ? CHOMPING_KEEP : CHOMPING_STRIP; + } else { + throwError(state, "repeat of a chomping mode identifier"); + } + } else if ((tmp = fromDecimalCode(ch)) >= 0) { + if (tmp === 0) { + throwError( + state, + "bad explicit indentation width of a block scalar; it cannot be less than one" + ); + } else if (!detectedIndent) { + textIndent = nodeIndent + tmp - 1; + detectedIndent = true; + } else { + throwError(state, "repeat of an indentation width identifier"); + } + } else { + break; + } + } + if (is_WHITE_SPACE(ch)) { + do { + ch = state.input.charCodeAt(++state.position); + } while (is_WHITE_SPACE(ch)); + if (ch === 35) { + do { + ch = state.input.charCodeAt(++state.position); + } while (!is_EOL(ch) && ch !== 0); + } + } + while (ch !== 0) { + readLineBreak(state); + state.lineIndent = 0; + ch = state.input.charCodeAt(state.position); + while ((!detectedIndent || state.lineIndent < textIndent) && ch === 32) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + if (!detectedIndent && state.lineIndent > textIndent) { + textIndent = state.lineIndent; + } + if (is_EOL(ch)) { + emptyLines++; + continue; + } + if (state.lineIndent < textIndent) { + if (chomping === CHOMPING_KEEP) { + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + } else if (chomping === CHOMPING_CLIP) { + if (didReadContent) { + state.result += "\n"; + } + } + break; + } + if (folding) { + if (is_WHITE_SPACE(ch)) { + atMoreIndented = true; + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + } else if (atMoreIndented) { + atMoreIndented = false; + state.result += common.repeat("\n", emptyLines + 1); + } else if (emptyLines === 0) { + if (didReadContent) { + state.result += " "; + } + } else { + state.result += common.repeat("\n", emptyLines); + } + } else { + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + } + didReadContent = true; + detectedIndent = true; + emptyLines = 0; + captureStart = state.position; + while (!is_EOL(ch) && ch !== 0) { + ch = state.input.charCodeAt(++state.position); + } + captureSegment(state, captureStart, state.position, false); + } + return true; +} +function readBlockSequence(state, nodeIndent) { + var _line, + _tag = state.tag, + _anchor = state.anchor, + _result = [], + following, + detected = false, + ch; + if (state.firstTabInLine !== -1) return false; + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + ch = state.input.charCodeAt(state.position); + while (ch !== 0) { + if (state.firstTabInLine !== -1) { + state.position = state.firstTabInLine; + throwError(state, "tab characters must not be used in indentation"); + } + if (ch !== 45) { + break; + } + following = state.input.charCodeAt(state.position + 1); + if (!is_WS_OR_EOL(following)) { + break; + } + detected = true; + state.position++; + if (skipSeparationSpace(state, true, -1)) { + if (state.lineIndent <= nodeIndent) { + _result.push(null); + ch = state.input.charCodeAt(state.position); + continue; + } + } + _line = state.line; + composeNode(state, nodeIndent, CONTEXT_BLOCK_IN, false, true); + _result.push(state.result); + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + if ((state.line === _line || state.lineIndent > nodeIndent) && ch !== 0) { + throwError(state, "bad indentation of a sequence entry"); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = "sequence"; + state.result = _result; + return true; + } + return false; +} +function readBlockMapping(state, nodeIndent, flowIndent) { + var following, + allowCompact, + _line, + _keyLine, + _keyLineStart, + _keyPos, + _tag = state.tag, + _anchor = state.anchor, + _result = {}, + overridableKeys = Object.create(null), + keyTag = null, + keyNode = null, + valueNode = null, + atExplicitKey = false, + detected = false, + ch; + if (state.firstTabInLine !== -1) return false; + if (state.anchor !== null) { + state.anchorMap[state.anchor] = _result; + } + ch = state.input.charCodeAt(state.position); + while (ch !== 0) { + if (!atExplicitKey && state.firstTabInLine !== -1) { + state.position = state.firstTabInLine; + throwError(state, "tab characters must not be used in indentation"); + } + following = state.input.charCodeAt(state.position + 1); + _line = state.line; + if ((ch === 63 || ch === 58) && is_WS_OR_EOL(following)) { + if (ch === 63) { + if (atExplicitKey) { + storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + null, + _keyLine, + _keyLineStart, + _keyPos + ); + keyTag = keyNode = valueNode = null; + } + detected = true; + atExplicitKey = true; + allowCompact = true; + } else if (atExplicitKey) { + atExplicitKey = false; + allowCompact = true; + } else { + throwError( + state, + "incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line" + ); + } + state.position += 1; + ch = following; + } else { + _keyLine = state.line; + _keyLineStart = state.lineStart; + _keyPos = state.position; + if (!composeNode(state, flowIndent, CONTEXT_FLOW_OUT, false, true)) { + break; + } + if (state.line === _line) { + ch = state.input.charCodeAt(state.position); + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + if (ch === 58) { + ch = state.input.charCodeAt(++state.position); + if (!is_WS_OR_EOL(ch)) { + throwError( + state, + "a whitespace character is expected after the key-value separator within a block mapping" + ); + } + if (atExplicitKey) { + storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + null, + _keyLine, + _keyLineStart, + _keyPos + ); + keyTag = keyNode = valueNode = null; + } + detected = true; + atExplicitKey = false; + allowCompact = false; + keyTag = state.tag; + keyNode = state.result; + } else if (detected) { + throwError( + state, + "can not read an implicit mapping pair; a colon is missed" + ); + } else { + state.tag = _tag; + state.anchor = _anchor; + return true; + } + } else if (detected) { + throwError( + state, + "can not read a block mapping entry; a multiline key may not be an implicit key" + ); + } else { + state.tag = _tag; + state.anchor = _anchor; + return true; + } + } + if (state.line === _line || state.lineIndent > nodeIndent) { + if (atExplicitKey) { + _keyLine = state.line; + _keyLineStart = state.lineStart; + _keyPos = state.position; + } + if ( + composeNode(state, nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact) + ) { + if (atExplicitKey) { + keyNode = state.result; + } else { + valueNode = state.result; + } + } + if (!atExplicitKey) { + storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + valueNode, + _keyLine, + _keyLineStart, + _keyPos + ); + keyTag = keyNode = valueNode = null; + } + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + } + if ((state.line === _line || state.lineIndent > nodeIndent) && ch !== 0) { + throwError(state, "bad indentation of a mapping entry"); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + if (atExplicitKey) { + storeMappingPair( + state, + _result, + overridableKeys, + keyTag, + keyNode, + null, + _keyLine, + _keyLineStart, + _keyPos + ); + } + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = "mapping"; + state.result = _result; + } + return detected; +} +function readTagProperty(state) { + var _position, + isVerbatim = false, + isNamed = false, + tagHandle, + tagName, + ch; + ch = state.input.charCodeAt(state.position); + if (ch !== 33) return false; + if (state.tag !== null) { + throwError(state, "duplication of a tag property"); + } + ch = state.input.charCodeAt(++state.position); + if (ch === 60) { + isVerbatim = true; + ch = state.input.charCodeAt(++state.position); + } else if (ch === 33) { + isNamed = true; + tagHandle = "!!"; + ch = state.input.charCodeAt(++state.position); + } else { + tagHandle = "!"; + } + _position = state.position; + if (isVerbatim) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0 && ch !== 62); + if (state.position < state.length) { + tagName = state.input.slice(_position, state.position); + ch = state.input.charCodeAt(++state.position); + } else { + throwError(state, "unexpected end of the stream within a verbatim tag"); + } + } else { + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + if (ch === 33) { + if (!isNamed) { + tagHandle = state.input.slice(_position - 1, state.position + 1); + if (!PATTERN_TAG_HANDLE.test(tagHandle)) { + throwError( + state, + "named tag handle cannot contain such characters" + ); + } + isNamed = true; + _position = state.position + 1; + } else { + throwError(state, "tag suffix cannot contain exclamation marks"); + } + } + ch = state.input.charCodeAt(++state.position); + } + tagName = state.input.slice(_position, state.position); + if (PATTERN_FLOW_INDICATORS.test(tagName)) { + throwError(state, "tag suffix cannot contain flow indicator characters"); + } + } + if (tagName && !PATTERN_TAG_URI.test(tagName)) { + throwError(state, "tag name cannot contain such characters: " + tagName); + } + try { + tagName = decodeURIComponent(tagName); + } catch (err) { + throwError(state, "tag name is malformed: " + tagName); + } + if (isVerbatim) { + state.tag = tagName; + } else if (_hasOwnProperty$1.call(state.tagMap, tagHandle)) { + state.tag = state.tagMap[tagHandle] + tagName; + } else if (tagHandle === "!") { + state.tag = "!" + tagName; + } else if (tagHandle === "!!") { + state.tag = "tag:yaml.org,2002:" + tagName; + } else { + throwError(state, 'undeclared tag handle "' + tagHandle + '"'); + } + return true; +} +function readAnchorProperty(state) { + var _position, ch; + ch = state.input.charCodeAt(state.position); + if (ch !== 38) return false; + if (state.anchor !== null) { + throwError(state, "duplication of an anchor property"); + } + ch = state.input.charCodeAt(++state.position); + _position = state.position; + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + if (state.position === _position) { + throwError( + state, + "name of an anchor node must contain at least one character" + ); + } + state.anchor = state.input.slice(_position, state.position); + return true; +} +function readAlias(state) { + var _position, alias, ch; + ch = state.input.charCodeAt(state.position); + if (ch !== 42) return false; + ch = state.input.charCodeAt(++state.position); + _position = state.position; + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + if (state.position === _position) { + throwError( + state, + "name of an alias node must contain at least one character" + ); + } + alias = state.input.slice(_position, state.position); + if (!_hasOwnProperty$1.call(state.anchorMap, alias)) { + throwError(state, 'unidentified alias "' + alias + '"'); + } + state.result = state.anchorMap[alias]; + skipSeparationSpace(state, true, -1); + return true; +} +function composeNode( + state, + parentIndent, + nodeContext, + allowToSeek, + allowCompact +) { + var allowBlockStyles, + allowBlockScalars, + allowBlockCollections, + indentStatus = 1, + atNewLine = false, + hasContent = false, + typeIndex, + typeQuantity, + typeList, + type2, + flowIndent, + blockIndent; + if (state.listener !== null) { + state.listener("open", state); + } + state.tag = null; + state.anchor = null; + state.kind = null; + state.result = null; + allowBlockStyles = + allowBlockScalars = + allowBlockCollections = + CONTEXT_BLOCK_OUT === nodeContext || CONTEXT_BLOCK_IN === nodeContext; + if (allowToSeek) { + if (skipSeparationSpace(state, true, -1)) { + atNewLine = true; + if (state.lineIndent > parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } + } + if (indentStatus === 1) { + while (readTagProperty(state) || readAnchorProperty(state)) { + if (skipSeparationSpace(state, true, -1)) { + atNewLine = true; + allowBlockCollections = allowBlockStyles; + if (state.lineIndent > parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } else { + allowBlockCollections = false; + } + } + } + if (allowBlockCollections) { + allowBlockCollections = atNewLine || allowCompact; + } + if (indentStatus === 1 || CONTEXT_BLOCK_OUT === nodeContext) { + if (CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext) { + flowIndent = parentIndent; + } else { + flowIndent = parentIndent + 1; + } + blockIndent = state.position - state.lineStart; + if (indentStatus === 1) { + if ( + (allowBlockCollections && + (readBlockSequence(state, blockIndent) || + readBlockMapping(state, blockIndent, flowIndent))) || + readFlowCollection(state, flowIndent) + ) { + hasContent = true; + } else { + if ( + (allowBlockScalars && readBlockScalar(state, flowIndent)) || + readSingleQuotedScalar(state, flowIndent) || + readDoubleQuotedScalar(state, flowIndent) + ) { + hasContent = true; + } else if (readAlias(state)) { + hasContent = true; + if (state.tag !== null || state.anchor !== null) { + throwError(state, "alias node should not have any properties"); + } + } else if ( + readPlainScalar(state, flowIndent, CONTEXT_FLOW_IN === nodeContext) + ) { + hasContent = true; + if (state.tag === null) { + state.tag = "?"; + } + } + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } else if (indentStatus === 0) { + hasContent = + allowBlockCollections && readBlockSequence(state, blockIndent); + } + } + if (state.tag === null) { + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } else if (state.tag === "?") { + if (state.result !== null && state.kind !== "scalar") { + throwError( + state, + 'unacceptable node kind for ! tag; it should be "scalar", not "' + + state.kind + + '"' + ); + } + for ( + typeIndex = 0, typeQuantity = state.implicitTypes.length; + typeIndex < typeQuantity; + typeIndex += 1 + ) { + type2 = state.implicitTypes[typeIndex]; + if (type2.resolve(state.result)) { + state.result = type2.construct(state.result); + state.tag = type2.tag; + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + break; + } + } + } else if (state.tag !== "!") { + if ( + _hasOwnProperty$1.call(state.typeMap[state.kind || "fallback"], state.tag) + ) { + type2 = state.typeMap[state.kind || "fallback"][state.tag]; + } else { + type2 = null; + typeList = state.typeMap.multi[state.kind || "fallback"]; + for ( + typeIndex = 0, typeQuantity = typeList.length; + typeIndex < typeQuantity; + typeIndex += 1 + ) { + if ( + state.tag.slice(0, typeList[typeIndex].tag.length) === + typeList[typeIndex].tag + ) { + type2 = typeList[typeIndex]; + break; + } + } + } + if (!type2) { + throwError(state, "unknown tag !<" + state.tag + ">"); + } + if (state.result !== null && type2.kind !== state.kind) { + throwError( + state, + "unacceptable node kind for !<" + + state.tag + + '> tag; it should be "' + + type2.kind + + '", not "' + + state.kind + + '"' + ); + } + if (!type2.resolve(state.result, state.tag)) { + throwError( + state, + "cannot resolve a node with !<" + state.tag + "> explicit tag" + ); + } else { + state.result = type2.construct(state.result, state.tag); + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } + if (state.listener !== null) { + state.listener("close", state); + } + return state.tag !== null || state.anchor !== null || hasContent; +} +function readDocument(state) { + var documentStart = state.position, + _position, + directiveName, + directiveArgs, + hasDirectives = false, + ch; + state.version = null; + state.checkLineBreaks = state.legacy; + state.tagMap = Object.create(null); + state.anchorMap = Object.create(null); + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + if (state.lineIndent > 0 || ch !== 37) { + break; + } + hasDirectives = true; + ch = state.input.charCodeAt(++state.position); + _position = state.position; + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + directiveName = state.input.slice(_position, state.position); + directiveArgs = []; + if (directiveName.length < 1) { + throwError( + state, + "directive name must not be less than one character in length" + ); + } + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + if (ch === 35) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0 && !is_EOL(ch)); + break; + } + if (is_EOL(ch)) break; + _position = state.position; + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + directiveArgs.push(state.input.slice(_position, state.position)); + } + if (ch !== 0) readLineBreak(state); + if (_hasOwnProperty$1.call(directiveHandlers, directiveName)) { + directiveHandlers[directiveName](state, directiveName, directiveArgs); + } else { + throwWarning(state, 'unknown document directive "' + directiveName + '"'); + } + } + skipSeparationSpace(state, true, -1); + if ( + state.lineIndent === 0 && + state.input.charCodeAt(state.position) === 45 && + state.input.charCodeAt(state.position + 1) === 45 && + state.input.charCodeAt(state.position + 2) === 45 + ) { + state.position += 3; + skipSeparationSpace(state, true, -1); + } else if (hasDirectives) { + throwError(state, "directives end mark is expected"); + } + composeNode(state, state.lineIndent - 1, CONTEXT_BLOCK_OUT, false, true); + skipSeparationSpace(state, true, -1); + if ( + state.checkLineBreaks && + PATTERN_NON_ASCII_LINE_BREAKS.test( + state.input.slice(documentStart, state.position) + ) + ) { + throwWarning(state, "non-ASCII line breaks are interpreted as content"); + } + state.documents.push(state.result); + if (state.position === state.lineStart && testDocumentSeparator(state)) { + if (state.input.charCodeAt(state.position) === 46) { + state.position += 3; + skipSeparationSpace(state, true, -1); + } + return; + } + if (state.position < state.length - 1) { + throwError(state, "end of the stream or a document separator is expected"); + } else { + return; + } +} +function loadDocuments(input, options) { + input = String(input); + options = options || {}; + if (input.length !== 0) { + if ( + input.charCodeAt(input.length - 1) !== 10 && + input.charCodeAt(input.length - 1) !== 13 + ) { + input += "\n"; + } + if (input.charCodeAt(0) === 65279) { + input = input.slice(1); + } + } + var state = new State$1(input, options); + var nullpos = input.indexOf("\0"); + if (nullpos !== -1) { + state.position = nullpos; + throwError(state, "null byte is not allowed in input"); + } + state.input += "\0"; + while (state.input.charCodeAt(state.position) === 32) { + state.lineIndent += 1; + state.position += 1; + } + while (state.position < state.length - 1) { + readDocument(state); + } + return state.documents; +} +function loadAll$1(input, iterator, options) { + if ( + iterator !== null && + typeof iterator === "object" && + typeof options === "undefined" + ) { + options = iterator; + iterator = null; + } + var documents = loadDocuments(input, options); + if (typeof iterator !== "function") { + return documents; + } + for (var index = 0, length = documents.length; index < length; index += 1) { + iterator(documents[index]); + } +} +function load$1(input, options) { + var documents = loadDocuments(input, options); + if (documents.length === 0) { + return void 0; + } else if (documents.length === 1) { + return documents[0]; + } + throw new exception( + "expected a single document in the stream, but found more" + ); +} +var loadAll_1 = loadAll$1; +var load_1 = load$1; +var loader = { + loadAll: loadAll_1, + load: load_1, +}; +var _toString = Object.prototype.toString; +var _hasOwnProperty = Object.prototype.hasOwnProperty; +var CHAR_BOM = 65279; +var CHAR_TAB = 9; +var CHAR_LINE_FEED = 10; +var CHAR_CARRIAGE_RETURN = 13; +var CHAR_SPACE = 32; +var CHAR_EXCLAMATION = 33; +var CHAR_DOUBLE_QUOTE = 34; +var CHAR_SHARP = 35; +var CHAR_PERCENT = 37; +var CHAR_AMPERSAND = 38; +var CHAR_SINGLE_QUOTE = 39; +var CHAR_ASTERISK = 42; +var CHAR_COMMA = 44; +var CHAR_MINUS = 45; +var CHAR_COLON = 58; +var CHAR_EQUALS = 61; +var CHAR_GREATER_THAN = 62; +var CHAR_QUESTION = 63; +var CHAR_COMMERCIAL_AT = 64; +var CHAR_LEFT_SQUARE_BRACKET = 91; +var CHAR_RIGHT_SQUARE_BRACKET = 93; +var CHAR_GRAVE_ACCENT = 96; +var CHAR_LEFT_CURLY_BRACKET = 123; +var CHAR_VERTICAL_LINE = 124; +var CHAR_RIGHT_CURLY_BRACKET = 125; +var ESCAPE_SEQUENCES = {}; +ESCAPE_SEQUENCES[0] = "\\0"; +ESCAPE_SEQUENCES[7] = "\\a"; +ESCAPE_SEQUENCES[8] = "\\b"; +ESCAPE_SEQUENCES[9] = "\\t"; +ESCAPE_SEQUENCES[10] = "\\n"; +ESCAPE_SEQUENCES[11] = "\\v"; +ESCAPE_SEQUENCES[12] = "\\f"; +ESCAPE_SEQUENCES[13] = "\\r"; +ESCAPE_SEQUENCES[27] = "\\e"; +ESCAPE_SEQUENCES[34] = '\\"'; +ESCAPE_SEQUENCES[92] = "\\\\"; +ESCAPE_SEQUENCES[133] = "\\N"; +ESCAPE_SEQUENCES[160] = "\\_"; +ESCAPE_SEQUENCES[8232] = "\\L"; +ESCAPE_SEQUENCES[8233] = "\\P"; +var DEPRECATED_BOOLEANS_SYNTAX = [ + "y", + "Y", + "yes", + "Yes", + "YES", + "on", + "On", + "ON", + "n", + "N", + "no", + "No", + "NO", + "off", + "Off", + "OFF", +]; +var DEPRECATED_BASE60_SYNTAX = /^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/; +function compileStyleMap(schema2, map2) { + var result, keys, index, length, tag, style, type2; + if (map2 === null) return {}; + result = {}; + keys = Object.keys(map2); + for (index = 0, length = keys.length; index < length; index += 1) { + tag = keys[index]; + style = String(map2[tag]); + if (tag.slice(0, 2) === "!!") { + tag = "tag:yaml.org,2002:" + tag.slice(2); + } + type2 = schema2.compiledTypeMap["fallback"][tag]; + if (type2 && _hasOwnProperty.call(type2.styleAliases, style)) { + style = type2.styleAliases[style]; + } + result[tag] = style; + } + return result; +} +function encodeHex(character) { + var string, handle, length; + string = character.toString(16).toUpperCase(); + if (character <= 255) { + handle = "x"; + length = 2; + } else if (character <= 65535) { + handle = "u"; + length = 4; + } else if (character <= 4294967295) { + handle = "U"; + length = 8; + } else { + throw new exception( + "code point within a string may not be greater than 0xFFFFFFFF" + ); + } + return "\\" + handle + common.repeat("0", length - string.length) + string; +} +var QUOTING_TYPE_SINGLE = 1, + QUOTING_TYPE_DOUBLE = 2; +function State(options) { + this.schema = options["schema"] || _default; + this.indent = Math.max(1, options["indent"] || 2); + this.noArrayIndent = options["noArrayIndent"] || false; + this.skipInvalid = options["skipInvalid"] || false; + this.flowLevel = common.isNothing(options["flowLevel"]) + ? -1 + : options["flowLevel"]; + this.styleMap = compileStyleMap(this.schema, options["styles"] || null); + this.sortKeys = options["sortKeys"] || false; + this.lineWidth = options["lineWidth"] || 80; + this.noRefs = options["noRefs"] || false; + this.noCompatMode = options["noCompatMode"] || false; + this.condenseFlow = options["condenseFlow"] || false; + this.quotingType = + options["quotingType"] === '"' ? QUOTING_TYPE_DOUBLE : QUOTING_TYPE_SINGLE; + this.forceQuotes = options["forceQuotes"] || false; + this.replacer = + typeof options["replacer"] === "function" ? options["replacer"] : null; + this.implicitTypes = this.schema.compiledImplicit; + this.explicitTypes = this.schema.compiledExplicit; + this.tag = null; + this.result = ""; + this.duplicates = []; + this.usedDuplicates = null; +} +function indentString(string, spaces) { + var ind = common.repeat(" ", spaces), + position = 0, + next = -1, + result = "", + line, + length = string.length; + while (position < length) { + next = string.indexOf("\n", position); + if (next === -1) { + line = string.slice(position); + position = length; + } else { + line = string.slice(position, next + 1); + position = next + 1; + } + if (line.length && line !== "\n") result += ind; + result += line; + } + return result; +} +function generateNextLine(state, level) { + return "\n" + common.repeat(" ", state.indent * level); +} +function testImplicitResolving(state, str2) { + var index, length, type2; + for ( + index = 0, length = state.implicitTypes.length; + index < length; + index += 1 + ) { + type2 = state.implicitTypes[index]; + if (type2.resolve(str2)) { + return true; + } + } + return false; +} +function isWhitespace(c) { + return c === CHAR_SPACE || c === CHAR_TAB; +} +function isPrintable(c) { + return ( + (32 <= c && c <= 126) || + (161 <= c && c <= 55295 && c !== 8232 && c !== 8233) || + (57344 <= c && c <= 65533 && c !== CHAR_BOM) || + (65536 <= c && c <= 1114111) + ); +} +function isNsCharOrWhitespace(c) { + return ( + isPrintable(c) && + c !== CHAR_BOM && + c !== CHAR_CARRIAGE_RETURN && + c !== CHAR_LINE_FEED + ); +} +function isPlainSafe(c, prev, inblock) { + var cIsNsCharOrWhitespace = isNsCharOrWhitespace(c); + var cIsNsChar = cIsNsCharOrWhitespace && !isWhitespace(c); + return ( + ((inblock + ? cIsNsCharOrWhitespace + : cIsNsCharOrWhitespace && + c !== CHAR_COMMA && + c !== CHAR_LEFT_SQUARE_BRACKET && + c !== CHAR_RIGHT_SQUARE_BRACKET && + c !== CHAR_LEFT_CURLY_BRACKET && + c !== CHAR_RIGHT_CURLY_BRACKET) && + c !== CHAR_SHARP && + !(prev === CHAR_COLON && !cIsNsChar)) || + (isNsCharOrWhitespace(prev) && !isWhitespace(prev) && c === CHAR_SHARP) || + (prev === CHAR_COLON && cIsNsChar) + ); +} +function isPlainSafeFirst(c) { + return ( + isPrintable(c) && + c !== CHAR_BOM && + !isWhitespace(c) && + c !== CHAR_MINUS && + c !== CHAR_QUESTION && + c !== CHAR_COLON && + c !== CHAR_COMMA && + c !== CHAR_LEFT_SQUARE_BRACKET && + c !== CHAR_RIGHT_SQUARE_BRACKET && + c !== CHAR_LEFT_CURLY_BRACKET && + c !== CHAR_RIGHT_CURLY_BRACKET && + c !== CHAR_SHARP && + c !== CHAR_AMPERSAND && + c !== CHAR_ASTERISK && + c !== CHAR_EXCLAMATION && + c !== CHAR_VERTICAL_LINE && + c !== CHAR_EQUALS && + c !== CHAR_GREATER_THAN && + c !== CHAR_SINGLE_QUOTE && + c !== CHAR_DOUBLE_QUOTE && + c !== CHAR_PERCENT && + c !== CHAR_COMMERCIAL_AT && + c !== CHAR_GRAVE_ACCENT + ); +} +function isPlainSafeLast(c) { + return !isWhitespace(c) && c !== CHAR_COLON; +} +function codePointAt(string, pos) { + var first = string.charCodeAt(pos), + second; + if (first >= 55296 && first <= 56319 && pos + 1 < string.length) { + second = string.charCodeAt(pos + 1); + if (second >= 56320 && second <= 57343) { + return (first - 55296) * 1024 + second - 56320 + 65536; + } + } + return first; +} +function needIndentIndicator(string) { + var leadingSpaceRe = /^\n* /; + return leadingSpaceRe.test(string); +} +var STYLE_PLAIN = 1, + STYLE_SINGLE = 2, + STYLE_LITERAL = 3, + STYLE_FOLDED = 4, + STYLE_DOUBLE = 5; +function chooseScalarStyle( + string, + singleLineOnly, + indentPerLevel, + lineWidth, + testAmbiguousType, + quotingType, + forceQuotes, + inblock +) { + var i; + var char = 0; + var prevChar = null; + var hasLineBreak = false; + var hasFoldableLine = false; + var shouldTrackWidth = lineWidth !== -1; + var previousLineBreak = -1; + var plain = + isPlainSafeFirst(codePointAt(string, 0)) && + isPlainSafeLast(codePointAt(string, string.length - 1)); + if (singleLineOnly || forceQuotes) { + for (i = 0; i < string.length; char >= 65536 ? (i += 2) : i++) { + char = codePointAt(string, i); + if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char, prevChar, inblock); + prevChar = char; + } + } else { + for (i = 0; i < string.length; char >= 65536 ? (i += 2) : i++) { + char = codePointAt(string, i); + if (char === CHAR_LINE_FEED) { + hasLineBreak = true; + if (shouldTrackWidth) { + hasFoldableLine = + hasFoldableLine || + (i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== " "); + previousLineBreak = i; + } + } else if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char, prevChar, inblock); + prevChar = char; + } + hasFoldableLine = + hasFoldableLine || + (shouldTrackWidth && + i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== " "); + } + if (!hasLineBreak && !hasFoldableLine) { + if (plain && !forceQuotes && !testAmbiguousType(string)) { + return STYLE_PLAIN; + } + return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE; + } + if (indentPerLevel > 9 && needIndentIndicator(string)) { + return STYLE_DOUBLE; + } + if (!forceQuotes) { + return hasFoldableLine ? STYLE_FOLDED : STYLE_LITERAL; + } + return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE; +} +function writeScalar(state, string, level, iskey, inblock) { + state.dump = (function () { + if (string.length === 0) { + return state.quotingType === QUOTING_TYPE_DOUBLE ? '""' : "''"; + } + if (!state.noCompatMode) { + if ( + DEPRECATED_BOOLEANS_SYNTAX.indexOf(string) !== -1 || + DEPRECATED_BASE60_SYNTAX.test(string) + ) { + return state.quotingType === QUOTING_TYPE_DOUBLE + ? '"' + string + '"' + : "'" + string + "'"; + } + } + var indent = state.indent * Math.max(1, level); + var lineWidth = + state.lineWidth === -1 + ? -1 + : Math.max(Math.min(state.lineWidth, 40), state.lineWidth - indent); + var singleLineOnly = + iskey || (state.flowLevel > -1 && level >= state.flowLevel); + function testAmbiguity(string2) { + return testImplicitResolving(state, string2); + } + switch ( + chooseScalarStyle( + string, + singleLineOnly, + state.indent, + lineWidth, + testAmbiguity, + state.quotingType, + state.forceQuotes && !iskey, + inblock + ) + ) { + case STYLE_PLAIN: + return string; + case STYLE_SINGLE: + return "'" + string.replace(/'/g, "''") + "'"; + case STYLE_LITERAL: + return ( + "|" + + blockHeader(string, state.indent) + + dropEndingNewline(indentString(string, indent)) + ); + case STYLE_FOLDED: + return ( + ">" + + blockHeader(string, state.indent) + + dropEndingNewline(indentString(foldString(string, lineWidth), indent)) + ); + case STYLE_DOUBLE: + return '"' + escapeString(string) + '"'; + default: + throw new exception("impossible error: invalid scalar style"); + } + })(); +} +function blockHeader(string, indentPerLevel) { + var indentIndicator = needIndentIndicator(string) + ? String(indentPerLevel) + : ""; + var clip = string[string.length - 1] === "\n"; + var keep = clip && (string[string.length - 2] === "\n" || string === "\n"); + var chomp = keep ? "+" : clip ? "" : "-"; + return indentIndicator + chomp + "\n"; +} +function dropEndingNewline(string) { + return string[string.length - 1] === "\n" ? string.slice(0, -1) : string; +} +function foldString(string, width) { + var lineRe = /(\n+)([^\n]*)/g; + var result = (function () { + var nextLF = string.indexOf("\n"); + nextLF = nextLF !== -1 ? nextLF : string.length; + lineRe.lastIndex = nextLF; + return foldLine(string.slice(0, nextLF), width); + })(); + var prevMoreIndented = string[0] === "\n" || string[0] === " "; + var moreIndented; + var match; + while ((match = lineRe.exec(string))) { + var prefix = match[1], + line = match[2]; + moreIndented = line[0] === " "; + result += + prefix + + (!prevMoreIndented && !moreIndented && line !== "" ? "\n" : "") + + foldLine(line, width); + prevMoreIndented = moreIndented; + } + return result; +} +function foldLine(line, width) { + if (line === "" || line[0] === " ") return line; + var breakRe = / [^ ]/g; + var match; + var start = 0, + end, + curr = 0, + next = 0; + var result = ""; + while ((match = breakRe.exec(line))) { + next = match.index; + if (next - start > width) { + end = curr > start ? curr : next; + result += "\n" + line.slice(start, end); + start = end + 1; + } + curr = next; + } + result += "\n"; + if (line.length - start > width && curr > start) { + result += line.slice(start, curr) + "\n" + line.slice(curr + 1); + } else { + result += line.slice(start); + } + return result.slice(1); +} +function escapeString(string) { + var result = ""; + var char = 0; + var escapeSeq; + for (var i = 0; i < string.length; char >= 65536 ? (i += 2) : i++) { + char = codePointAt(string, i); + escapeSeq = ESCAPE_SEQUENCES[char]; + if (!escapeSeq && isPrintable(char)) { + result += string[i]; + if (char >= 65536) result += string[i + 1]; + } else { + result += escapeSeq || encodeHex(char); + } + } + return result; +} +function writeFlowSequence(state, level, object) { + var _result = "", + _tag = state.tag, + index, + length, + value; + for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + if ( + writeNode(state, level, value, false, false) || + (typeof value === "undefined" && + writeNode(state, level, null, false, false)) + ) { + if (_result !== "") _result += "," + (!state.condenseFlow ? " " : ""); + _result += state.dump; + } + } + state.tag = _tag; + state.dump = "[" + _result + "]"; +} +function writeBlockSequence(state, level, object, compact) { + var _result = "", + _tag = state.tag, + index, + length, + value; + for (index = 0, length = object.length; index < length; index += 1) { + value = object[index]; + if (state.replacer) { + value = state.replacer.call(object, String(index), value); + } + if ( + writeNode(state, level + 1, value, true, true, false, true) || + (typeof value === "undefined" && + writeNode(state, level + 1, null, true, true, false, true)) + ) { + if (!compact || _result !== "") { + _result += generateNextLine(state, level); + } + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + _result += "-"; + } else { + _result += "- "; + } + _result += state.dump; + } + } + state.tag = _tag; + state.dump = _result || "[]"; +} +function writeFlowMapping(state, level, object) { + var _result = "", + _tag = state.tag, + objectKeyList = Object.keys(object), + index, + length, + objectKey, + objectValue, + pairBuffer; + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + pairBuffer = ""; + if (_result !== "") pairBuffer += ", "; + if (state.condenseFlow) pairBuffer += '"'; + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + if (!writeNode(state, level, objectKey, false, false)) { + continue; + } + if (state.dump.length > 1024) pairBuffer += "? "; + pairBuffer += + state.dump + + (state.condenseFlow ? '"' : "") + + ":" + + (state.condenseFlow ? "" : " "); + if (!writeNode(state, level, objectValue, false, false)) { + continue; + } + pairBuffer += state.dump; + _result += pairBuffer; + } + state.tag = _tag; + state.dump = "{" + _result + "}"; +} +function writeBlockMapping(state, level, object, compact) { + var _result = "", + _tag = state.tag, + objectKeyList = Object.keys(object), + index, + length, + objectKey, + objectValue, + explicitPair, + pairBuffer; + if (state.sortKeys === true) { + objectKeyList.sort(); + } else if (typeof state.sortKeys === "function") { + objectKeyList.sort(state.sortKeys); + } else if (state.sortKeys) { + throw new exception("sortKeys must be a boolean or a function"); + } + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + pairBuffer = ""; + if (!compact || _result !== "") { + pairBuffer += generateNextLine(state, level); + } + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + if (state.replacer) { + objectValue = state.replacer.call(object, objectKey, objectValue); + } + if (!writeNode(state, level + 1, objectKey, true, true, true)) { + continue; + } + explicitPair = + (state.tag !== null && state.tag !== "?") || + (state.dump && state.dump.length > 1024); + if (explicitPair) { + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += "?"; + } else { + pairBuffer += "? "; + } + } + pairBuffer += state.dump; + if (explicitPair) { + pairBuffer += generateNextLine(state, level); + } + if (!writeNode(state, level + 1, objectValue, true, explicitPair)) { + continue; + } + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += ":"; + } else { + pairBuffer += ": "; + } + pairBuffer += state.dump; + _result += pairBuffer; + } + state.tag = _tag; + state.dump = _result || "{}"; +} +function detectType(state, object, explicit) { + var _result, typeList, index, length, type2, style; + typeList = explicit ? state.explicitTypes : state.implicitTypes; + for (index = 0, length = typeList.length; index < length; index += 1) { + type2 = typeList[index]; + if ( + (type2.instanceOf || type2.predicate) && + (!type2.instanceOf || + (typeof object === "object" && object instanceof type2.instanceOf)) && + (!type2.predicate || type2.predicate(object)) + ) { + if (explicit) { + if (type2.multi && type2.representName) { + state.tag = type2.representName(object); + } else { + state.tag = type2.tag; + } + } else { + state.tag = "?"; + } + if (type2.represent) { + style = state.styleMap[type2.tag] || type2.defaultStyle; + if (_toString.call(type2.represent) === "[object Function]") { + _result = type2.represent(object, style); + } else if (_hasOwnProperty.call(type2.represent, style)) { + _result = type2.represent[style](object, style); + } else { + throw new exception( + "!<" + + type2.tag + + '> tag resolver accepts not "' + + style + + '" style' + ); + } + state.dump = _result; + } + return true; + } + } + return false; +} +function writeNode(state, level, object, block, compact, iskey, isblockseq) { + state.tag = null; + state.dump = object; + if (!detectType(state, object, false)) { + detectType(state, object, true); + } + var type2 = _toString.call(state.dump); + var inblock = block; + var tagStr; + if (block) { + block = state.flowLevel < 0 || state.flowLevel > level; + } + var objectOrArray = type2 === "[object Object]" || type2 === "[object Array]", + duplicateIndex, + duplicate; + if (objectOrArray) { + duplicateIndex = state.duplicates.indexOf(object); + duplicate = duplicateIndex !== -1; + } + if ( + (state.tag !== null && state.tag !== "?") || + duplicate || + (state.indent !== 2 && level > 0) + ) { + compact = false; + } + if (duplicate && state.usedDuplicates[duplicateIndex]) { + state.dump = "*ref_" + duplicateIndex; + } else { + if (objectOrArray && duplicate && !state.usedDuplicates[duplicateIndex]) { + state.usedDuplicates[duplicateIndex] = true; + } + if (type2 === "[object Object]") { + if (block && Object.keys(state.dump).length !== 0) { + writeBlockMapping(state, level, state.dump, compact); + if (duplicate) { + state.dump = "&ref_" + duplicateIndex + state.dump; + } + } else { + writeFlowMapping(state, level, state.dump); + if (duplicate) { + state.dump = "&ref_" + duplicateIndex + " " + state.dump; + } + } + } else if (type2 === "[object Array]") { + if (block && state.dump.length !== 0) { + if (state.noArrayIndent && !isblockseq && level > 0) { + writeBlockSequence(state, level - 1, state.dump, compact); + } else { + writeBlockSequence(state, level, state.dump, compact); + } + if (duplicate) { + state.dump = "&ref_" + duplicateIndex + state.dump; + } + } else { + writeFlowSequence(state, level, state.dump); + if (duplicate) { + state.dump = "&ref_" + duplicateIndex + " " + state.dump; + } + } + } else if (type2 === "[object String]") { + if (state.tag !== "?") { + writeScalar(state, state.dump, level, iskey, inblock); + } + } else if (type2 === "[object Undefined]") { + return false; + } else { + if (state.skipInvalid) return false; + throw new exception("unacceptable kind of an object to dump " + type2); + } + if (state.tag !== null && state.tag !== "?") { + tagStr = encodeURI( + state.tag[0] === "!" ? state.tag.slice(1) : state.tag + ).replace(/!/g, "%21"); + if (state.tag[0] === "!") { + tagStr = "!" + tagStr; + } else if (tagStr.slice(0, 18) === "tag:yaml.org,2002:") { + tagStr = "!!" + tagStr.slice(18); + } else { + tagStr = "!<" + tagStr + ">"; + } + state.dump = tagStr + " " + state.dump; + } + } + return true; +} +function getDuplicateReferences(object, state) { + var objects = [], + duplicatesIndexes = [], + index, + length; + inspectNode(object, objects, duplicatesIndexes); + for ( + index = 0, length = duplicatesIndexes.length; + index < length; + index += 1 + ) { + state.duplicates.push(objects[duplicatesIndexes[index]]); + } + state.usedDuplicates = new Array(length); +} +function inspectNode(object, objects, duplicatesIndexes) { + var objectKeyList, index, length; + if (object !== null && typeof object === "object") { + index = objects.indexOf(object); + if (index !== -1) { + if (duplicatesIndexes.indexOf(index) === -1) { + duplicatesIndexes.push(index); + } + } else { + objects.push(object); + if (Array.isArray(object)) { + for (index = 0, length = object.length; index < length; index += 1) { + inspectNode(object[index], objects, duplicatesIndexes); + } + } else { + objectKeyList = Object.keys(object); + for ( + index = 0, length = objectKeyList.length; + index < length; + index += 1 + ) { + inspectNode(object[objectKeyList[index]], objects, duplicatesIndexes); + } + } + } + } +} +function dump$1(input, options) { + options = options || {}; + var state = new State(options); + if (!state.noRefs) getDuplicateReferences(input, state); + var value = input; + if (state.replacer) { + value = state.replacer.call({ "": value }, "", value); + } + if (writeNode(state, 0, value, true, true)) return state.dump + "\n"; + return ""; +} +var dump_1 = dump$1; +var dumper = { + dump: dump_1, +}; +function renamed(from, to) { + return function () { + throw new Error( + "Function yaml." + + from + + " is removed in js-yaml 4. Use yaml." + + to + + " instead, which is now safe by default." + ); + }; +} +var Type = type; +var Schema = schema; +var FAILSAFE_SCHEMA = failsafe; +var JSON_SCHEMA = json; +var CORE_SCHEMA = core; +var DEFAULT_SCHEMA = _default; +var load = loader.load; +var loadAll = loader.loadAll; +var dump = dumper.dump; +var YAMLException = exception; +var types = { + binary, + float, + map, + null: _null, + pairs, + set, + timestamp, + bool, + int, + merge, + omap, + seq, + str, +}; +var safeLoad = renamed("safeLoad", "load"); +var safeLoadAll = renamed("safeLoadAll", "loadAll"); +var safeDump = renamed("safeDump", "dump"); +var jsYaml = { + Type, + Schema, + FAILSAFE_SCHEMA, + JSON_SCHEMA, + CORE_SCHEMA, + DEFAULT_SCHEMA, + load, + loadAll, + dump, + YAMLException, + types, + safeLoad, + safeLoadAll, + safeDump, +}; +export default jsYaml; +export { + CORE_SCHEMA, + DEFAULT_SCHEMA, + FAILSAFE_SCHEMA, + JSON_SCHEMA, + Schema, + Type, + YAMLException, + dump, + load, + loadAll, + safeDump, + safeLoad, + safeLoadAll, + types, + // 2022-04-11: this was added _by hand_ by cscheid. It's a terrible hack + // but it's the easiest way forward right now. + failsafe, + int, + bool, + float, + _null, +}; diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index 055b7175..e8a48513 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -7,7 +7,9 @@ "types": "./src/index.ts", "dependencies": { "mapped-string": "*", + "tidyverse-errors": "*", "tsconfig": "*", + "yaml": "^2.4.2", "typescript": "^5.4.2" }, "devDependencies": { } diff --git a/packages/annotated-json/src/annotated-yaml.ts b/packages/annotated-json/src/annotated-yaml.ts new file mode 100644 index 00000000..da1d80cc --- /dev/null +++ b/packages/annotated-json/src/annotated-yaml.ts @@ -0,0 +1,727 @@ +/* + * annotated-yaml.ts + * + * Copyright (C) 2021-2022 Posit Software, PBC + */ + +import { indexToLineCol, lineColToIndex } from "./text"; +import { AnnotatedParse, JSONValue } from "./types"; + +import { + asMappedString, + mappedIndexToLineCol, + mappedLines, + MappedString, + createSourceContext +} from "mapped-string"; + +import { load as jsYamlParse } from "../external/js-yaml.js"; + +import { QuartoJSONSchema } from "./js-yaml-quarto-schema"; +import { tidyverseInfo } from "tidyverse-errors"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TreeSitterParse = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TreeSitterNode = any; + +// jsYaml sometimes reports "trivial" nested annotations where the span of +// the internal contents is identical to the outside content. This +// happens in case of unusually-indented yaml arrays such as +// +// foo: +// [ +// a, +// b, +// c +// ] +// +// This incorrect annotation breaks our navigation, so we work around it here +// by normalizing those away. +function postProcessAnnotation(parse: AnnotatedParse): AnnotatedParse { + if ( + parse.components.length === 1 && + parse.start === parse.components[0].start && + parse.end === parse.components[0].end + ) { + return postProcessAnnotation(parse.components[0]); + } else { + parse.components = parse.components.map(postProcessAnnotation); + return parse; + } +} + +function jsYamlParseLenient(yml: string): unknown { + try { + return jsYamlParse(yml, { schema: QuartoJSONSchema }); + } catch (_e) { + // this should not happen because it indicates a parse error + // but we only call jsYamlParseLenient from inside buildAnnotated, which + // walks the TreeSitter parser AST. However, the TreeSitter AST + // sometimes is malformed because of bad (outer) yaml, so all + // bets are off. But in that case, we're in error recovery mode anyway, + // so returning the raw string is as good as anything else. + return yml; + } +} + +export function readAnnotatedYamlFromString(yml: string) { + return readAnnotatedYamlFromMappedString(asMappedString(yml))!; +} + +export function readAnnotatedYamlFromMappedString( + mappedSource: MappedString, +) { + /* + * We use both tree-sitter-yaml and js-yaml to get the + * best that both offer. tree-sitter offers error resiliency + * but reports unrecoverable parse errors poorly. + * + * In addition, tree-sitter-yaml fails to parse some valid yaml, see https://github.com/ikatyang/tree-sitter-yaml/issues/29 + * + * It also generated incorrect parses for some inputs, like + * + * foo: + * bar + * + * tree-sitter parses this as { "foo": { "bar": null } }, + * + * but the correct output should be { "foo": "bar" } + * + * So we use tree-sitter-yaml if lenient === true, meaning we want + * to generate parses on bad inputs (and consequently live with + * incorrect tree-sitter parses) + * + * if lenient === false, we use jsYaml, a compliant parser. + */ + + + try { + return buildJsYamlAnnotation(mappedSource); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if (e.name === "YAMLError") { + e.name = "YAML Parsing"; + } + // FIXME we should convert this to a TidyverseError + // for now, we just use the util functions. + const m = e.stack.split("\n")[0].match(/^.+ \((\d+):(\d+)\)$/); + if (m) { + // this error is bad enough for users that we + // need to actively overwrite the parser's error message + // https://github.com/quarto-dev/quarto-cli/issues/9496 + const m1 = mappedSource.value.match(/([^\s]+):([^\s]+)/); + if ( + m1 && + e.reason.match(/a multiline key may not be an implicit key/) + ) { + e.name = "YAML Parse Error"; + e.reason = "block has incorrect key formatting"; + const { originalString } = mappedSource.map(m1.index!, true)!; + const filename = originalString.fileName!; + const map = mappedSource.map(m1.index!)!; + const { line, column } = indexToLineCol(map.originalString.value)( + map.index, + ); + + const sourceContext = createSourceContext(mappedSource, { + start: m1.index! + 1, + end: m1.index! + m1[0].length, + }); + e.stack = `${e.reason} (${filename}, ${line + 1}:${ + column + 1 + })\n${sourceContext}`; + e.message = e.stack; + e.message = `${e.message}\n${ + tidyverseInfo( + "Is it possible you missed a space after a colon in the key-value mapping?", + ) + }`; + } else { + const f = lineColToIndex(mappedSource.value); + const location = { line: Number(m[1]) - 1, column: Number(m[2] - 1) }; + const offset = f(location); + const { originalString } = mappedSource.map(offset, true)!; + const filename = originalString.fileName!; + const f2 = mappedIndexToLineCol(mappedSource); + const { line, column } = f2(offset); + const sourceContext = createSourceContext(mappedSource, { + start: offset, + end: offset + 1, + }); + e.stack = `${e.reason} (${filename}, ${line + 1}:${ + column + 1 + })\n${sourceContext}`; + e.message = e.stack; + if ( + mappedLines(mappedSource)[location.line].value.indexOf("!expr") !== + -1 && + e.reason.match(/bad indentation of a mapping entry/) + ) { + e.message = `${e.message}\n${ + tidyverseInfo( + "YAML tags like !expr must be followed by YAML strings.", + ) + }\n${ + tidyverseInfo( + "Is it possible you need to quote the value you passed to !expr ?", + ) + }`; + } + } + e.stack = ""; + } + throw e; + } +} + +export function buildJsYamlAnnotation(mappedYaml: MappedString) { + const yml = mappedYaml.value; + + // deno-lint-ignore no-explicit-any + const stack: any[] = []; + const results: AnnotatedParse[] = []; + + // deno-lint-ignore no-explicit-any + function listener(what: string, state: any) { + const { result, position, kind } = state; + if (what === "close") { + const { position: openPosition, kind: openKind } = stack.pop(); + if (results.length > 0) { + const last = results[results.length - 1]; + // sometimes we get repeated instances of (start, end) pairs + // (probably because of recursive calls in parse() that don't + // consume the string) so we skip those explicitly here + if (last.start === openPosition && last.end === position) { + return; + } + } + // deno-lint-ignore no-explicit-any + const components: any[] = []; + while (results.length > 0) { + const last = results[results.length - 1]; + if (last.end <= openPosition) { + break; + } + components.push(results.pop()); + } + components.reverse(); + + const rawRange = yml.substring(openPosition, position); + // trim spaces if needed + const leftTrim = rawRange.length - rawRange.trimStart().length; + const rightTrim = rawRange.length - rawRange.trimEnd().length; + + if (openKind === null && kind === null) { + // We've observed that openKind === null && kind === null + // can happen sometimes while parsing multiline yaml strings, and + // should be ignored. + } else if (rawRange.trim().length === 0) { + // special case for when string is empty + results.push({ + start: position - rightTrim, + end: position - rightTrim, + result: result as JSONValue, + components, + kind, + source: mappedYaml, + }); + } else { + results.push({ + start: openPosition + leftTrim, + end: position - rightTrim, + result: result, + components, + kind, + source: mappedYaml, + }); + } + } else { + stack.push({ position, kind }); + } + } + + jsYamlParse(yml, { listener, schema: QuartoJSONSchema }); + + if (results.length === 0) { + return { + start: 0, + end: 0, + result: null, + kind: "null", + components: [], + source: mappedYaml, + }; + } + if (results.length !== 1) { + throw new Error( + `Expected a single result, got ${results.length} instead`, + ); + } + + JSON.stringify(results[0]); // this is here so that we throw on circular structures + return postProcessAnnotation(results[0]); +} + +export function buildTreeSitterAnnotation( + tree: TreeSitterParse, + mappedSource: MappedString, +): AnnotatedParse | null { + const errors: { start: number; end: number; message: string }[] = []; + const singletonBuild = (node: TreeSitterNode) => { + // some singleton nodes can contain more than one child, especially in the case of comments. + // So we find the first non-comment to return. + let tag: TreeSitterNode | undefined = undefined; + for (const child of node.children) { + if (child.type === "tag") { + tag = child; + continue; + } + if (child.type !== "comment") { + const result = buildNode(child, node.endIndex); + if (tag) { + return annotateTag(result, tag, node); + } else { + return result; + } + } + } + // if there's only comments, we fail. + return annotateEmpty(node.endIndex); + }; + const buildNode = ( + node: TreeSitterNode, + endIndex?: number, + ): AnnotatedParse => { + if (node === null) { + // This can come up with parse errors + return annotateEmpty(endIndex === undefined ? -1 : endIndex); + } + if (dispatch[node.type] === undefined) { + // we don't support this construction, but let's try not to crash. + return annotateEmpty(endIndex || node.endIndex || -1); + } + return dispatch[node.type](node); + }; + + const annotateEmpty = (position: number): AnnotatedParse => { + return { + start: position, + end: position, + result: null, + kind: "<>", + components: [], + source: mappedSource, + }; + }; + + const annotate = ( + node: TreeSitterNode, + // deno-lint-ignore no-explicit-any + result: any, + components: AnnotatedParse[], + ): AnnotatedParse => { + return { + start: node.startIndex, + end: node.endIndex, + result: result as JSONValue, + kind: node.type, // NB this doesn't match js-yaml, so you need + // to make sure your annotated walkers know + // about tree-sitter and js-yaml both. + components, + source: mappedSource, + }; + }; + + const annotateTag = ( + innerParse: AnnotatedParse, + tagNode: TreeSitterNode, + outerNode: TreeSitterNode, + ): AnnotatedParse => { + const tagParse = annotate(tagNode, tagNode.text, []); + const result = annotate(outerNode, { + tag: tagNode.text, + value: innerParse.result, + }, [tagParse, innerParse]); + return result; + }; + + const buildPair = (node: TreeSitterNode) => { + let key, value; + // deno-lint-ignore no-explicit-any + const children = node.children.filter((n: any) => n.type !== "comment"); + + if (children.length === 3) { + // when three children exist, we assume a good parse + key = annotate(children[0], children[0].text, []); + value = buildNode(children[2], node.endIndex); + } else if (children.length === 2) { + // when two children exist, we assume a bad parse with missing value + key = annotate(children[0], children[0].text, []); + value = annotateEmpty(node.endIndex); + } else { + // otherwise, we assume a bad parse, return empty on both key and value + key = annotateEmpty(node.endIndex); + value = annotateEmpty(node.endIndex); + } + + return annotate(node, { + key: key.result, + value: value.result, + }, [key, value]); + }; + + const dispatch: Record AnnotatedParse> = { + "stream": singletonBuild, + "document": singletonBuild, + "block_node": singletonBuild, + "flow_node": singletonBuild, + "double_quote_scalar": (node) => { + return annotate(node, jsYamlParseLenient(node.text), []); + }, + "single_quote_scalar": (node) => { + return annotate(node, jsYamlParseLenient(node.text), []); + }, + "plain_scalar": (node) => { + return annotate(node, jsYamlParseLenient(node.text), []); + }, + "block_scalar": (node) => { + return annotate(node, jsYamlParseLenient(node.text), []); + }, + "block_sequence": (node) => { + const result = [], components = []; + for (let i = 0; i < node.childCount; ++i) { + const child = node.child(i); + if (child.type !== "block_sequence_item") { + continue; + } + const component = buildNode(child, node.endIndex); + components.push(component); + result.push(component && component.result); + } + return annotate(node, result, components); + }, + "block_sequence_item": (node) => { + if (node.childCount < 2) { + return annotateEmpty(node.endIndex); + } else { + return buildNode(node.child(1), node.endIndex); + } + }, + "flow_sequence": (node) => { + // deno-lint-ignore no-explicit-any + const result: any[] = [], components = []; + for (let i = 0; i < node.childCount; ++i) { + const child = node.child(i); + if (child.type !== "flow_node") { + continue; + } + const component = buildNode(child, node.endIndex); + components.push(component); + result.push(component.result); + } + return annotate(node, result, components); + }, + "block_mapping": (node) => { + // deno-lint-ignore no-explicit-any + const result: Record = {}, components: AnnotatedParse[] = []; + for (let i = 0; i < node.childCount; ++i) { + const child = node.child(i); + let component; + if (child.type === "ERROR") { + // attempt to recover from error + result[child.text] = "<>"; + const key = annotate(child, child.text, []); + const value = annotateEmpty(child.endIndex); + component = annotate(child, { + key: key.result, + value: value.result, + }, [key, value]); + } else if (child.type !== "block_mapping_pair") { + continue; + } else { + component = buildNode(child, node.endIndex); + } + const { key, value } = component.result as { [key: string]: JSONValue }; + // TODO what do we do in the presence of parse errors that produce empty keys? + // if (key === null) { } + result[String(key)] = value; + components.push(...(component.components!)); + } + return annotate(node, result, components); + }, + "flow_pair": buildPair, + "flow_mapping": (node) => { + // deno-lint-ignore no-explicit-any + const result: Record = {}, components: AnnotatedParse[] = []; + // skip flow_nodes at the boundary + for (let i = 0; i < node.childCount; ++i) { + const child = node.child(i); + if (child.type === "flow_node") { + continue; + } + if (child.type === "flow_pair") { + const component = buildNode(child, node.endIndex); + const { key, value } = component.result as { + [key: string]: JSONValue; + }; + result[String(key)] = value; + components.push(...(component.components!)); + } + } + return annotate(node, result, components); + }, + "block_mapping_pair": buildPair, + }; + + const result = buildNode(tree.rootNode, tree.rootNode.endIndex); + if (errors.length) { + result.errors = errors; + } + + // some tree-sitter "error-tolerant parses" are particularly bad + // for us here. We must guard against "partial" parses where + // tree-sitter doesn't consume the entire string, since this is + // symptomatic of a bad object. When this happens, bail on the + // current parse. + // + // There's an added complication in that it seems that sometimes + // treesitter consumes line breaks at the end of the file, and + // sometimes it doesn't. So exact checks don't quite work. We're + // then resigned to a heuristic that is bound to sometimes + // fail. That heuristic is, roughly, that we consider something a + // failed parse if it misses more than 5% of the characters in the + // original string span. + // + // This is, clearly, a terrible hack. + // + // I really ought to consider rebuilding this whole infrastructure + const parsedSize = tree.rootNode.text.trim().length; + const codeSize = mappedSource.value.trim().length; + const lossage = parsedSize / codeSize; + + if (lossage < 0.95) { + return null; + } + + return result; +} + +// locateCursor is lenient wrt locating inside the last character of a +// range (by using position <= foo instead of position < foo). That +// happens because tree-sitter's robust parsing sometimes returns +// "partial objects" which are missing parts of the tree. In those +// cases, we want the cursor to be "inside a null value", and they +// correspond to the edges of an object, where position == range.end. +export interface LocateCursorResult { + withError: boolean; + value?: (string | number)[]; + kind?: "key" | "value"; + annotation?: AnnotatedParse; +} + +export function locateCursor( + annotation: AnnotatedParse, + position: number, +): LocateCursorResult { + let failedLast = false; + let innermostAnnotation: AnnotatedParse; + let keyOrValue: "key" | "value"; + const result: (string | number)[] = []; + const kInternalLocateError = "Cursor outside bounds in sequence locate"; + + function locate(node: AnnotatedParse): void { + if ( + node.kind === "block_mapping" || node.kind === "flow_mapping" || + node.kind === "mapping" + ) { + for (let i = 0; i < node.components.length; i += 2) { + const keyC = node.components[i], + valueC = node.components[i + 1]; + if (keyC.start <= position && position <= keyC.end) { + innermostAnnotation = keyC; + result.push(keyC.result as string); + keyOrValue = "key"; + return; + } else if (valueC.start <= position && position <= valueC.end) { + result.push(keyC.result as string); + innermostAnnotation = valueC; + return locate(valueC); + } + } + + // TODO decide what to do if cursor lands exactly on ":"? + + // if we "fell through the pair cracks", that is, if the cursor is inside a mapping + // but not inside any of the actual mapping pairs, then we stop the location at the + // object itself, but report an error so that the recipients may handle it + // case-by-base. + + failedLast = true; + + return; + } else if ( + node.kind === "block_sequence" || node.kind === "flow_sequence" + ) { + for (let i = 0; i < node.components.length; ++i) { + const valueC = node.components[i]; + if (valueC.start <= position && position <= valueC.end) { + result.push(i); + innermostAnnotation = valueC; + return locate(valueC); + } + if (valueC.start > position) { + // We went too far: that means we're caught in between entries. Assume + // that we're inside the previous element but that we can't navigate any further + // If we're at the beginning of the sequence, assume that we're done exactly here. + if (i === 0) { + return; + } else { + result.push(i - 1); + return; + } + } + } + + throw new Error(kInternalLocateError); + } else { + if (node.kind !== "<>") { + keyOrValue = "value"; + return; + } else { + // we're inside an error, don't report that. + return; + } + } + } + try { + locate(annotation); + return { + withError: failedLast, + value: result, + kind: keyOrValue!, + annotation: innermostAnnotation!, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if (e.message === kInternalLocateError) { + return { + withError: true, + }; + } else { + throw e; + } + } +} + +export function locateAnnotation( + annotation: AnnotatedParse, + position: (number | string)[], + kind?: "key" | "value", +): AnnotatedParse { + // FIXME we temporarily work around AnnotatedParse bugs + // here + + const originalSource = annotation.source; + + kind = kind || "value"; + for (let i = 0; i < position.length; ++i) { + const value = position[i]; + if (typeof value === "number") { + const inner = annotation.components[value]; + if (inner === undefined) { + throw new Error("invalid path for locateAnnotation"); + } + annotation = inner; + } else { + let found = false; + for (let j = 0; j < annotation.components.length; j += 2) { + if ( + originalSource.value.substring( + annotation.components[j].start, + annotation.components[j].end, + ).trim() === + value + ) { + // on last entry, we discriminate between key and value contexts + if (i === position.length - 1) { + if (kind === "key") { + annotation = annotation.components[j]; + } else { + annotation = annotation.components[j + 1]; + } + } + found = true; + break; + } + } + if (!found) { + throw new Error("invalid path for locateAnnotation"); + } + } + } + return annotation; +} + +// this supports AnnotatedParse results built +// from deno yaml as well as tree-sitter. +export function navigate( + path: (number | string)[], + annotation: AnnotatedParse | undefined, + returnKey = false, // if true, then return the *key* entry as the final result rather than the *value* entry. + pathIndex = 0, +): AnnotatedParse | undefined { + // this looks a little strange, but it's easier to catch the error + // here than in the different cases below + if (annotation === undefined) { + throw new Error("Can't navigate an undefined annotation"); + } + if (pathIndex >= path.length) { + return annotation; + } + if (annotation.kind === "mapping" || annotation.kind === "block_mapping") { + const { components } = annotation; + const searchKey = path[pathIndex]; + // this loop is inverted to provide better error messages in the + // case of repeated keys. Repeated keys are an error in any case, but + // the parsing by the validation infrastructure reports the last + // entry of a given key in the mapping as the one that counts + // (instead of the first, which would be what we'd get if running + // the loop forward). + // + // In that case, the validation errors will also point to the last + // entry. In order for the errors to be at least consistent, + // we then loop backwards + const lastKeyIndex = ~~((components.length - 1) / 2) * 2; + for (let i = lastKeyIndex; i >= 0; i -= 2) { + const key = components[i]!.result; + if (key === searchKey) { + if (returnKey && pathIndex === path.length - 1) { + return navigate(path, components[i], returnKey, pathIndex + 1); + } else { + return navigate(path, components[i + 1], returnKey, pathIndex + 1); + } + } + } + return annotation; + } else if ( + ["sequence", "block_sequence", "flow_sequence"].indexOf(annotation.kind) !== + -1 + ) { + const searchKey = Number(path[pathIndex]); + if ( + isNaN(searchKey) || searchKey < 0 || + searchKey >= annotation.components.length + ) { + return annotation; + } + return navigate( + path, + annotation.components[searchKey], + returnKey, + pathIndex + 1, + ); + } else { + return annotation; + } +} diff --git a/packages/annotated-json/src/glb.ts b/packages/annotated-json/src/glb.ts new file mode 100644 index 00000000..6a34a59c --- /dev/null +++ b/packages/annotated-json/src/glb.ts @@ -0,0 +1,70 @@ +/* +* binary-search.ts +* +* Copyright (C) 2021-2024 Posit Software, PBC +* +*/ + +export function glb( + array: T[], + value: U, + compare?: (a: U, b: T) => number, +) { + compare = compare || + ((a: unknown, b: unknown) => (a as number) - (b as number)); + if (array.length === 0) { + return -1; + } + if (array.length === 1) { + if (compare(value, array[0]) < 0) { + return -1; + } else { + return 0; + } + } + + let left = 0; + let right = array.length - 1; + const vLeft = array[left], vRight = array[right]; + + if (compare(value, vRight) >= 0) { + // pre: value >= vRight + return right; + } + if (compare(value, vLeft) < 0) { + // pre: value < vLeft + return -1; + } + + // pre: compare(value, vRight) === -1 => value < vRight + // pre: compare(value, vLeft) === {0, 1} => compare(vLeft, value) === {-1, 0} => vLeft <= value + // pre: vLeft <= value < vRight + + while (right - left > 1) { + // pre: right - left > 1 => ((right - left) >> 1) > 0 + // pre: vLeft <= value < vRight (from before while start and end of while loop) + + const center = left + ((right - left) >> 1); + const vCenter = array[center]; + const cmp = compare(value, vCenter); + + if (cmp < 0) { + right = center; + // vRight = vCenter + // pre: value < vCenter + // pre: value < vRight + } else if (cmp === 0) { + // pre: value === center => center <= value + left = center; + // vLeft = vCenter + // pre: vLeft <= value + } else { + // pre: cmp > 0 + // pre: value > center => center < value => center <= value + left = center; + // pre: vLeft <= value + } + // pre: vLeft <= value < vRight + } + return left; +} diff --git a/packages/annotated-json/src/js-yaml-quarto-schema.ts b/packages/annotated-json/src/js-yaml-quarto-schema.ts new file mode 100644 index 00000000..aacba6b0 --- /dev/null +++ b/packages/annotated-json/src/js-yaml-quarto-schema.ts @@ -0,0 +1,38 @@ +/* + * js-yaml-quarto-schema.ts + * + * Copyright (C) 2024 by Posit Software, PBC + * + */ + +import { + _null, // this is "nil" in deno's version...? :shrug: + bool, + failsafe, + float, + int, + Schema, + Type, +} from "../external/js-yaml"; + +// Standard YAML's JSON schema + an expr tag handler () +// http://www.yaml.org/spec/1.2/spec.html#id2803231 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const QuartoJSONSchema: any = new Schema({ + implicit: [_null, bool, int, float], + include: [failsafe], + explicit: [ + new Type("!expr", { + kind: "scalar", + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + construct(data: any): Record { + const result: string = data !== null ? data : ""; + return { + value: result, + tag: "!expr", + }; + }, + }), + ], +}); diff --git a/packages/annotated-json/src/text.ts b/packages/annotated-json/src/text.ts new file mode 100644 index 00000000..9425aa07 --- /dev/null +++ b/packages/annotated-json/src/text.ts @@ -0,0 +1,57 @@ +/* + * text.ts + * + * Text utilities for annotated-json. + * + * Copyright (C) 2024 by Posit Software, PBC + * + */ + +// NB we can't use JS matchAll or replaceAll here because we need to support old +// Chromium in the IDE +// +// NB this mutates the regexp. + +import { glb } from "./glb"; + +export function* matchAll(text: string, regexp: RegExp) { + if (!regexp.global) { + throw new Error("matchAll requires global regexps"); + } + let match; + while ((match = regexp.exec(text)) !== null) { + yield match; + } +} + +export function* lineOffsets(text: string) { + yield 0; + for (const match of matchAll(text, /\r?\n/g)) { + yield match.index + match[0].length; + } +} + +export function indexToLineCol(text: string) { + const offsets = Array.from(lineOffsets(text)); + return function (offset: number) { + if (offset === 0) { + return { + line: 0, + column: 0, + }; + } + + const startIndex = glb(offsets, offset); + return { + line: startIndex, + column: offset - offsets[startIndex], + }; + }; +} + +export function lineColToIndex(text: string) { + const offsets = Array.from(lineOffsets(text)); + return function (position: { line: number; column: number }) { + return offsets[position.line] + position.column; + }; +} diff --git a/packages/annotated-json/tsconfig.json b/packages/annotated-json/tsconfig.json index aad794c2..171477f6 100644 --- a/packages/annotated-json/tsconfig.json +++ b/packages/annotated-json/tsconfig.json @@ -1,5 +1,8 @@ { "exclude": ["node_modules"], "extends": "tsconfig/base.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "allowJs": true + } } diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index 5ad19600..f170b179 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -7,7 +7,8 @@ "types": "./src/index.ts", "dependencies": { "tsconfig": "*", - "typescript": "^5.4.2" + "typescript": "^5.4.2", + "ansi-colors": "^4.1.3" }, "devDependencies": { } } diff --git a/packages/mapped-string/src/error.ts b/packages/mapped-string/src/error.ts index ae6e6018..9d9eaccf 100644 --- a/packages/mapped-string/src/error.ts +++ b/packages/mapped-string/src/error.ts @@ -4,6 +4,11 @@ * Copyright (C) 2020-2024 Posit Software, PBC */ +import { mappedIndexToLineCol } from "./mapped-text"; +import { lines } from "./text"; +import { MappedString, Range } from "./types"; +import * as colors from "ansi-colors"; + export class InternalError extends Error { constructor( message: string, @@ -59,3 +64,127 @@ export function asErrorEx(e: unknown) { return new ErrorEx("Error", String(e), false, true); } } + +export function quotedStringColor(msg: string) { + return colors.blue(msg); +} + +export function formatLineRange( + text: string, + firstLine: number, + lastLine: number, +) { + const lineWidth = Math.max( + String(firstLine + 1).length, + String(lastLine + 1).length, + ); + const pad = " ".repeat(lineWidth); + + const ls = lines(text); + + const result = []; + for (let i = firstLine; i <= lastLine; ++i) { + const numberStr = `${pad}${i + 1}: `.slice(-(lineWidth + 2)); + const lineStr = ls[i]; + result.push({ + lineNumber: i, + content: numberStr + quotedStringColor(lineStr), + rawLine: ls[i], + }); + } + return { + prefixWidth: lineWidth + 2, + lines: result, + }; +} + + +/** + * Create a formatted string describing the surroundings of an error. + * Used in the generation of nicely-formatted error messages. + * + * @param src the string containing the source of the error + * @param location the location range in src + * @returns a string containing a formatted description of the context around the error + */ +export function createSourceContext( + src: MappedString, + location: Range, +): string { + if (src.value.length === 0) { + // if the file is empty, don't try to create a source context + return ""; + } + const startMapResult = src.map(location.start, true); + const endMapResult = src.map(location.end, true); + + const locF = mappedIndexToLineCol(src); + + let sourceLocation; + try { + sourceLocation = { + start: locF(location.start), + end: locF(location.end), + }; + } catch (_e) { + sourceLocation = { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }; + } + + if (startMapResult === undefined || endMapResult === undefined) { + throw new InternalError( + `createSourceContext called with bad location ${location.start}-${location.end}.`, + ); + } + + if (startMapResult.originalString !== endMapResult.originalString) { + throw new InternalError( + "don't know how to create source context across different source files", + ); + } + const originalString = startMapResult.originalString; + // TODO this is computed every time, might be inefficient on large files. + const nLines = lines(originalString.value).length; + + const { + start, + end, + } = sourceLocation; + const { + prefixWidth, + lines: formattedLines, + } = formatLineRange( + originalString.value, + Math.max(0, start.line - 1), + Math.min(end.line + 1, nLines - 1), + ); + const contextLines: string[] = []; + let mustPrintEllipsis = true; + for (const { lineNumber, content, rawLine } of formattedLines) { + if (lineNumber < start.line || lineNumber > end.line) { + if (rawLine.trim().length) { + contextLines.push(content); + } + } else { + if ( + lineNumber >= start.line + 2 && lineNumber <= end.line - 2 + ) { + if (mustPrintEllipsis) { + mustPrintEllipsis = false; + contextLines.push("..."); + } + } else { + const startColumn = lineNumber > start.line ? 0 : start.column; + const endColumn = lineNumber < end.line ? rawLine.length : end.column; + contextLines.push(content); + contextLines.push( + " ".repeat(prefixWidth + startColumn - 1) + + "~".repeat(endColumn - startColumn + 1), + ); + } + } + } + return contextLines.join("\n"); +} diff --git a/packages/mapped-string/src/index.ts b/packages/mapped-string/src/index.ts index 7c34a002..8df49580 100644 --- a/packages/mapped-string/src/index.ts +++ b/packages/mapped-string/src/index.ts @@ -24,4 +24,5 @@ export * from "./ranged-text"; export * from "./mapped-text"; -export * from "./types"; \ No newline at end of file +export * from "./types"; +export * from "./error"; \ No newline at end of file diff --git a/packages/tidyverse-errors/package.json b/packages/tidyverse-errors/package.json new file mode 100644 index 00000000..b8021154 --- /dev/null +++ b/packages/tidyverse-errors/package.json @@ -0,0 +1,15 @@ +{ + "name": "tidyverse-errors", + "version": "0.1.0", + "private": true, + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "tsconfig": "*", + "typescript": "^5.4.2", + "ansi-colors": "^4.1.3" + }, + "devDependencies": { } +} + diff --git a/packages/tidyverse-errors/src/errors.ts b/packages/tidyverse-errors/src/errors.ts new file mode 100644 index 00000000..97f74515 --- /dev/null +++ b/packages/tidyverse-errors/src/errors.ts @@ -0,0 +1,56 @@ +// tidyverse error message styling +// https://style.tidyverse.org/error-messages.html +// +// Currently, the only way in which we disagree with the tidyverse +// style guide is in the phrasing of the "hint" (here, "info") prompts. +// Instead of using question marks, we use actionable, but tentative phrasing. +// +// Where the style guide would suggest "have you tried x instead?" +// +// here, we will say "Try x instead." +// + +import * as colors from "ansi-colors"; + +const getPlatform = () => { + // Adapted from https://stackoverflow.com/a/19176790 + let OSName = "unknown"; + if (window.navigator.userAgent.indexOf("Windows NT 10.0")!== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 6.3") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 6.2") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 6.1") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 6.0") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 5.1") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Windows NT 5.0") !== -1) OSName="windows"; + if (window.navigator.userAgent.indexOf("Mac") !== -1) OSName="darwin"; + if (window.navigator.userAgent.indexOf("X11") !== -1) OSName="linux"; + if (window.navigator.userAgent.indexOf("Linux") !== -1) OSName="linux"; + + return OSName; +} + +function platformHasNonAsciiCharacters(): boolean { + try { + return getPlatform() !== "windows"; + } catch (_e) { + return false; + } +} + +// formats an info message according to the tidyverse style guide +export function tidyverseInfo(msg: string) { + if (platformHasNonAsciiCharacters()) { + return `${colors.blue("ℹ")} ${msg}`; + } else { + return `${colors.blue("i")} ${msg}`; + } +} + +// formats an error message according to the tidyverse style guide +export function tidyverseError(msg: string) { + if (platformHasNonAsciiCharacters()) { + return `${colors.red("✖")} ${msg}`; + } else { + return `${colors.red("x")} ${msg}`; + } +} diff --git a/packages/tidyverse-errors/src/index.ts b/packages/tidyverse-errors/src/index.ts new file mode 100644 index 00000000..87d4bcdf --- /dev/null +++ b/packages/tidyverse-errors/src/index.ts @@ -0,0 +1,25 @@ +/* + * index.ts + * + * Copyright (C) 2024 by Posit Software, PBC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export * from "./errors"; \ No newline at end of file diff --git a/packages/tidyverse-errors/tsconfig.json b/packages/tidyverse-errors/tsconfig.json new file mode 100644 index 00000000..171477f6 --- /dev/null +++ b/packages/tidyverse-errors/tsconfig.json @@ -0,0 +1,8 @@ +{ + "exclude": ["node_modules"], + "extends": "tsconfig/base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true + } +} From e9d256b67ae15875b326c5b39418097c546b46c2 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 14:39:28 -0700 Subject: [PATCH 03/18] more partial work --- packages/annotated-json/package.json | 1 - packages/annotated-json/{ => src}/external/js-yaml.js | 3 ++- packages/annotated-json/src/js-yaml-quarto-schema.ts | 2 +- packages/annotated-json/tsconfig.json | 7 ++++--- yarn.lock | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) rename packages/annotated-json/{ => src}/external/js-yaml.js (99%) diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index e8a48513..968dbb57 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -9,7 +9,6 @@ "mapped-string": "*", "tidyverse-errors": "*", "tsconfig": "*", - "yaml": "^2.4.2", "typescript": "^5.4.2" }, "devDependencies": { } diff --git a/packages/annotated-json/external/js-yaml.js b/packages/annotated-json/src/external/js-yaml.js similarity index 99% rename from packages/annotated-json/external/js-yaml.js rename to packages/annotated-json/src/external/js-yaml.js index 01e26b21..7a11a83b 100644 --- a/packages/annotated-json/external/js-yaml.js +++ b/packages/annotated-json/src/external/js-yaml.js @@ -1,3 +1,5 @@ +/* eslint-disable no-control-regex */ +/* eslint-disable no-useless-escape */ // eslint-disable /** NB: we needed to hack our way around the fact that js-yaml doesn't export some entry points @@ -923,7 +925,6 @@ var CHOMPING_CLIP = 1; var CHOMPING_STRIP = 2; var CHOMPING_KEEP = 3; var PATTERN_NON_PRINTABLE = - // eslint-disable-next-line no-control-regex /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; var PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/; var PATTERN_FLOW_INDICATORS = /[,\[\]\{\}]/; diff --git a/packages/annotated-json/src/js-yaml-quarto-schema.ts b/packages/annotated-json/src/js-yaml-quarto-schema.ts index aacba6b0..cf5f8326 100644 --- a/packages/annotated-json/src/js-yaml-quarto-schema.ts +++ b/packages/annotated-json/src/js-yaml-quarto-schema.ts @@ -13,7 +13,7 @@ import { int, Schema, Type, -} from "../external/js-yaml"; +} from "./external/js-yaml"; // Standard YAML's JSON schema + an expr tag handler () // http://www.yaml.org/spec/1.2/spec.html#id2803231 diff --git a/packages/annotated-json/tsconfig.json b/packages/annotated-json/tsconfig.json index 171477f6..6682bf9a 100644 --- a/packages/annotated-json/tsconfig.json +++ b/packages/annotated-json/tsconfig.json @@ -1,8 +1,9 @@ { - "exclude": ["node_modules"], + "exclude": ["node_modules", "dist"], "extends": "tsconfig/base.json", "include": ["src"], "compilerOptions": { - "allowJs": true - } + "allowJs": true, + "outDir": "./dist", + }, } diff --git a/yarn.lock b/yarn.lock index 220b6b81..da4753c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2457,7 +2457,7 @@ ansi-colors@4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-colors@^4.1.1: +ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== From 1df836b00bcec8a070d3ae3e612a473e23710d4a Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 15:20:44 -0700 Subject: [PATCH 04/18] more partial work --- packages/annotated-json/src/annotated-yaml.ts | 25 +- packages/annotated-json/src/index.ts | 3 +- packages/json-validator/package.json | 17 + packages/json-validator/src/errors.ts | 1061 +++++++++ .../json-validator/src/external/regexpp.mjs | 2089 +++++++++++++++++ packages/json-validator/src/index.ts | 25 + packages/json-validator/src/regexp.js | 66 + packages/json-validator/src/resolve.ts | 93 + .../json-validator/src/schema-navigation.ts | 191 ++ packages/json-validator/src/schema-utils.ts | 324 +++ packages/json-validator/src/schema.ts | 128 + packages/json-validator/src/semaphore.ts | 50 + packages/json-validator/src/state.ts | 69 + packages/json-validator/src/types.ts | 362 +++ .../json-validator/src/validator-queue.ts | 82 + packages/json-validator/src/validator.ts | 688 ++++++ packages/json-validator/src/yaml-schema.ts | 127 + packages/json-validator/tsconfig.json | 9 + packages/mapped-string/package.json | 3 +- packages/mapped-string/src/error.ts | 6 +- packages/mapped-string/src/index.ts | 3 +- packages/tidyverse-errors/src/errors.ts | 25 + 22 files changed, 5426 insertions(+), 20 deletions(-) create mode 100644 packages/json-validator/package.json create mode 100644 packages/json-validator/src/errors.ts create mode 100644 packages/json-validator/src/external/regexpp.mjs create mode 100644 packages/json-validator/src/index.ts create mode 100644 packages/json-validator/src/regexp.js create mode 100644 packages/json-validator/src/resolve.ts create mode 100644 packages/json-validator/src/schema-navigation.ts create mode 100644 packages/json-validator/src/schema-utils.ts create mode 100644 packages/json-validator/src/schema.ts create mode 100644 packages/json-validator/src/semaphore.ts create mode 100644 packages/json-validator/src/state.ts create mode 100644 packages/json-validator/src/types.ts create mode 100644 packages/json-validator/src/validator-queue.ts create mode 100644 packages/json-validator/src/validator.ts create mode 100644 packages/json-validator/src/yaml-schema.ts create mode 100644 packages/json-validator/tsconfig.json diff --git a/packages/annotated-json/src/annotated-yaml.ts b/packages/annotated-json/src/annotated-yaml.ts index da1d80cc..3504e100 100644 --- a/packages/annotated-json/src/annotated-yaml.ts +++ b/packages/annotated-json/src/annotated-yaml.ts @@ -12,10 +12,11 @@ import { mappedIndexToLineCol, mappedLines, MappedString, - createSourceContext + createSourceContext, + EitherString } from "mapped-string"; -import { load as jsYamlParse } from "../external/js-yaml.js"; +import { load as jsYamlParse } from "./external/js-yaml.js"; import { QuartoJSONSchema } from "./js-yaml-quarto-schema"; import { tidyverseInfo } from "tidyverse-errors"; @@ -65,8 +66,8 @@ function jsYamlParseLenient(yml: string): unknown { } } -export function readAnnotatedYamlFromString(yml: string) { - return readAnnotatedYamlFromMappedString(asMappedString(yml))!; +export function parse(yml: EitherString): AnnotatedParse { + return readAnnotatedYamlFromMappedString(asMappedString(yml)); } export function readAnnotatedYamlFromMappedString( @@ -178,11 +179,11 @@ export function readAnnotatedYamlFromMappedString( export function buildJsYamlAnnotation(mappedYaml: MappedString) { const yml = mappedYaml.value; - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const stack: any[] = []; const results: AnnotatedParse[] = []; - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any function listener(what: string, state: any) { const { result, position, kind } = state; if (what === "close") { @@ -196,7 +197,7 @@ export function buildJsYamlAnnotation(mappedYaml: MappedString) { return; } } - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const components: any[] = []; while (results.length > 0) { const last = results[results.length - 1]; @@ -317,7 +318,7 @@ export function buildTreeSitterAnnotation( const annotate = ( node: TreeSitterNode, - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any result: any, components: AnnotatedParse[], ): AnnotatedParse => { @@ -348,7 +349,7 @@ export function buildTreeSitterAnnotation( const buildPair = (node: TreeSitterNode) => { let key, value; - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const children = node.children.filter((n: any) => n.type !== "comment"); if (children.length === 3) { @@ -409,7 +410,7 @@ export function buildTreeSitterAnnotation( } }, "flow_sequence": (node) => { - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any[] = [], components = []; for (let i = 0; i < node.childCount; ++i) { const child = node.child(i); @@ -423,7 +424,7 @@ export function buildTreeSitterAnnotation( return annotate(node, result, components); }, "block_mapping": (node) => { - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}, components: AnnotatedParse[] = []; for (let i = 0; i < node.childCount; ++i) { const child = node.child(i); @@ -452,7 +453,7 @@ export function buildTreeSitterAnnotation( }, "flow_pair": buildPair, "flow_mapping": (node) => { - // deno-lint-ignore no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}, components: AnnotatedParse[] = []; // skip flow_nodes at the boundary for (let i = 0; i < node.childCount; ++i) { diff --git a/packages/annotated-json/src/index.ts b/packages/annotated-json/src/index.ts index efd5bfc7..dc630e50 100644 --- a/packages/annotated-json/src/index.ts +++ b/packages/annotated-json/src/index.ts @@ -22,4 +22,5 @@ * SOFTWARE. */ -export * from "./types"; \ No newline at end of file +export * from "./types"; +export * from "./annotated-yaml"; \ No newline at end of file diff --git a/packages/json-validator/package.json b/packages/json-validator/package.json new file mode 100644 index 00000000..33cc813e --- /dev/null +++ b/packages/json-validator/package.json @@ -0,0 +1,17 @@ +{ + "name": "json-validator", + "version": "0.1.0", + "private": true, + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "tsconfig": "*", + "typescript": "^5.4.2", + "mapped-string": "*", + "tidyverse-errors": "*", + "annotated-json": "*" + }, + "devDependencies": { } +} + diff --git a/packages/json-validator/src/errors.ts b/packages/json-validator/src/errors.ts new file mode 100644 index 00000000..971d03c5 --- /dev/null +++ b/packages/json-validator/src/errors.ts @@ -0,0 +1,1061 @@ +/* + * errors.ts + * + * Functions for creating/setting yaml validation errors + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import * as colors from "ansi-colors"; + +import { YAMLSchemaT } from "./types"; + +import { quotedStringColor, TidyverseError } from "tidyverse-errors"; + +import { + mappedIndexToLineCol, + MappedString, + mappedString, + Range, + ErrorLocation +} from "mapped-string"; + +import { possibleSchemaKeys, possibleSchemaValues } from "./schema-utils"; + +// this truly needs to be in a separate package +import { editDistance } from "mapped-string"; + +import { AnnotatedParse, JSONValue } from "annotated-json"; + +import { + InstancePath, + LocalizedError, + ObjectSchema, + Schema, + schemaCall, + schemaDescription, + SchemaPath, + schemaType, +} from "./types"; + +import { formatLineRange, lines } from "mapped-string"; + + +//////////////////////////////////////////////////////////////////////////////// + +export function locationString(loc: ErrorLocation) { + const { start, end } = loc; + if (start.line === end.line) { + if (start.column === end.column) { + return `(line ${start.line + 1}, column ${start.column + 1})`; + } else { + return `(line ${start.line + 1}, columns ${start.column + 1}--${ + end.column + 1 + })`; + } + } else { + return `(line ${start.line + 1}, column ${start.column + 1} through line ${ + end.line + 1 + }, column ${end.column + 1})`; + } +} + +export function addFileInfo(msg: TidyverseError, src: MappedString) { + if (src.fileName !== undefined) { + msg.fileName = src.fileName; + } +} + +export function addInstancePathInfo( + msg: TidyverseError, + instancePath: (number | string)[], +) { + if (instancePath.length) { + const niceInstancePath = instancePath.map((s) => colors.blue(String(s))) + .join(":"); + msg.info["instance-path-location"] = + `The error happened in location ${niceInstancePath}.`; + } +} + +//////////////////////////////////////////////////////////////////////////////// + +export function setDefaultErrorHandlers(validator: YAMLSchemaT) { + validator.addHandler(ignoreExprViolations); + validator.addHandler(expandEmptySpan); + validator.addHandler(improveErrorHeadingForValueErrors); + validator.addHandler(checkForTypeMismatch); + validator.addHandler(checkForBadBoolean); + validator.addHandler(checkForBadColon); + validator.addHandler(checkForBadEquals); + validator.addHandler(identifyKeyErrors); + validator.addHandler(checkForNearbyCorrection); + validator.addHandler(checkForNearbyRequired); + validator.addHandler(schemaDefinedErrors); +} + +export function errorKeyword( + error: LocalizedError, +): string { + if (error.schemaPath.length === 0) { + return ""; + } + return String(error.schemaPath[error.schemaPath.length - 1]); +} + +export function schemaPathMatches( + error: LocalizedError, + strs: string[], +): boolean { + const schemaPath = error.schemaPath.slice(-strs.length); + if (schemaPath.length !== strs.length) { + return false; + } + return strs.every((str, i) => str === schemaPath[i]); +} + +export function getBadKey(error: LocalizedError): string | undefined { + if ( + error.schemaPath.indexOf("propertyNames") === -1 && + error.schemaPath.indexOf("closed") === -1 + ) { + return undefined; + } + const result = error.violatingObject.result; + if (typeof result !== "string") { + throw new Error( + "propertyNames error has a violating non-string.", + ); + } + return result; +} + +function getVerbatimInput(error: LocalizedError) { + return error.source.value; +} + +// this supports AnnotatedParse results built +// from deno yaml as well as tree-sitter. +function navigate( + path: (number | string)[], + annotation: AnnotatedParse | undefined, + returnKey = false, // if true, then return the *key* entry as the final result rather than the *value* entry. + pathIndex = 0, +): AnnotatedParse | undefined { + // this looks a little strange, but it's easier to catch the error + // here than in the different cases below + if (annotation === undefined) { + throw new Error("Can't navigate an undefined annotation"); + } + if (pathIndex >= path.length) { + return annotation; + } + if (annotation.kind === "mapping" || annotation.kind === "block_mapping") { + const { components } = annotation; + const searchKey = path[pathIndex]; + // this loop is inverted to provide better error messages in the + // case of repeated keys. Repeated keys are an error in any case, but + // the parsing by the validation infrastructure reports the last + // entry of a given key in the mapping as the one that counts + // (instead of the first, which would be what we'd get if running + // the loop forward). + // + // In that case, the validation errors will also point to the last + // entry. In order for the errors to be at least consistent, + // we then loop backwards + const lastKeyIndex = ~~((components.length - 1) / 2) * 2; + for (let i = lastKeyIndex; i >= 0; i -= 2) { + const key = components[i]!.result; + if (key === searchKey) { + if (returnKey && pathIndex === path.length - 1) { + return navigate(path, components[i], returnKey, pathIndex + 1); + } else { + return navigate(path, components[i + 1], returnKey, pathIndex + 1); + } + } + } + return annotation; + } else if ( + ["sequence", "block_sequence", "flow_sequence"].indexOf(annotation.kind) !== + -1 + ) { + const searchKey = Number(path[pathIndex]); + if ( + isNaN(searchKey) || searchKey < 0 || + searchKey >= annotation.components.length + ) { + return annotation; + } + return navigate( + path, + annotation.components[searchKey], + returnKey, + pathIndex + 1, + ); + } else { + return annotation; + } +} + +function isEmptyValue(error: LocalizedError) { + const rawVerbatimInput = getVerbatimInput(error); + return rawVerbatimInput.trim().length === 0; +} + +function getLastFragment( + instancePath: (string | number)[], +): undefined | number | string { + if (instancePath.length === 0) { + return undefined; + } + return instancePath[instancePath.length - 1]; +} + +/* reindent: produce a minimally-indented version +of the yaml string given. + +This is messy. Consider the following example in a chunk. + +```{r} +#| foo: +#| bar: 1 +#| bah: +#| baz: 3 +``` +Let's say we want to reindent the object starting at "bah:". + +we'd like the "reindent" to be + +bah: + baz: 3 + +but the string we have is 'bah:\n baz: 3', so we don't actually know +how much to cut. We need the column where the object +starts. _however_, in our mappedstrings infra, that is the column _in +the target space_, not the _original column_ information (which is +what we have). + +So we're going to use a heuristic. We first will figure out +all indentation amounts in the string. In the case above +we have Set(4). In an more-deeply nested object such as + +#| foo: +#| bar: 1 +#| bah: +#| baz: +#| bam: 3 + +the string would be 'bah:\n baz:\n bam: 3', so the set of +indentation amounts is Set(4, 6). The heuristic is that if +we find two or more amounts, then the difference between the smallest +two values is the desired amount. Otherwise, we indent by 2 characters. +*/ + +export function reindent( + str: string, +) { + const s: Set = new Set(); + const ls = lines(str); + for (const l of ls) { + const r = l.match("^[ ]+"); + if (r) { + s.add(r[0].length); + } + } + if (s.size === 0) { + return str; + } else if (s.size === 1) { + const v = Array.from(s)[0]; + const oldIndent = " ".repeat(v); + if (v <= 2) { + return str; + } + return ls.map((l) => l.startsWith(oldIndent) ? l.slice(v - 2) : l).join( + "\n", + ); + } else { + const [first, second] = Array.from(s); + const oldIndent = " ".repeat(first); + const newIndent = second - first; + if (newIndent >= first) { + return str; + } + return ls.map((l) => + l.startsWith(oldIndent) ? l.slice(first - newIndent) : l + ).join("\n"); + } +} + +function ignoreExprViolations( + error: LocalizedError, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): LocalizedError | null { + const { result } = error.violatingObject; + if ( + typeof result !== "object" || + Array.isArray(result) || + result === null || + error.schemaPath.slice(-1)[0] !== "type" + ) { + return error; + } + + if (result.tag === "!expr" && typeof result.value === "string") { + // assume that this validation error came from !expr, drop the error. + return null; + } else { + return error; + } +} + +function formatHeadingForKeyError( + _error: LocalizedError, + _parse: AnnotatedParse, + _schema: Schema, + key: string, +): string { + return `property name ${colors.blue(key)} is invalid`; +} + +function formatHeadingForValueError( + error: LocalizedError, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): string { + const rawVerbatimInput = reindent(getVerbatimInput(error)); + const rawLines = lines(rawVerbatimInput); + let verbatimInput: string; + if (rawLines.length > 4) { + verbatimInput = quotedStringColor( + [...rawLines.slice(0, 2), "...", ...rawLines.slice(-2)] + .join("\n"), + ); + } else { + verbatimInput = quotedStringColor(rawVerbatimInput); + } + + const empty = isEmptyValue(error); + const lastFragment = getLastFragment(error.instancePath); + + switch (typeof lastFragment) { + case "undefined": // empty + if (empty) { + return "YAML value is missing."; + } else { + return `YAML value ${verbatimInput} must ${ + schemaDescription(error.schema) + }.`; + } + case "number": // array + if (empty) { + return `Array entry ${lastFragment + 1} is empty but it must instead ${ + schemaDescription(error.schema) + }.`; + } else { + return `Array entry ${ + lastFragment + 1 + } with value ${verbatimInput} failed to ${ + schemaDescription(error.schema) + }.`; + } + case "string": { // object + const formatLastFragment = '"' + colors.blue(lastFragment) + '"'; + if (empty) { + return `Field ${formatLastFragment} has empty value but it must instead ${ + schemaDescription(error.schema) + }`; + } else { + if (verbatimInput.indexOf("\n") !== -1) { + return `Field ${formatLastFragment} has value + +${verbatimInput} + +The value must instead ${schemaDescription(error.schema)}.`; + } else { + return `Field ${formatLastFragment} has value ${verbatimInput}, which must instead ${ + schemaDescription(error.schema) + }`; + } + } + } + } +} + +function identifyKeyErrors( + error: LocalizedError, + parse: AnnotatedParse, + schema: Schema, +): LocalizedError { + if ( + error.schemaPath.indexOf("propertyNames") === -1 && + error.schemaPath.indexOf("closed") === -1 + ) { + return error; + } + + const badKey = getBadKey(error); + if (badKey) { + if ( + error.instancePath.length && + error.instancePath[error.instancePath.length - 1] !== badKey + ) { + addInstancePathInfo( + error.niceError, + [...error.instancePath, badKey], + ); + } else { + addInstancePathInfo( + error.niceError, + error.instancePath, + ); + } + + error.niceError.heading = formatHeadingForKeyError( + error, + parse, + schema, + badKey, + ); + } + + return error; +} + +function improveErrorHeadingForValueErrors( + error: LocalizedError, + parse: AnnotatedParse, + schema: Schema, +): LocalizedError { + // TODO this check is supposed to be "don't mess with errors where + // the violating object is in key position". I think my condition + // catches everything but I'm not positive. + // + // 2022-02-08: yup, I was wrong, there's also missing properties errors + // which are not addressed here. + + if ( + error.schemaPath.indexOf("propertyNames") !== -1 || + error.schemaPath.indexOf("closed") !== -1 || + errorKeyword(error) === "required" + ) { + return error; + } + return { + ...error, + niceError: { + ...error.niceError, + heading: formatHeadingForValueError(error, parse, schema), + }, + }; +} + +// in cases where the span of an error message is empty (which happens +// when eg an empty value is associated with a key), we artificially +// move the span to the _key_ so that the error is printed somewhat +// more legibly. +function expandEmptySpan( + error: LocalizedError, + parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): LocalizedError { + if ( + error.location.start.line !== error.location.end.line || + error.location.start.column !== error.location.end.column || + !isEmptyValue(error) || + (typeof getLastFragment(error.instancePath) === "undefined") + ) { + return error; + } + + const lastKey = navigate( + error.instancePath, + parse, + true, + )!; + const locF = mappedIndexToLineCol(parse.source); + try { + const location = { + start: locF(lastKey.start), + end: locF(lastKey.end), + }; + + return { + ...error, + location, + niceError: { + ...error.niceError, + location, + }, + }; + } catch (_e) { + return error; + } +} + +function checkForTypeMismatch( + error: LocalizedError, + parse: AnnotatedParse, + schema: Schema, +) { + const rawVerbatimInput = getVerbatimInput(error); + const rawLines = lines(rawVerbatimInput); + let verbatimInput: string; + if (rawLines.length > 4) { + verbatimInput = quotedStringColor( + [...rawLines.slice(0, 2), "...", ...rawLines.slice(-2)] + .join("\n"), + ); + } else { + verbatimInput = quotedStringColor(rawVerbatimInput); + } + const goodType = (obj: JSONValue) => { + if (Array.isArray(obj)) { + return "an array"; + } + if (obj === null) { + return "a null value"; + } + return typeof obj; + }; + + if (errorKeyword(error) === "type" && rawVerbatimInput.length > 0) { + const reindented = reindent(verbatimInput); + const subject = (reindented.indexOf("\n") === -1) + ? `The value ${reindented} ` + : `The value + +${reindented} + +`; + const newError: TidyverseError = { + ...error.niceError, + heading: formatHeadingForValueError( + error, + parse, + schema, + ), + error: [ + `${subject}is of type ${ + goodType( + error.violatingObject + .result, + ) + }.`, + ], + info: {}, + location: error.niceError.location, + }; + addInstancePathInfo(newError, error.instancePath); + addFileInfo(newError, error.source); + return { + ...error, + niceError: newError, + }; + } + return error; +} + +function checkForBadBoolean( + error: LocalizedError, + parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +) { + const schema = error.schema; + if ( + !(typeof error.violatingObject.result === "string" && + errorKeyword(error) === "type" && + (schemaType(schema) === "boolean")) + ) { + return error; + } + const strValue = error.violatingObject.result; + const verbatimInput = quotedStringColor(getVerbatimInput(error)); + + // from https://yaml.org/type/bool.html + const yesses = new Set("y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON".split("|")); + const nos = new Set("n|N|no|No|NO|false|False|FALSE|off|Off|OFF".split("|")); + let fix; + if (yesses.has(strValue)) { + fix = true; + } else if (nos.has(strValue)) { + fix = false; + } else { + return error; + } + + const errorMessage = `The value ${verbatimInput} is a string.`; + const suggestion1 = + `Quarto uses YAML 1.2, which interprets booleans strictly.`; + const suggestion2 = `Try using ${quotedStringColor(String(fix))} instead.`; + const newError: TidyverseError = { + heading: formatHeadingForValueError(error, parse, schema), + error: [errorMessage], + info: {}, + location: error.niceError.location, + }; + addInstancePathInfo(newError, error.instancePath); + addFileInfo(newError, error.source); + newError.info["yaml-version-1.2"] = suggestion1; + newError.info["suggestion-fix"] = suggestion2; + + return { + ...error, + niceError: newError, + }; +} + +// provides better error message when +// "echo:false" +function checkForBadColon( + error: LocalizedError, + parse: AnnotatedParse, + schema: Schema, +) { + if (typeof error.violatingObject.result !== "string") { + return error; + } + + if (!schemaPathMatches(error, ["object", "type"])) { + return error; + } + + if ( + !((error.violatingObject.result as string).match(/^.+:[^ ].*$/)) + ) { + return error; + } + + const verbatimInput = quotedStringColor(getVerbatimInput(error)); + const errorMessage = `The value ${verbatimInput} is a string.`; + const suggestion1 = + `In YAML, key-value pairs in objects must be separated by a space.`; + const suggestion2 = `Did you mean ${ + quotedStringColor( + quotedStringColor(getVerbatimInput(error)).replace(/:/g, ": "), + ) + } instead?`; + const newError: TidyverseError = { + heading: formatHeadingForValueError(error, parse, schema), + error: [errorMessage], + info: {}, + location: error.niceError.location, + }; + addInstancePathInfo(newError, error.instancePath); + addFileInfo(newError, error.source); + newError.info["yaml-key-value-pairs"] = suggestion1; + newError.info["suggestion-fix"] = suggestion2; + + return { + ...error, + niceError: newError, + }; +} + +function checkForBadEquals( + error: LocalizedError, + parse: AnnotatedParse, + schema: Schema, +) { + if (typeof error.violatingObject.result !== "string") { + return error; + } + + if ( + !schemaPathMatches(error, ["object", "type"]) && + !schemaPathMatches(error, ["object", "propertyNames", "string", "pattern"]) + ) { + return error; + } + + if ( + !((error.violatingObject.result as string).match(/^.+ *= *.+$/)) + ) { + return error; + } + + const verbatimInput = quotedStringColor(getVerbatimInput(error)); + const errorMessage = `The value ${verbatimInput} is a string.`; + const suggestion1 = + `In YAML, key-value pairs in objects must be separated by a colon and a space.`; + const suggestion2 = `Did you mean ${ + quotedStringColor( + quotedStringColor(getVerbatimInput(error)).replace(/ *= */g, ": "), + ) + } instead?`; + const newError: TidyverseError = { + heading: formatHeadingForValueError(error, parse, schema), + error: [errorMessage], + info: {}, + location: error.niceError.location, + }; + addInstancePathInfo(newError, error.instancePath); + addFileInfo(newError, error.source); + newError.info["yaml-key-value-pairs"] = suggestion1; + newError.info["suggestion-fix"] = suggestion2; + + return { + ...error, + niceError: newError, + }; +} + +function createErrorFragments(error: LocalizedError) { + const rawVerbatimInput = getVerbatimInput(error); + const verbatimInput = quotedStringColor(reindent(rawVerbatimInput)); + + const pathFragments = error.instancePath.map((s) => colors.blue(String(s))); + + return { + location: locationString(error.location), + fullPath: pathFragments.join(":"), + key: pathFragments[pathFragments.length - 1], + value: verbatimInput, + }; +} + +// FIXME we should navigate the schema path +// to find the schema-defined error in case it's not +// error.schema +function schemaDefinedErrors( + error: LocalizedError, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): LocalizedError { + const schema = error.schema; + if (schema === true || schema === false) { + return error; + } + if (schema.errorMessage === undefined) { + return error; + } + if (typeof schema.errorMessage !== "string") { + return error; + } + + let result = schema.errorMessage; + for (const [k, v] of Object.entries(createErrorFragments(error))) { + result = result.replace("${" + k + "}", v); + } + + return { + ...error, + niceError: { + ...error.niceError, + heading: result, + }, + }; +} + +function checkForNearbyRequired( + error: LocalizedError, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): LocalizedError { + const schema = error.schema; + + if (errorKeyword(error) !== "required") { + return error; + } + const missingKeys: string[] = []; + const errObj = error.violatingObject.result as Record; + const keys = Object.keys(errObj); + + schemaCall(schema, { + object(s: ObjectSchema) { + if (s.required === undefined) { + throw new Error( + "required schema error without a required field", + ); + } + // find required properties. + for (const r of s.required) { + if (keys.indexOf(r) === -1) { + missingKeys.push(r); + } + } + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + }, (_) => { + throw new Error("required error on a non-object schema"); + }); + + for (const missingKey of missingKeys) { + let bestCorrection: string[] | undefined; + let bestDistance = Infinity; + for (const correction of keys) { + const d = editDistance(correction, missingKey); + if (d < bestDistance) { + bestCorrection = [correction]; + bestDistance = d; + } else if (d === bestDistance) { + bestCorrection!.push(correction); + bestDistance = d; + } + } + + // TODO we need a defensible way of determining a cutoff here. + // One idea is to turn this into a hypothesis test, checking random + // english words against a dictionary and looking at the distribution + // of edit distances. Presently, we hack. + + // if best edit distance is more than 30% of the word, don't suggest + if (bestDistance > missingKey.length * 10 * 0.3) { + continue; + } + + const suggestions = bestCorrection!.map((s: string) => colors.blue(s)); + if (suggestions.length === 1) { + error.niceError.info[`did-you-mean-key`] = `Is ${ + suggestions[0] + } a typo of ${colors.blue(missingKey)}?`; + } else if (suggestions.length === 2) { + error.niceError.info[`did-you-mean-key`] = `Is ${suggestions[0]} or ${ + suggestions[1] + } a typo of ${colors.blue(missingKey)}?`; + } else { + suggestions[suggestions.length - 1] = `or ${ + suggestions[suggestions.length - 1] + }`; + error.niceError.info[`did-you-mean-key`] = `Is one of ${ + suggestions.join(", ") + } a typo of ${colors.blue(missingKey)}?`; + } + } + + return error; +} + +function checkForNearbyCorrection( + error: LocalizedError, + parse: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: Schema, +): LocalizedError { + const schema = error.schema; + const corrections: string[] = []; + + let errVal = ""; + let keyOrValue = ""; + const key = getBadKey(error); + + if (key) { + errVal = key; + corrections.push(...possibleSchemaKeys(schema)); + keyOrValue = "key"; + } else { + const val = navigate(error.instancePath, parse); + + if (typeof val!.result !== "string") { + // error didn't happen in a string, can't suggest corrections. + return error; + } + errVal = val!.result; + corrections.push(...possibleSchemaValues(schema)); + keyOrValue = "value"; + } + if (corrections.length === 0) { + return error; + } + + let bestCorrection: string[] | undefined; + let bestDistance = Infinity; + for (const correction of corrections) { + const d = editDistance(correction, errVal); + if (d < bestDistance) { + bestCorrection = [correction]; + bestDistance = d; + } else if (d === bestDistance) { + bestCorrection!.push(correction); + bestDistance = d; + } + } + + // TODO we need a defensible way of determining a cutoff here. + // One idea is to turn this into a hypothesis test, checking random + // english words against a dictionary and looking at the distribution + // of edit distances. Presently, we hack. + + // if best edit distance is more than 30% of the word, don't suggest + if (bestDistance > errVal.length * 10 * 0.3) { + return error; + } + + const suggestions = bestCorrection!.map((s: string) => colors.blue(s)); + if (suggestions.length === 1) { + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ + suggestions[0] + }?`; + } else if (suggestions.length === 2) { + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ + suggestions[0] + } or ${suggestions[1]}?`; + } else { + suggestions[suggestions.length - 1] = `or ${ + suggestions[suggestions.length - 1] + }`; + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ + suggestions.join(", ") + }?`; + } + + return error; +} + +/** + * Create a formatted string describing the surroundings of an error. + * Used in the generation of nicely-formatted error messages. + * + * @param src the string containing the source of the error + * @param location the location range in src + * @returns a string containing a formatted description of the context around the error + */ +export function createSourceContext( + src: MappedString, + location: Range, +): string { + if (src.value.length === 0) { + // if the file is empty, don't try to create a source context + return ""; + } + const startMapResult = src.map(location.start, true); + const endMapResult = src.map(location.end, true); + + const locF = mappedIndexToLineCol(src); + + let sourceLocation; + try { + sourceLocation = { + start: locF(location.start), + end: locF(location.end), + }; + } catch (_e) { + sourceLocation = { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }; + } + + if (startMapResult === undefined || endMapResult === undefined) { + throw new Error( + `createSourceContext called with bad location ${location.start}-${location.end}.`, + ); + } + + if (startMapResult.originalString !== endMapResult.originalString) { + throw new Error( + "don't know how to create source context across different source files", + ); + } + const originalString = startMapResult.originalString; + // TODO this is computed every time, might be inefficient on large files. + const nLines = lines(originalString.value).length; + + const { + start, + end, + } = sourceLocation; + const { + prefixWidth, + lines: formattedLines, + } = formatLineRange( + originalString.value, + Math.max(0, start.line - 1), + Math.min(end.line + 1, nLines - 1), + ); + const contextLines: string[] = []; + let mustPrintEllipsis = true; + for (const { lineNumber, content, rawLine } of formattedLines) { + if (lineNumber < start.line || lineNumber > end.line) { + if (rawLine.trim().length) { + contextLines.push(content); + } + } else { + if ( + lineNumber >= start.line + 2 && lineNumber <= end.line - 2 + ) { + if (mustPrintEllipsis) { + mustPrintEllipsis = false; + contextLines.push("..."); + } + } else { + const startColumn = lineNumber > start.line ? 0 : start.column; + const endColumn = lineNumber < end.line ? rawLine.length : end.column; + contextLines.push(content); + contextLines.push( + " ".repeat(prefixWidth + startColumn - 1) + + "~".repeat(endColumn - startColumn + 1), + ); + } + } + } + return contextLines.join("\n"); +} + +export function createLocalizedError(obj: { + violatingObject: AnnotatedParse; + instancePath: InstancePath; + schemaPath: SchemaPath; + source: MappedString; + message: string; + schema: Schema; +}): LocalizedError { + const { + violatingObject, + instancePath, + schemaPath, + source, + message, + schema, + } = obj; + const locF = mappedIndexToLineCol(source); + + let location; + try { + location = { + start: locF(violatingObject.start), + end: locF(violatingObject.end), + }; + } catch (_e) { + location = { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }; + } + + const mapResult = source.map(violatingObject.start); + const fileName = mapResult ? mapResult.originalString.fileName : undefined; + return { + source: mappedString(source, [{ + start: violatingObject.start, + end: violatingObject.end, + }]), + violatingObject: violatingObject, + instancePath, + schemaPath, + schema, + message, + location: location!, + niceError: { + heading: message, + error: [], + info: {}, + fileName, + location: location!, + sourceContext: createSourceContext(violatingObject.source, { + start: violatingObject.start, + end: violatingObject.end, + }), // location!), + }, + }; +} diff --git a/packages/json-validator/src/external/regexpp.mjs b/packages/json-validator/src/external/regexpp.mjs new file mode 100644 index 00000000..c8bae6c4 --- /dev/null +++ b/packages/json-validator/src/external/regexpp.mjs @@ -0,0 +1,2089 @@ +/*! @author Toru Nagashima */ + + +var ast = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +let largeIdStartRanges = undefined; +let largeIdContinueRanges = undefined; +function isIdStart(cp) { + if (cp < 0x41) + return false; + if (cp < 0x5b) + return true; + if (cp < 0x61) + return false; + if (cp < 0x7b) + return true; + return isLargeIdStart(cp); +} +function isIdContinue(cp) { + if (cp < 0x30) + return false; + if (cp < 0x3a) + return true; + if (cp < 0x41) + return false; + if (cp < 0x5b) + return true; + if (cp === 0x5f) + return true; + if (cp < 0x61) + return false; + if (cp < 0x7b) + return true; + return isLargeIdStart(cp) || isLargeIdContinue(cp); +} +function isLargeIdStart(cp) { + return isInRange(cp, largeIdStartRanges || (largeIdStartRanges = initLargeIdStartRanges())); +} +function isLargeIdContinue(cp) { + return isInRange(cp, largeIdContinueRanges || + (largeIdContinueRanges = initLargeIdContinueRanges())); +} +function initLargeIdStartRanges() { + return restoreRanges("4q 0 b 0 5 0 6 m 2 u 2 cp 5 b f 4 8 0 2 0 3m 4 2 1 3 3 2 0 7 0 2 2 2 0 2 j 2 2a 2 3u 9 4l 2 11 3 0 7 14 20 q 5 3 1a 16 10 1 2 2q 2 0 g 1 8 1 b 2 3 0 h 0 2 t u 2g c 0 p w a 1 5 0 6 l 5 0 a 0 4 0 o o 8 a 1i k 2 h 1p 1h 4 0 j 0 8 9 g f 5 7 3 1 3 l 2 6 2 0 4 3 4 0 h 0 e 1 2 2 f 1 b 0 9 5 5 1 3 l 2 6 2 1 2 1 2 1 w 3 2 0 k 2 h 8 2 2 2 l 2 6 2 1 2 4 4 0 j 0 g 1 o 0 c 7 3 1 3 l 2 6 2 1 2 4 4 0 v 1 2 2 g 0 i 0 2 5 4 2 2 3 4 1 2 0 2 1 4 1 4 2 4 b n 0 1h 7 2 2 2 m 2 f 4 0 r 2 6 1 v 0 5 7 2 2 2 m 2 9 2 4 4 0 x 0 2 1 g 1 i 8 2 2 2 14 3 0 h 0 6 2 9 2 p 5 6 h 4 n 2 8 2 0 3 6 1n 1b 2 1 d 6 1n 1 2 0 2 4 2 n 2 0 2 9 2 1 a 0 3 4 2 0 m 3 x 0 1s 7 2 z s 4 38 16 l 0 h 5 5 3 4 0 4 1 8 2 5 c d 0 i 11 2 0 6 0 3 16 2 98 2 3 3 6 2 0 2 3 3 14 2 3 3 w 2 3 3 6 2 0 2 3 3 e 2 1k 2 3 3 1u 12 f h 2d 3 5 4 h7 3 g 2 p 6 22 4 a 8 c 2 3 f h f h f c 2 2 g 1f 10 0 5 0 1w 2g 8 14 2 0 6 1x b u 1e t 3 4 c 17 5 p 1j m a 1g 2b 0 2m 1a i 6 1k t e 1 b 17 r z 16 2 b z 3 8 8 16 3 2 16 3 2 5 2 1 4 0 6 5b 1t 7p 3 5 3 11 3 5 3 7 2 0 2 0 2 0 2 u 3 1g 2 6 2 0 4 2 2 6 4 3 3 5 5 c 6 2 2 6 39 0 e 0 h c 2u 0 5 0 3 9 2 0 3 5 7 0 2 0 2 0 2 f 3 3 6 4 5 0 i 14 22g 1a 2 1a 2 3o 7 3 4 1 d 11 2 0 6 0 3 1j 8 0 h m a 6 2 6 2 6 2 6 2 6 2 6 2 6 2 6 fb 2 q 8 8 4 3 4 5 2d 5 4 2 2h 2 3 6 16 2 2l i v 1d f e9 533 1t g70 4 wc 1w 19 3 7g 4 f b 1 l 1a h u 3 27 14 8 3 2u 3 1g 3 8 17 c 2 2 2 3 2 m u 1f f 1d 1r 5 4 0 2 1 c r b m q s 8 1a t 0 h 4 2 9 b 4 2 14 o 2 2 7 l m 4 0 4 1d 2 0 4 1 3 4 3 0 2 0 p 2 3 a 8 2 d 5 3 5 3 5 a 6 2 6 2 16 2 d 7 36 u 8mb d m 5 1c 6it a5 3 2x 13 6 d 4 6 0 2 9 2 c 2 4 2 0 2 1 2 1 2 2z y a2 j 1r 3 1h 15 b 39 4 2 3q 11 p 7 p c 2g 4 5 3 5 3 5 3 2 10 b 2 p 2 i 2 1 2 e 3 d z 3e 1y 1g 7g s 4 1c 1c v e t 6 11 b t 3 z 5 7 2 4 17 4d j z 5 z 5 13 9 1f 4d 8m a l b 7 49 5 3 0 2 17 2 1 4 0 3 m b m a u 1u i 2 1 b l b p 1z 1j 7 1 1t 0 g 3 2 2 2 s 17 s 4 s 10 7 2 r s 1h b l b i e h 33 20 1k 1e e 1e e z 9p 15 7 1 27 s b 0 9 l 2z k s m d 1g 24 18 x o r z u 0 3 0 9 y 4 0 d 1b f 3 m 0 2 0 10 h 2 o 2d 6 2 0 2 3 2 e 2 9 8 1a 13 7 3 1 3 l 2 6 2 1 2 4 4 0 j 0 d 4 4f 1g j 3 l 2 v 1b l 1 2 0 55 1a 16 3 11 1b l 0 1o 16 e 0 20 q 6e 17 39 1r w 7 3 0 3 7 2 1 2 n g 0 2 0 2n 7 3 12 h 0 2 0 t 0 b 13 8 0 m 0 c 19 k 0 z 1k 7c 8 2 10 i 0 1e t 35 6 2 1 2 11 m 0 q 5 2 1 2 v f 0 94 i 5a 0 28 pl 2v 32 i 5f 24d tq 34i g6 6nu fs 8 u 36 t j 1b h 3 w k 6 i j5 1r 3l 22 6 0 1v c 1t 1 2 0 t 4qf 9 yd 17 8 6wo 7y 1e 2 i 3 9 az 1s5 2y 6 c 4 8 8 9 4mf 2c 2 1y 2 1 3 0 3 1 3 3 2 b 2 0 2 6 2 1s 2 3 3 7 2 6 2 r 2 3 2 4 2 0 4 6 2 9f 3 o 2 o 2 u 2 o 2 u 2 o 2 u 2 o 2 u 2 o 2 7 1th 18 b 6 h 0 aa 17 105 5g 1o 1v 8 0 xh 3 2 q 2 1 2 0 3 0 2 9 2 3 2 0 2 0 7 0 5 0 2 0 2 0 2 2 2 1 2 0 3 0 2 0 2 0 2 0 2 0 2 1 2 0 3 3 2 6 2 3 2 3 2 0 2 9 2 g 6 2 2 4 2 g 3et wyl z 378 c 65 3 4g1 f 5rk 2e8 f1 15v 3t6"); +} +function initLargeIdContinueRanges() { + return restoreRanges("53 0 g9 33 o 0 70 4 7e 18 2 0 2 1 2 1 2 0 21 a 1d u 7 0 2u 6 3 5 3 1 2 3 3 9 o 0 v q 2k a g 9 y 8 a 0 p 3 2 8 2 2 2 4 18 2 3c e 2 w 1j 2 2 h 2 6 b 1 3 9 i 2 1l 0 2 6 3 1 3 2 a 0 b 1 3 9 f 0 3 2 1l 0 2 4 5 1 3 2 4 0 l b 4 0 c 2 1l 0 2 7 2 2 2 2 l 1 3 9 b 5 2 2 1l 0 2 6 3 1 3 2 8 2 b 1 3 9 j 0 1o 4 4 2 2 3 a 0 f 9 h 4 1m 6 2 2 2 3 8 1 c 1 3 9 i 2 1l 0 2 6 2 2 2 3 8 1 c 1 3 9 h 3 1k 1 2 6 2 2 2 3 a 0 b 1 3 9 i 2 1z 0 5 5 2 0 2 7 7 9 3 1 1q 0 3 6 d 7 2 9 2g 0 3 8 c 5 3 9 1r 1 7 9 c 0 2 0 2 0 5 1 1e j 2 1 6 a 2 z a 0 2t j 2 9 d 3 5 2 2 2 3 6 4 3 e b 2 e jk 2 a 8 pt 2 u 2 u 1 v 1 1t v a 0 3 9 y 2 3 9 40 0 3b b 5 b b 9 3l a 1p 4 1m 9 2 s 3 a 7 9 n d 2 1 1s 4 1c g c 9 i 8 d 2 v c 3 9 19 d 1d j 9 9 7 9 3b 2 2 k 5 0 7 0 3 2 5j 1l 2 4 g0 1 k 0 3g c 5 0 4 b 2db 2 3y 0 2p v ff 5 2y 1 n7q 9 1y 0 5 9 x 1 29 1 7l 0 4 0 5 0 o 4 5 0 2c 1 1f h b 9 7 h e a t 7 q c 19 3 1c d g 9 c 0 b 9 1c d d 0 9 1 3 9 y 2 1f 0 2 2 3 1 6 1 2 0 16 4 6 1 6l 7 2 1 3 9 fmt 0 ki f h f 4 1 p 2 5d 9 12 0 ji 0 6b 0 46 4 86 9 120 2 2 1 6 3 15 2 5 0 4m 1 fy 3 9 9 aa 1 4a a 4w 2 1i e w 9 g 3 1a a 1i 9 7 2 11 d 2 9 6 1 19 0 d 2 1d d 9 3 2 b 2b b 7 0 4h b 6 9 7 3 1k 1 2 6 3 1 3 2 a 0 b 1 3 6 4 4 5d h a 9 5 0 2a j d 9 5y 6 3 8 s 1 2b g g 9 2a c 9 9 2c e 5 9 6r e 4m 9 1z 5 2 1 3 3 2 0 2 1 d 9 3c 6 3 6 4 0 t 9 15 6 2 3 9 0 a a 1b f ba 7 2 7 h 9 1l l 2 d 3f 5 4 0 2 1 2 6 2 0 9 9 1d 4 2 1 2 4 9 9 96 3 ewa 9 3r 4 1o 6 q 9 s6 0 2 1i 8 3 2a 0 c 1 f58 1 43r 4 4 5 9 7 3 6 v 3 45 2 13e 1d e9 1i 5 1d 9 0 f 0 n 4 2 e 11t 6 2 g 3 6 2 1 2 4 7a 6 a 9 bn d 15j 6 32 6 6 9 3o7 9 gvt3 6n"); +} +function isInRange(cp, ranges) { + let l = 0, r = (ranges.length / 2) | 0, i = 0, min = 0, max = 0; + while (l < r) { + i = ((l + r) / 2) | 0; + min = ranges[2 * i]; + max = ranges[2 * i + 1]; + if (cp < min) { + r = i; + } + else if (cp > max) { + l = i + 1; + } + else { + return true; + } + } + return false; +} +function restoreRanges(data) { + let last = 0; + return data.split(" ").map(s => (last += parseInt(s, 36) | 0)); +} + +class DataSet { + constructor(raw2018, raw2019, raw2020, raw2021) { + this._raw2018 = raw2018; + this._raw2019 = raw2019; + this._raw2020 = raw2020; + this._raw2021 = raw2021; + } + get es2018() { + return (this._set2018 || (this._set2018 = new Set(this._raw2018.split(" ")))); + } + get es2019() { + return (this._set2019 || (this._set2019 = new Set(this._raw2019.split(" ")))); + } + get es2020() { + return (this._set2020 || (this._set2020 = new Set(this._raw2020.split(" ")))); + } + get es2021() { + return (this._set2021 || (this._set2021 = new Set(this._raw2021.split(" ")))); + } +} +const gcNameSet = new Set(["General_Category", "gc"]); +const scNameSet = new Set(["Script", "Script_Extensions", "sc", "scx"]); +const gcValueSets = new DataSet("C Cased_Letter Cc Cf Close_Punctuation Cn Co Combining_Mark Connector_Punctuation Control Cs Currency_Symbol Dash_Punctuation Decimal_Number Enclosing_Mark Final_Punctuation Format Initial_Punctuation L LC Letter Letter_Number Line_Separator Ll Lm Lo Lowercase_Letter Lt Lu M Mark Math_Symbol Mc Me Mn Modifier_Letter Modifier_Symbol N Nd Nl No Nonspacing_Mark Number Open_Punctuation Other Other_Letter Other_Number Other_Punctuation Other_Symbol P Paragraph_Separator Pc Pd Pe Pf Pi Po Private_Use Ps Punctuation S Sc Separator Sk Sm So Space_Separator Spacing_Mark Surrogate Symbol Titlecase_Letter Unassigned Uppercase_Letter Z Zl Zp Zs cntrl digit punct", "", "", ""); +const scValueSets = new DataSet("Adlam Adlm Aghb Ahom Anatolian_Hieroglyphs Arab Arabic Armenian Armi Armn Avestan Avst Bali Balinese Bamu Bamum Bass Bassa_Vah Batak Batk Beng Bengali Bhaiksuki Bhks Bopo Bopomofo Brah Brahmi Brai Braille Bugi Buginese Buhd Buhid Cakm Canadian_Aboriginal Cans Cari Carian Caucasian_Albanian Chakma Cham Cher Cherokee Common Copt Coptic Cprt Cuneiform Cypriot Cyrillic Cyrl Deseret Deva Devanagari Dsrt Dupl Duployan Egyp Egyptian_Hieroglyphs Elba Elbasan Ethi Ethiopic Geor Georgian Glag Glagolitic Gonm Goth Gothic Gran Grantha Greek Grek Gujarati Gujr Gurmukhi Guru Han Hang Hangul Hani Hano Hanunoo Hatr Hatran Hebr Hebrew Hira Hiragana Hluw Hmng Hung Imperial_Aramaic Inherited Inscriptional_Pahlavi Inscriptional_Parthian Ital Java Javanese Kaithi Kali Kana Kannada Katakana Kayah_Li Khar Kharoshthi Khmer Khmr Khoj Khojki Khudawadi Knda Kthi Lana Lao Laoo Latin Latn Lepc Lepcha Limb Limbu Lina Linb Linear_A Linear_B Lisu Lyci Lycian Lydi Lydian Mahajani Mahj Malayalam Mand Mandaic Mani Manichaean Marc Marchen Masaram_Gondi Meetei_Mayek Mend Mende_Kikakui Merc Mero Meroitic_Cursive Meroitic_Hieroglyphs Miao Mlym Modi Mong Mongolian Mro Mroo Mtei Mult Multani Myanmar Mymr Nabataean Narb Nbat New_Tai_Lue Newa Nko Nkoo Nshu Nushu Ogam Ogham Ol_Chiki Olck Old_Hungarian Old_Italic Old_North_Arabian Old_Permic Old_Persian Old_South_Arabian Old_Turkic Oriya Orkh Orya Osage Osge Osma Osmanya Pahawh_Hmong Palm Palmyrene Pau_Cin_Hau Pauc Perm Phag Phags_Pa Phli Phlp Phnx Phoenician Plrd Prti Psalter_Pahlavi Qaac Qaai Rejang Rjng Runic Runr Samaritan Samr Sarb Saur Saurashtra Sgnw Sharada Shavian Shaw Shrd Sidd Siddham SignWriting Sind Sinh Sinhala Sora Sora_Sompeng Soyo Soyombo Sund Sundanese Sylo Syloti_Nagri Syrc Syriac Tagalog Tagb Tagbanwa Tai_Le Tai_Tham Tai_Viet Takr Takri Tale Talu Tamil Taml Tang Tangut Tavt Telu Telugu Tfng Tglg Thaa Thaana Thai Tibetan Tibt Tifinagh Tirh Tirhuta Ugar Ugaritic Vai Vaii Wara Warang_Citi Xpeo Xsux Yi Yiii Zanabazar_Square Zanb Zinh Zyyy", "Dogr Dogra Gong Gunjala_Gondi Hanifi_Rohingya Maka Makasar Medefaidrin Medf Old_Sogdian Rohg Sogd Sogdian Sogo", "Elym Elymaic Hmnp Nand Nandinagari Nyiakeng_Puachue_Hmong Wancho Wcho", "Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi"); +const binPropertySets = new DataSet("AHex ASCII ASCII_Hex_Digit Alpha Alphabetic Any Assigned Bidi_C Bidi_Control Bidi_M Bidi_Mirrored CI CWCF CWCM CWKCF CWL CWT CWU Case_Ignorable Cased Changes_When_Casefolded Changes_When_Casemapped Changes_When_Lowercased Changes_When_NFKC_Casefolded Changes_When_Titlecased Changes_When_Uppercased DI Dash Default_Ignorable_Code_Point Dep Deprecated Dia Diacritic Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Ext Extender Gr_Base Gr_Ext Grapheme_Base Grapheme_Extend Hex Hex_Digit IDC IDS IDSB IDST IDS_Binary_Operator IDS_Trinary_Operator ID_Continue ID_Start Ideo Ideographic Join_C Join_Control LOE Logical_Order_Exception Lower Lowercase Math NChar Noncharacter_Code_Point Pat_Syn Pat_WS Pattern_Syntax Pattern_White_Space QMark Quotation_Mark RI Radical Regional_Indicator SD STerm Sentence_Terminal Soft_Dotted Term Terminal_Punctuation UIdeo Unified_Ideograph Upper Uppercase VS Variation_Selector White_Space XIDC XIDS XID_Continue XID_Start space", "Extended_Pictographic", "", "EBase EComp EMod EPres ExtPict"); +function isValidUnicodeProperty(version, name, value) { + if (gcNameSet.has(name)) { + return version >= 2018 && gcValueSets.es2018.has(value); + } + if (scNameSet.has(name)) { + return ((version >= 2018 && scValueSets.es2018.has(value)) || + (version >= 2019 && scValueSets.es2019.has(value)) || + (version >= 2020 && scValueSets.es2020.has(value)) || + (version >= 2021 && scValueSets.es2021.has(value))); + } + return false; +} +function isValidLoneUnicodeProperty(version, value) { + return ((version >= 2018 && binPropertySets.es2018.has(value)) || + (version >= 2019 && binPropertySets.es2019.has(value)) || + (version >= 2021 && binPropertySets.es2021.has(value))); +} + +const Backspace = 0x08; +const CharacterTabulation = 0x09; +const LineFeed = 0x0a; +const LineTabulation = 0x0b; +const FormFeed = 0x0c; +const CarriageReturn = 0x0d; +const ExclamationMark = 0x21; +const DollarSign = 0x24; +const LeftParenthesis = 0x28; +const RightParenthesis = 0x29; +const Asterisk = 0x2a; +const PlusSign = 0x2b; +const Comma = 0x2c; +const HyphenMinus = 0x2d; +const FullStop = 0x2e; +const Solidus = 0x2f; +const DigitZero = 0x30; +const DigitOne = 0x31; +const DigitSeven = 0x37; +const DigitNine = 0x39; +const Colon = 0x3a; +const LessThanSign = 0x3c; +const EqualsSign = 0x3d; +const GreaterThanSign = 0x3e; +const QuestionMark = 0x3f; +const LatinCapitalLetterA = 0x41; +const LatinCapitalLetterB = 0x42; +const LatinCapitalLetterD = 0x44; +const LatinCapitalLetterF = 0x46; +const LatinCapitalLetterP = 0x50; +const LatinCapitalLetterS = 0x53; +const LatinCapitalLetterW = 0x57; +const LatinCapitalLetterZ = 0x5a; +const LowLine = 0x5f; +const LatinSmallLetterA = 0x61; +const LatinSmallLetterB = 0x62; +const LatinSmallLetterC = 0x63; +const LatinSmallLetterD = 0x64; +const LatinSmallLetterF = 0x66; +const LatinSmallLetterG = 0x67; +const LatinSmallLetterI = 0x69; +const LatinSmallLetterK = 0x6b; +const LatinSmallLetterM = 0x6d; +const LatinSmallLetterN = 0x6e; +const LatinSmallLetterP = 0x70; +const LatinSmallLetterR = 0x72; +const LatinSmallLetterS = 0x73; +const LatinSmallLetterT = 0x74; +const LatinSmallLetterU = 0x75; +const LatinSmallLetterV = 0x76; +const LatinSmallLetterW = 0x77; +const LatinSmallLetterX = 0x78; +const LatinSmallLetterY = 0x79; +const LatinSmallLetterZ = 0x7a; +const LeftSquareBracket = 0x5b; +const ReverseSolidus = 0x5c; +const RightSquareBracket = 0x5d; +const CircumflexAccent = 0x5e; +const LeftCurlyBracket = 0x7b; +const VerticalLine = 0x7c; +const RightCurlyBracket = 0x7d; +const ZeroWidthNonJoiner = 0x200c; +const ZeroWidthJoiner = 0x200d; +const LineSeparator = 0x2028; +const ParagraphSeparator = 0x2029; +const MinCodePoint = 0x00; +const MaxCodePoint = 0x10ffff; +function isLatinLetter(code) { + return ((code >= LatinCapitalLetterA && code <= LatinCapitalLetterZ) || + (code >= LatinSmallLetterA && code <= LatinSmallLetterZ)); +} +function isDecimalDigit(code) { + return code >= DigitZero && code <= DigitNine; +} +function isOctalDigit(code) { + return code >= DigitZero && code <= DigitSeven; +} +function isHexDigit(code) { + return ((code >= DigitZero && code <= DigitNine) || + (code >= LatinCapitalLetterA && code <= LatinCapitalLetterF) || + (code >= LatinSmallLetterA && code <= LatinSmallLetterF)); +} +function isLineTerminator(code) { + return (code === LineFeed || + code === CarriageReturn || + code === LineSeparator || + code === ParagraphSeparator); +} +function isValidUnicode(code) { + return code >= MinCodePoint && code <= MaxCodePoint; +} +function digitToInt(code) { + if (code >= LatinSmallLetterA && code <= LatinSmallLetterF) { + return code - LatinSmallLetterA + 10; + } + if (code >= LatinCapitalLetterA && code <= LatinCapitalLetterF) { + return code - LatinCapitalLetterA + 10; + } + return code - DigitZero; +} +function isLeadSurrogate(code) { + return code >= 0xd800 && code <= 0xdbff; +} +function isTrailSurrogate(code) { + return code >= 0xdc00 && code <= 0xdfff; +} +function combineSurrogatePair(lead, trail) { + return (lead - 0xd800) * 0x400 + (trail - 0xdc00) + 0x10000; +} + +const legacyImpl = { + at(s, end, i) { + return i < end ? s.charCodeAt(i) : -1; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + width(c) { + return 1; + }, +}; +const unicodeImpl = { + at(s, end, i) { + return i < end ? s.codePointAt(i) : -1; + }, + width(c) { + return c > 0xffff ? 2 : 1; + }, +}; +class Reader { + constructor() { + this._impl = legacyImpl; + this._s = ""; + this._i = 0; + this._end = 0; + this._cp1 = -1; + this._w1 = 1; + this._cp2 = -1; + this._w2 = 1; + this._cp3 = -1; + this._w3 = 1; + this._cp4 = -1; + } + get source() { + return this._s; + } + get index() { + return this._i; + } + get currentCodePoint() { + return this._cp1; + } + get nextCodePoint() { + return this._cp2; + } + get nextCodePoint2() { + return this._cp3; + } + get nextCodePoint3() { + return this._cp4; + } + reset(source, start, end, uFlag) { + this._impl = uFlag ? unicodeImpl : legacyImpl; + this._s = source; + this._end = end; + this.rewind(start); + } + rewind(index) { + const impl = this._impl; + this._i = index; + this._cp1 = impl.at(this._s, this._end, index); + this._w1 = impl.width(this._cp1); + this._cp2 = impl.at(this._s, this._end, index + this._w1); + this._w2 = impl.width(this._cp2); + this._cp3 = impl.at(this._s, this._end, index + this._w1 + this._w2); + this._w3 = impl.width(this._cp3); + this._cp4 = impl.at(this._s, this._end, index + this._w1 + this._w2 + this._w3); + } + advance() { + if (this._cp1 !== -1) { + const impl = this._impl; + this._i += this._w1; + this._cp1 = this._cp2; + this._w1 = this._w2; + this._cp2 = this._cp3; + this._w2 = impl.width(this._cp2); + this._cp3 = this._cp4; + this._w3 = impl.width(this._cp3); + this._cp4 = impl.at(this._s, this._end, this._i + this._w1 + this._w2 + this._w3); + } + } + eat(cp) { + if (this._cp1 === cp) { + this.advance(); + return true; + } + return false; + } + eat2(cp1, cp2) { + if (this._cp1 === cp1 && this._cp2 === cp2) { + this.advance(); + this.advance(); + return true; + } + return false; + } + eat3(cp1, cp2, cp3) { + if (this._cp1 === cp1 && this._cp2 === cp2 && this._cp3 === cp3) { + this.advance(); + this.advance(); + this.advance(); + return true; + } + return false; + } +} + +class RegExpSyntaxError extends SyntaxError { + constructor(source, uFlag, index, message) { + if (source) { + if (!source.startsWith("/")) { + source = `/${source}/${uFlag ? "u" : ""}`; + } + source = `: ${source}`; + } + super(`Invalid regular expression${source}: ${message}`); + this.index = index; + } +} + +function isSyntaxCharacter(cp) { + return (cp === CircumflexAccent || + cp === DollarSign || + cp === ReverseSolidus || + cp === FullStop || + cp === Asterisk || + cp === PlusSign || + cp === QuestionMark || + cp === LeftParenthesis || + cp === RightParenthesis || + cp === LeftSquareBracket || + cp === RightSquareBracket || + cp === LeftCurlyBracket || + cp === RightCurlyBracket || + cp === VerticalLine); +} +function isRegExpIdentifierStart(cp) { + return isIdStart(cp) || cp === DollarSign || cp === LowLine; +} +function isRegExpIdentifierPart(cp) { + return (isIdContinue(cp) || + cp === DollarSign || + cp === LowLine || + cp === ZeroWidthNonJoiner || + cp === ZeroWidthJoiner); +} +function isUnicodePropertyNameCharacter(cp) { + return isLatinLetter(cp) || cp === LowLine; +} +function isUnicodePropertyValueCharacter(cp) { + return isUnicodePropertyNameCharacter(cp) || isDecimalDigit(cp); +} +class RegExpValidator { + constructor(options) { + this._reader = new Reader(); + this._uFlag = false; + this._nFlag = false; + this._lastIntValue = 0; + this._lastMinValue = 0; + this._lastMaxValue = 0; + this._lastStrValue = ""; + this._lastKeyValue = ""; + this._lastValValue = ""; + this._lastAssertionIsQuantifiable = false; + this._numCapturingParens = 0; + this._groupNames = new Set(); + this._backreferenceNames = new Set(); + this._options = options || {}; + } + validateLiteral(source, start = 0, end = source.length) { + this._uFlag = this._nFlag = false; + this.reset(source, start, end); + this.onLiteralEnter(start); + if (this.eat(Solidus) && this.eatRegExpBody() && this.eat(Solidus)) { + const flagStart = this.index; + const uFlag = source.includes("u", flagStart); + this.validateFlags(source, flagStart, end); + this.validatePattern(source, start + 1, flagStart - 1, uFlag); + } + else if (start >= end) { + this.raise("Empty"); + } + else { + const c = String.fromCodePoint(this.currentCodePoint); + this.raise(`Unexpected character '${c}'`); + } + this.onLiteralLeave(start, end); + } + validateFlags(source, start = 0, end = source.length) { + const existingFlags = new Set(); + let global = false; + let ignoreCase = false; + let multiline = false; + let sticky = false; + let unicode = false; + let dotAll = false; + let hasIndices = false; + for (let i = start; i < end; ++i) { + const flag = source.charCodeAt(i); + if (existingFlags.has(flag)) { + this.raise(`Duplicated flag '${source[i]}'`); + } + existingFlags.add(flag); + if (flag === LatinSmallLetterG) { + global = true; + } + else if (flag === LatinSmallLetterI) { + ignoreCase = true; + } + else if (flag === LatinSmallLetterM) { + multiline = true; + } + else if (flag === LatinSmallLetterU && this.ecmaVersion >= 2015) { + unicode = true; + } + else if (flag === LatinSmallLetterY && this.ecmaVersion >= 2015) { + sticky = true; + } + else if (flag === LatinSmallLetterS && this.ecmaVersion >= 2018) { + dotAll = true; + } + else if (flag === LatinSmallLetterD && this.ecmaVersion >= 2022) { + hasIndices = true; + } + else { + this.raise(`Invalid flag '${source[i]}'`); + } + } + this.onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices); + } + validatePattern(source, start = 0, end = source.length, uFlag = false) { + this._uFlag = uFlag && this.ecmaVersion >= 2015; + this._nFlag = uFlag && this.ecmaVersion >= 2018; + this.reset(source, start, end); + this.consumePattern(); + if (!this._nFlag && + this.ecmaVersion >= 2018 && + this._groupNames.size > 0) { + this._nFlag = true; + this.rewind(start); + this.consumePattern(); + } + } + get strict() { + return Boolean(this._options.strict || this._uFlag); + } + get ecmaVersion() { + return this._options.ecmaVersion || 2022; + } + onLiteralEnter(start) { + if (this._options.onLiteralEnter) { + this._options.onLiteralEnter(start); + } + } + onLiteralLeave(start, end) { + if (this._options.onLiteralLeave) { + this._options.onLiteralLeave(start, end); + } + } + onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices) { + if (this._options.onFlags) { + this._options.onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices); + } + } + onPatternEnter(start) { + if (this._options.onPatternEnter) { + this._options.onPatternEnter(start); + } + } + onPatternLeave(start, end) { + if (this._options.onPatternLeave) { + this._options.onPatternLeave(start, end); + } + } + onDisjunctionEnter(start) { + if (this._options.onDisjunctionEnter) { + this._options.onDisjunctionEnter(start); + } + } + onDisjunctionLeave(start, end) { + if (this._options.onDisjunctionLeave) { + this._options.onDisjunctionLeave(start, end); + } + } + onAlternativeEnter(start, index) { + if (this._options.onAlternativeEnter) { + this._options.onAlternativeEnter(start, index); + } + } + onAlternativeLeave(start, end, index) { + if (this._options.onAlternativeLeave) { + this._options.onAlternativeLeave(start, end, index); + } + } + onGroupEnter(start) { + if (this._options.onGroupEnter) { + this._options.onGroupEnter(start); + } + } + onGroupLeave(start, end) { + if (this._options.onGroupLeave) { + this._options.onGroupLeave(start, end); + } + } + onCapturingGroupEnter(start, name) { + if (this._options.onCapturingGroupEnter) { + this._options.onCapturingGroupEnter(start, name); + } + } + onCapturingGroupLeave(start, end, name) { + if (this._options.onCapturingGroupLeave) { + this._options.onCapturingGroupLeave(start, end, name); + } + } + onQuantifier(start, end, min, max, greedy) { + if (this._options.onQuantifier) { + this._options.onQuantifier(start, end, min, max, greedy); + } + } + onLookaroundAssertionEnter(start, kind, negate) { + if (this._options.onLookaroundAssertionEnter) { + this._options.onLookaroundAssertionEnter(start, kind, negate); + } + } + onLookaroundAssertionLeave(start, end, kind, negate) { + if (this._options.onLookaroundAssertionLeave) { + this._options.onLookaroundAssertionLeave(start, end, kind, negate); + } + } + onEdgeAssertion(start, end, kind) { + if (this._options.onEdgeAssertion) { + this._options.onEdgeAssertion(start, end, kind); + } + } + onWordBoundaryAssertion(start, end, kind, negate) { + if (this._options.onWordBoundaryAssertion) { + this._options.onWordBoundaryAssertion(start, end, kind, negate); + } + } + onAnyCharacterSet(start, end, kind) { + if (this._options.onAnyCharacterSet) { + this._options.onAnyCharacterSet(start, end, kind); + } + } + onEscapeCharacterSet(start, end, kind, negate) { + if (this._options.onEscapeCharacterSet) { + this._options.onEscapeCharacterSet(start, end, kind, negate); + } + } + onUnicodePropertyCharacterSet(start, end, kind, key, value, negate) { + if (this._options.onUnicodePropertyCharacterSet) { + this._options.onUnicodePropertyCharacterSet(start, end, kind, key, value, negate); + } + } + onCharacter(start, end, value) { + if (this._options.onCharacter) { + this._options.onCharacter(start, end, value); + } + } + onBackreference(start, end, ref) { + if (this._options.onBackreference) { + this._options.onBackreference(start, end, ref); + } + } + onCharacterClassEnter(start, negate) { + if (this._options.onCharacterClassEnter) { + this._options.onCharacterClassEnter(start, negate); + } + } + onCharacterClassLeave(start, end, negate) { + if (this._options.onCharacterClassLeave) { + this._options.onCharacterClassLeave(start, end, negate); + } + } + onCharacterClassRange(start, end, min, max) { + if (this._options.onCharacterClassRange) { + this._options.onCharacterClassRange(start, end, min, max); + } + } + get source() { + return this._reader.source; + } + get index() { + return this._reader.index; + } + get currentCodePoint() { + return this._reader.currentCodePoint; + } + get nextCodePoint() { + return this._reader.nextCodePoint; + } + get nextCodePoint2() { + return this._reader.nextCodePoint2; + } + get nextCodePoint3() { + return this._reader.nextCodePoint3; + } + reset(source, start, end) { + this._reader.reset(source, start, end, this._uFlag); + } + rewind(index) { + this._reader.rewind(index); + } + advance() { + this._reader.advance(); + } + eat(cp) { + return this._reader.eat(cp); + } + eat2(cp1, cp2) { + return this._reader.eat2(cp1, cp2); + } + eat3(cp1, cp2, cp3) { + return this._reader.eat3(cp1, cp2, cp3); + } + raise(message) { + throw new RegExpSyntaxError(this.source, this._uFlag, this.index, message); + } + eatRegExpBody() { + const start = this.index; + let inClass = false; + let escaped = false; + for (;;) { + const cp = this.currentCodePoint; + if (cp === -1 || isLineTerminator(cp)) { + const kind = inClass ? "character class" : "regular expression"; + this.raise(`Unterminated ${kind}`); + } + if (escaped) { + escaped = false; + } + else if (cp === ReverseSolidus) { + escaped = true; + } + else if (cp === LeftSquareBracket) { + inClass = true; + } + else if (cp === RightSquareBracket) { + inClass = false; + } + else if ((cp === Solidus && !inClass) || + (cp === Asterisk && this.index === start)) { + break; + } + this.advance(); + } + return this.index !== start; + } + consumePattern() { + const start = this.index; + this._numCapturingParens = this.countCapturingParens(); + this._groupNames.clear(); + this._backreferenceNames.clear(); + this.onPatternEnter(start); + this.consumeDisjunction(); + const cp = this.currentCodePoint; + if (this.currentCodePoint !== -1) { + if (cp === RightParenthesis) { + this.raise("Unmatched ')'"); + } + if (cp === ReverseSolidus) { + this.raise("\\ at end of pattern"); + } + if (cp === RightSquareBracket || cp === RightCurlyBracket) { + this.raise("Lone quantifier brackets"); + } + const c = String.fromCodePoint(cp); + this.raise(`Unexpected character '${c}'`); + } + for (const name of this._backreferenceNames) { + if (!this._groupNames.has(name)) { + this.raise("Invalid named capture referenced"); + } + } + this.onPatternLeave(start, this.index); + } + countCapturingParens() { + const start = this.index; + let inClass = false; + let escaped = false; + let count = 0; + let cp = 0; + while ((cp = this.currentCodePoint) !== -1) { + if (escaped) { + escaped = false; + } + else if (cp === ReverseSolidus) { + escaped = true; + } + else if (cp === LeftSquareBracket) { + inClass = true; + } + else if (cp === RightSquareBracket) { + inClass = false; + } + else if (cp === LeftParenthesis && + !inClass && + (this.nextCodePoint !== QuestionMark || + (this.nextCodePoint2 === LessThanSign && + this.nextCodePoint3 !== EqualsSign && + this.nextCodePoint3 !== ExclamationMark))) { + count += 1; + } + this.advance(); + } + this.rewind(start); + return count; + } + consumeDisjunction() { + const start = this.index; + let i = 0; + this.onDisjunctionEnter(start); + do { + this.consumeAlternative(i++); + } while (this.eat(VerticalLine)); + if (this.consumeQuantifier(true)) { + this.raise("Nothing to repeat"); + } + if (this.eat(LeftCurlyBracket)) { + this.raise("Lone quantifier brackets"); + } + this.onDisjunctionLeave(start, this.index); + } + consumeAlternative(i) { + const start = this.index; + this.onAlternativeEnter(start, i); + // eslint-disable-next-line no-empty + while (this.currentCodePoint !== -1 && this.consumeTerm()) { + } + this.onAlternativeLeave(start, this.index, i); + } + consumeTerm() { + if (this._uFlag || this.strict) { + return (this.consumeAssertion() || + (this.consumeAtom() && this.consumeOptionalQuantifier())); + } + return ((this.consumeAssertion() && + (!this._lastAssertionIsQuantifiable || + this.consumeOptionalQuantifier())) || + (this.consumeExtendedAtom() && this.consumeOptionalQuantifier())); + } + consumeOptionalQuantifier() { + this.consumeQuantifier(); + return true; + } + consumeAssertion() { + const start = this.index; + this._lastAssertionIsQuantifiable = false; + if (this.eat(CircumflexAccent)) { + this.onEdgeAssertion(start, this.index, "start"); + return true; + } + if (this.eat(DollarSign)) { + this.onEdgeAssertion(start, this.index, "end"); + return true; + } + if (this.eat2(ReverseSolidus, LatinCapitalLetterB)) { + this.onWordBoundaryAssertion(start, this.index, "word", true); + return true; + } + if (this.eat2(ReverseSolidus, LatinSmallLetterB)) { + this.onWordBoundaryAssertion(start, this.index, "word", false); + return true; + } + if (this.eat2(LeftParenthesis, QuestionMark)) { + const lookbehind = this.ecmaVersion >= 2018 && this.eat(LessThanSign); + let negate = false; + if (this.eat(EqualsSign) || (negate = this.eat(ExclamationMark))) { + const kind = lookbehind ? "lookbehind" : "lookahead"; + this.onLookaroundAssertionEnter(start, kind, negate); + this.consumeDisjunction(); + if (!this.eat(RightParenthesis)) { + this.raise("Unterminated group"); + } + this._lastAssertionIsQuantifiable = !lookbehind && !this.strict; + this.onLookaroundAssertionLeave(start, this.index, kind, negate); + return true; + } + this.rewind(start); + } + return false; + } + consumeQuantifier(noConsume = false) { + const start = this.index; + let min = 0; + let max = 0; + let greedy = false; + if (this.eat(Asterisk)) { + min = 0; + max = Number.POSITIVE_INFINITY; + } + else if (this.eat(PlusSign)) { + min = 1; + max = Number.POSITIVE_INFINITY; + } + else if (this.eat(QuestionMark)) { + min = 0; + max = 1; + } + else if (this.eatBracedQuantifier(noConsume)) { + min = this._lastMinValue; + max = this._lastMaxValue; + } + else { + return false; + } + greedy = !this.eat(QuestionMark); + if (!noConsume) { + this.onQuantifier(start, this.index, min, max, greedy); + } + return true; + } + eatBracedQuantifier(noError) { + const start = this.index; + if (this.eat(LeftCurlyBracket)) { + this._lastMinValue = 0; + this._lastMaxValue = Number.POSITIVE_INFINITY; + if (this.eatDecimalDigits()) { + this._lastMinValue = this._lastMaxValue = this._lastIntValue; + if (this.eat(Comma)) { + this._lastMaxValue = this.eatDecimalDigits() + ? this._lastIntValue + : Number.POSITIVE_INFINITY; + } + if (this.eat(RightCurlyBracket)) { + if (!noError && this._lastMaxValue < this._lastMinValue) { + this.raise("numbers out of order in {} quantifier"); + } + return true; + } + } + if (!noError && (this._uFlag || this.strict)) { + this.raise("Incomplete quantifier"); + } + this.rewind(start); + } + return false; + } + consumeAtom() { + return (this.consumePatternCharacter() || + this.consumeDot() || + this.consumeReverseSolidusAtomEscape() || + this.consumeCharacterClass() || + this.consumeUncapturingGroup() || + this.consumeCapturingGroup()); + } + consumeDot() { + if (this.eat(FullStop)) { + this.onAnyCharacterSet(this.index - 1, this.index, "any"); + return true; + } + return false; + } + consumeReverseSolidusAtomEscape() { + const start = this.index; + if (this.eat(ReverseSolidus)) { + if (this.consumeAtomEscape()) { + return true; + } + this.rewind(start); + } + return false; + } + consumeUncapturingGroup() { + const start = this.index; + if (this.eat3(LeftParenthesis, QuestionMark, Colon)) { + this.onGroupEnter(start); + this.consumeDisjunction(); + if (!this.eat(RightParenthesis)) { + this.raise("Unterminated group"); + } + this.onGroupLeave(start, this.index); + return true; + } + return false; + } + consumeCapturingGroup() { + const start = this.index; + if (this.eat(LeftParenthesis)) { + let name = null; + if (this.ecmaVersion >= 2018) { + if (this.consumeGroupSpecifier()) { + name = this._lastStrValue; + } + } + else if (this.currentCodePoint === QuestionMark) { + this.raise("Invalid group"); + } + this.onCapturingGroupEnter(start, name); + this.consumeDisjunction(); + if (!this.eat(RightParenthesis)) { + this.raise("Unterminated group"); + } + this.onCapturingGroupLeave(start, this.index, name); + return true; + } + return false; + } + consumeExtendedAtom() { + return (this.consumeDot() || + this.consumeReverseSolidusAtomEscape() || + this.consumeReverseSolidusFollowedByC() || + this.consumeCharacterClass() || + this.consumeUncapturingGroup() || + this.consumeCapturingGroup() || + this.consumeInvalidBracedQuantifier() || + this.consumeExtendedPatternCharacter()); + } + consumeReverseSolidusFollowedByC() { + const start = this.index; + if (this.currentCodePoint === ReverseSolidus && + this.nextCodePoint === LatinSmallLetterC) { + this._lastIntValue = this.currentCodePoint; + this.advance(); + this.onCharacter(start, this.index, ReverseSolidus); + return true; + } + return false; + } + consumeInvalidBracedQuantifier() { + if (this.eatBracedQuantifier(true)) { + this.raise("Nothing to repeat"); + } + return false; + } + consumePatternCharacter() { + const start = this.index; + const cp = this.currentCodePoint; + if (cp !== -1 && !isSyntaxCharacter(cp)) { + this.advance(); + this.onCharacter(start, this.index, cp); + return true; + } + return false; + } + consumeExtendedPatternCharacter() { + const start = this.index; + const cp = this.currentCodePoint; + if (cp !== -1 && + cp !== CircumflexAccent && + cp !== DollarSign && + cp !== ReverseSolidus && + cp !== FullStop && + cp !== Asterisk && + cp !== PlusSign && + cp !== QuestionMark && + cp !== LeftParenthesis && + cp !== RightParenthesis && + cp !== LeftSquareBracket && + cp !== VerticalLine) { + this.advance(); + this.onCharacter(start, this.index, cp); + return true; + } + return false; + } + consumeGroupSpecifier() { + if (this.eat(QuestionMark)) { + if (this.eatGroupName()) { + if (!this._groupNames.has(this._lastStrValue)) { + this._groupNames.add(this._lastStrValue); + return true; + } + this.raise("Duplicate capture group name"); + } + this.raise("Invalid group"); + } + return false; + } + consumeAtomEscape() { + if (this.consumeBackreference() || + this.consumeCharacterClassEscape() || + this.consumeCharacterEscape() || + (this._nFlag && this.consumeKGroupName())) { + return true; + } + if (this.strict || this._uFlag) { + this.raise("Invalid escape"); + } + return false; + } + consumeBackreference() { + const start = this.index; + if (this.eatDecimalEscape()) { + const n = this._lastIntValue; + if (n <= this._numCapturingParens) { + this.onBackreference(start - 1, this.index, n); + return true; + } + if (this.strict || this._uFlag) { + this.raise("Invalid escape"); + } + this.rewind(start); + } + return false; + } + consumeCharacterClassEscape() { + const start = this.index; + if (this.eat(LatinSmallLetterD)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "digit", false); + return true; + } + if (this.eat(LatinCapitalLetterD)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "digit", true); + return true; + } + if (this.eat(LatinSmallLetterS)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "space", false); + return true; + } + if (this.eat(LatinCapitalLetterS)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "space", true); + return true; + } + if (this.eat(LatinSmallLetterW)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "word", false); + return true; + } + if (this.eat(LatinCapitalLetterW)) { + this._lastIntValue = -1; + this.onEscapeCharacterSet(start - 1, this.index, "word", true); + return true; + } + let negate = false; + if (this._uFlag && + this.ecmaVersion >= 2018 && + (this.eat(LatinSmallLetterP) || + (negate = this.eat(LatinCapitalLetterP)))) { + this._lastIntValue = -1; + if (this.eat(LeftCurlyBracket) && + this.eatUnicodePropertyValueExpression() && + this.eat(RightCurlyBracket)) { + this.onUnicodePropertyCharacterSet(start - 1, this.index, "property", this._lastKeyValue, this._lastValValue || null, negate); + return true; + } + this.raise("Invalid property name"); + } + return false; + } + consumeCharacterEscape() { + const start = this.index; + if (this.eatControlEscape() || + this.eatCControlLetter() || + this.eatZero() || + this.eatHexEscapeSequence() || + this.eatRegExpUnicodeEscapeSequence() || + (!this.strict && + !this._uFlag && + this.eatLegacyOctalEscapeSequence()) || + this.eatIdentityEscape()) { + this.onCharacter(start - 1, this.index, this._lastIntValue); + return true; + } + return false; + } + consumeKGroupName() { + const start = this.index; + if (this.eat(LatinSmallLetterK)) { + if (this.eatGroupName()) { + const groupName = this._lastStrValue; + this._backreferenceNames.add(groupName); + this.onBackreference(start - 1, this.index, groupName); + return true; + } + this.raise("Invalid named reference"); + } + return false; + } + consumeCharacterClass() { + const start = this.index; + if (this.eat(LeftSquareBracket)) { + const negate = this.eat(CircumflexAccent); + this.onCharacterClassEnter(start, negate); + this.consumeClassRanges(); + if (!this.eat(RightSquareBracket)) { + this.raise("Unterminated character class"); + } + this.onCharacterClassLeave(start, this.index, negate); + return true; + } + return false; + } + consumeClassRanges() { + const strict = this.strict || this._uFlag; + for (;;) { + const rangeStart = this.index; + if (!this.consumeClassAtom()) { + break; + } + const min = this._lastIntValue; + if (!this.eat(HyphenMinus)) { + continue; + } + this.onCharacter(this.index - 1, this.index, HyphenMinus); + if (!this.consumeClassAtom()) { + break; + } + const max = this._lastIntValue; + if (min === -1 || max === -1) { + if (strict) { + this.raise("Invalid character class"); + } + continue; + } + if (min > max) { + this.raise("Range out of order in character class"); + } + this.onCharacterClassRange(rangeStart, this.index, min, max); + } + } + consumeClassAtom() { + const start = this.index; + const cp = this.currentCodePoint; + if (cp !== -1 && cp !== ReverseSolidus && cp !== RightSquareBracket) { + this.advance(); + this._lastIntValue = cp; + this.onCharacter(start, this.index, this._lastIntValue); + return true; + } + if (this.eat(ReverseSolidus)) { + if (this.consumeClassEscape()) { + return true; + } + if (!this.strict && this.currentCodePoint === LatinSmallLetterC) { + this._lastIntValue = ReverseSolidus; + this.onCharacter(start, this.index, this._lastIntValue); + return true; + } + if (this.strict || this._uFlag) { + this.raise("Invalid escape"); + } + this.rewind(start); + } + return false; + } + consumeClassEscape() { + const start = this.index; + if (this.eat(LatinSmallLetterB)) { + this._lastIntValue = Backspace; + this.onCharacter(start - 1, this.index, this._lastIntValue); + return true; + } + if (this._uFlag && this.eat(HyphenMinus)) { + this._lastIntValue = HyphenMinus; + this.onCharacter(start - 1, this.index, this._lastIntValue); + return true; + } + let cp = 0; + if (!this.strict && + !this._uFlag && + this.currentCodePoint === LatinSmallLetterC && + (isDecimalDigit((cp = this.nextCodePoint)) || cp === LowLine)) { + this.advance(); + this.advance(); + this._lastIntValue = cp % 0x20; + this.onCharacter(start - 1, this.index, this._lastIntValue); + return true; + } + return (this.consumeCharacterClassEscape() || this.consumeCharacterEscape()); + } + eatGroupName() { + if (this.eat(LessThanSign)) { + if (this.eatRegExpIdentifierName() && this.eat(GreaterThanSign)) { + return true; + } + this.raise("Invalid capture group name"); + } + return false; + } + eatRegExpIdentifierName() { + if (this.eatRegExpIdentifierStart()) { + this._lastStrValue = String.fromCodePoint(this._lastIntValue); + while (this.eatRegExpIdentifierPart()) { + this._lastStrValue += String.fromCodePoint(this._lastIntValue); + } + return true; + } + return false; + } + eatRegExpIdentifierStart() { + const start = this.index; + const forceUFlag = !this._uFlag && this.ecmaVersion >= 2020; + let cp = this.currentCodePoint; + this.advance(); + if (cp === ReverseSolidus && + this.eatRegExpUnicodeEscapeSequence(forceUFlag)) { + cp = this._lastIntValue; + } + else if (forceUFlag && + isLeadSurrogate(cp) && + isTrailSurrogate(this.currentCodePoint)) { + cp = combineSurrogatePair(cp, this.currentCodePoint); + this.advance(); + } + if (isRegExpIdentifierStart(cp)) { + this._lastIntValue = cp; + return true; + } + if (this.index !== start) { + this.rewind(start); + } + return false; + } + eatRegExpIdentifierPart() { + const start = this.index; + const forceUFlag = !this._uFlag && this.ecmaVersion >= 2020; + let cp = this.currentCodePoint; + this.advance(); + if (cp === ReverseSolidus && + this.eatRegExpUnicodeEscapeSequence(forceUFlag)) { + cp = this._lastIntValue; + } + else if (forceUFlag && + isLeadSurrogate(cp) && + isTrailSurrogate(this.currentCodePoint)) { + cp = combineSurrogatePair(cp, this.currentCodePoint); + this.advance(); + } + if (isRegExpIdentifierPart(cp)) { + this._lastIntValue = cp; + return true; + } + if (this.index !== start) { + this.rewind(start); + } + return false; + } + eatCControlLetter() { + const start = this.index; + if (this.eat(LatinSmallLetterC)) { + if (this.eatControlLetter()) { + return true; + } + this.rewind(start); + } + return false; + } + eatZero() { + if (this.currentCodePoint === DigitZero && + !isDecimalDigit(this.nextCodePoint)) { + this._lastIntValue = 0; + this.advance(); + return true; + } + return false; + } + eatControlEscape() { + if (this.eat(LatinSmallLetterF)) { + this._lastIntValue = FormFeed; + return true; + } + if (this.eat(LatinSmallLetterN)) { + this._lastIntValue = LineFeed; + return true; + } + if (this.eat(LatinSmallLetterR)) { + this._lastIntValue = CarriageReturn; + return true; + } + if (this.eat(LatinSmallLetterT)) { + this._lastIntValue = CharacterTabulation; + return true; + } + if (this.eat(LatinSmallLetterV)) { + this._lastIntValue = LineTabulation; + return true; + } + return false; + } + eatControlLetter() { + const cp = this.currentCodePoint; + if (isLatinLetter(cp)) { + this.advance(); + this._lastIntValue = cp % 0x20; + return true; + } + return false; + } + eatRegExpUnicodeEscapeSequence(forceUFlag = false) { + const start = this.index; + const uFlag = forceUFlag || this._uFlag; + if (this.eat(LatinSmallLetterU)) { + if ((uFlag && this.eatRegExpUnicodeSurrogatePairEscape()) || + this.eatFixedHexDigits(4) || + (uFlag && this.eatRegExpUnicodeCodePointEscape())) { + return true; + } + if (this.strict || uFlag) { + this.raise("Invalid unicode escape"); + } + this.rewind(start); + } + return false; + } + eatRegExpUnicodeSurrogatePairEscape() { + const start = this.index; + if (this.eatFixedHexDigits(4)) { + const lead = this._lastIntValue; + if (isLeadSurrogate(lead) && + this.eat(ReverseSolidus) && + this.eat(LatinSmallLetterU) && + this.eatFixedHexDigits(4)) { + const trail = this._lastIntValue; + if (isTrailSurrogate(trail)) { + this._lastIntValue = combineSurrogatePair(lead, trail); + return true; + } + } + this.rewind(start); + } + return false; + } + eatRegExpUnicodeCodePointEscape() { + const start = this.index; + if (this.eat(LeftCurlyBracket) && + this.eatHexDigits() && + this.eat(RightCurlyBracket) && + isValidUnicode(this._lastIntValue)) { + return true; + } + this.rewind(start); + return false; + } + eatIdentityEscape() { + const cp = this.currentCodePoint; + if (this.isValidIdentityEscape(cp)) { + this._lastIntValue = cp; + this.advance(); + return true; + } + return false; + } + isValidIdentityEscape(cp) { + if (cp === -1) { + return false; + } + if (this._uFlag) { + return isSyntaxCharacter(cp) || cp === Solidus; + } + if (this.strict) { + return !isIdContinue(cp); + } + if (this._nFlag) { + return !(cp === LatinSmallLetterC || cp === LatinSmallLetterK); + } + return cp !== LatinSmallLetterC; + } + eatDecimalEscape() { + this._lastIntValue = 0; + let cp = this.currentCodePoint; + if (cp >= DigitOne && cp <= DigitNine) { + do { + this._lastIntValue = 10 * this._lastIntValue + (cp - DigitZero); + this.advance(); + } while ((cp = this.currentCodePoint) >= DigitZero && + cp <= DigitNine); + return true; + } + return false; + } + eatUnicodePropertyValueExpression() { + const start = this.index; + if (this.eatUnicodePropertyName() && this.eat(EqualsSign)) { + this._lastKeyValue = this._lastStrValue; + if (this.eatUnicodePropertyValue()) { + this._lastValValue = this._lastStrValue; + if (isValidUnicodeProperty(this.ecmaVersion, this._lastKeyValue, this._lastValValue)) { + return true; + } + this.raise("Invalid property name"); + } + } + this.rewind(start); + if (this.eatLoneUnicodePropertyNameOrValue()) { + const nameOrValue = this._lastStrValue; + if (isValidUnicodeProperty(this.ecmaVersion, "General_Category", nameOrValue)) { + this._lastKeyValue = "General_Category"; + this._lastValValue = nameOrValue; + return true; + } + if (isValidLoneUnicodeProperty(this.ecmaVersion, nameOrValue)) { + this._lastKeyValue = nameOrValue; + this._lastValValue = ""; + return true; + } + this.raise("Invalid property name"); + } + return false; + } + eatUnicodePropertyName() { + this._lastStrValue = ""; + while (isUnicodePropertyNameCharacter(this.currentCodePoint)) { + this._lastStrValue += String.fromCodePoint(this.currentCodePoint); + this.advance(); + } + return this._lastStrValue !== ""; + } + eatUnicodePropertyValue() { + this._lastStrValue = ""; + while (isUnicodePropertyValueCharacter(this.currentCodePoint)) { + this._lastStrValue += String.fromCodePoint(this.currentCodePoint); + this.advance(); + } + return this._lastStrValue !== ""; + } + eatLoneUnicodePropertyNameOrValue() { + return this.eatUnicodePropertyValue(); + } + eatHexEscapeSequence() { + const start = this.index; + if (this.eat(LatinSmallLetterX)) { + if (this.eatFixedHexDigits(2)) { + return true; + } + if (this._uFlag || this.strict) { + this.raise("Invalid escape"); + } + this.rewind(start); + } + return false; + } + eatDecimalDigits() { + const start = this.index; + this._lastIntValue = 0; + while (isDecimalDigit(this.currentCodePoint)) { + this._lastIntValue = + 10 * this._lastIntValue + digitToInt(this.currentCodePoint); + this.advance(); + } + return this.index !== start; + } + eatHexDigits() { + const start = this.index; + this._lastIntValue = 0; + while (isHexDigit(this.currentCodePoint)) { + this._lastIntValue = + 16 * this._lastIntValue + digitToInt(this.currentCodePoint); + this.advance(); + } + return this.index !== start; + } + eatLegacyOctalEscapeSequence() { + if (this.eatOctalDigit()) { + const n1 = this._lastIntValue; + if (this.eatOctalDigit()) { + const n2 = this._lastIntValue; + if (n1 <= 3 && this.eatOctalDigit()) { + this._lastIntValue = n1 * 64 + n2 * 8 + this._lastIntValue; + } + else { + this._lastIntValue = n1 * 8 + n2; + } + } + else { + this._lastIntValue = n1; + } + return true; + } + return false; + } + eatOctalDigit() { + const cp = this.currentCodePoint; + if (isOctalDigit(cp)) { + this.advance(); + this._lastIntValue = cp - DigitZero; + return true; + } + this._lastIntValue = 0; + return false; + } + eatFixedHexDigits(length) { + const start = this.index; + this._lastIntValue = 0; + for (let i = 0; i < length; ++i) { + const cp = this.currentCodePoint; + if (!isHexDigit(cp)) { + this.rewind(start); + return false; + } + this._lastIntValue = 16 * this._lastIntValue + digitToInt(cp); + this.advance(); + } + return true; + } +} + +const DummyPattern = {}; +const DummyFlags = {}; +const DummyCapturingGroup = {}; +class RegExpParserState { + constructor(options) { + this._node = DummyPattern; + this._flags = DummyFlags; + this._backreferences = []; + this._capturingGroups = []; + this.source = ""; + this.strict = Boolean(options && options.strict); + this.ecmaVersion = (options && options.ecmaVersion) || 2022; + } + get pattern() { + if (this._node.type !== "Pattern") { + throw new Error("UnknownError"); + } + return this._node; + } + get flags() { + if (this._flags.type !== "Flags") { + throw new Error("UnknownError"); + } + return this._flags; + } + onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices) { + this._flags = { + type: "Flags", + parent: null, + start, + end, + raw: this.source.slice(start, end), + global, + ignoreCase, + multiline, + unicode, + sticky, + dotAll, + hasIndices, + }; + } + onPatternEnter(start) { + this._node = { + type: "Pattern", + parent: null, + start, + end: start, + raw: "", + alternatives: [], + }; + this._backreferences.length = 0; + this._capturingGroups.length = 0; + } + onPatternLeave(start, end) { + this._node.end = end; + this._node.raw = this.source.slice(start, end); + for (const reference of this._backreferences) { + const ref = reference.ref; + const group = typeof ref === "number" + ? this._capturingGroups[ref - 1] + : this._capturingGroups.find(g => g.name === ref); + reference.resolved = group; + group.references.push(reference); + } + } + onAlternativeEnter(start) { + const parent = this._node; + if (parent.type !== "Assertion" && + parent.type !== "CapturingGroup" && + parent.type !== "Group" && + parent.type !== "Pattern") { + throw new Error("UnknownError"); + } + this._node = { + type: "Alternative", + parent, + start, + end: start, + raw: "", + elements: [], + }; + parent.alternatives.push(this._node); + } + onAlternativeLeave(start, end) { + const node = this._node; + if (node.type !== "Alternative") { + throw new Error("UnknownError"); + } + node.end = end; + node.raw = this.source.slice(start, end); + this._node = node.parent; + } + onGroupEnter(start) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + this._node = { + type: "Group", + parent, + start, + end: start, + raw: "", + alternatives: [], + }; + parent.elements.push(this._node); + } + onGroupLeave(start, end) { + const node = this._node; + if (node.type !== "Group" || node.parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + node.end = end; + node.raw = this.source.slice(start, end); + this._node = node.parent; + } + onCapturingGroupEnter(start, name) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + this._node = { + type: "CapturingGroup", + parent, + start, + end: start, + raw: "", + name, + alternatives: [], + references: [], + }; + parent.elements.push(this._node); + this._capturingGroups.push(this._node); + } + onCapturingGroupLeave(start, end) { + const node = this._node; + if (node.type !== "CapturingGroup" || + node.parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + node.end = end; + node.raw = this.source.slice(start, end); + this._node = node.parent; + } + onQuantifier(start, end, min, max, greedy) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + const element = parent.elements.pop(); + if (element == null || + element.type === "Quantifier" || + (element.type === "Assertion" && element.kind !== "lookahead")) { + throw new Error("UnknownError"); + } + const node = { + type: "Quantifier", + parent, + start: element.start, + end, + raw: this.source.slice(element.start, end), + min, + max, + greedy, + element, + }; + parent.elements.push(node); + element.parent = node; + } + onLookaroundAssertionEnter(start, kind, negate) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + const node = (this._node = { + type: "Assertion", + parent, + start, + end: start, + raw: "", + kind, + negate, + alternatives: [], + }); + parent.elements.push(node); + } + onLookaroundAssertionLeave(start, end) { + const node = this._node; + if (node.type !== "Assertion" || node.parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + node.end = end; + node.raw = this.source.slice(start, end); + this._node = node.parent; + } + onEdgeAssertion(start, end, kind) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "Assertion", + parent, + start, + end, + raw: this.source.slice(start, end), + kind, + }); + } + onWordBoundaryAssertion(start, end, kind, negate) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "Assertion", + parent, + start, + end, + raw: this.source.slice(start, end), + kind, + negate, + }); + } + onAnyCharacterSet(start, end, kind) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "CharacterSet", + parent, + start, + end, + raw: this.source.slice(start, end), + kind, + }); + } + onEscapeCharacterSet(start, end, kind, negate) { + const parent = this._node; + if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "CharacterSet", + parent, + start, + end, + raw: this.source.slice(start, end), + kind, + negate, + }); + } + onUnicodePropertyCharacterSet(start, end, kind, key, value, negate) { + const parent = this._node; + if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "CharacterSet", + parent, + start, + end, + raw: this.source.slice(start, end), + kind, + key, + value, + negate, + }); + } + onCharacter(start, end, value) { + const parent = this._node; + if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { + throw new Error("UnknownError"); + } + parent.elements.push({ + type: "Character", + parent, + start, + end, + raw: this.source.slice(start, end), + value, + }); + } + onBackreference(start, end, ref) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + const node = { + type: "Backreference", + parent, + start, + end, + raw: this.source.slice(start, end), + ref, + resolved: DummyCapturingGroup, + }; + parent.elements.push(node); + this._backreferences.push(node); + } + onCharacterClassEnter(start, negate) { + const parent = this._node; + if (parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + this._node = { + type: "CharacterClass", + parent, + start, + end: start, + raw: "", + negate, + elements: [], + }; + parent.elements.push(this._node); + } + onCharacterClassLeave(start, end) { + const node = this._node; + if (node.type !== "CharacterClass" || + node.parent.type !== "Alternative") { + throw new Error("UnknownError"); + } + node.end = end; + node.raw = this.source.slice(start, end); + this._node = node.parent; + } + onCharacterClassRange(start, end) { + const parent = this._node; + if (parent.type !== "CharacterClass") { + throw new Error("UnknownError"); + } + const elements = parent.elements; + const max = elements.pop(); + const hyphen = elements.pop(); + const min = elements.pop(); + if (!min || + !max || + !hyphen || + min.type !== "Character" || + max.type !== "Character" || + hyphen.type !== "Character" || + hyphen.value !== HyphenMinus) { + throw new Error("UnknownError"); + } + const node = { + type: "CharacterClassRange", + parent, + start, + end, + raw: this.source.slice(start, end), + min, + max, + }; + min.parent = node; + max.parent = node; + elements.push(node); + } +} +class RegExpParser { + constructor(options) { + this._state = new RegExpParserState(options); + this._validator = new RegExpValidator(this._state); + } + parseLiteral(source, start = 0, end = source.length) { + this._state.source = source; + this._validator.validateLiteral(source, start, end); + const pattern = this._state.pattern; + const flags = this._state.flags; + const literal = { + type: "RegExpLiteral", + parent: null, + start, + end, + raw: source, + pattern, + flags, + }; + pattern.parent = literal; + flags.parent = literal; + return literal; + } + parseFlags(source, start = 0, end = source.length) { + this._state.source = source; + this._validator.validateFlags(source, start, end); + return this._state.flags; + } + parsePattern(source, start = 0, end = source.length, uFlag = false) { + this._state.source = source; + this._validator.validatePattern(source, start, end, uFlag); + return this._state.pattern; + } +} + +class RegExpVisitor { + constructor(handlers) { + this._handlers = handlers; + } + visit(node) { + switch (node.type) { + case "Alternative": + this.visitAlternative(node); + break; + case "Assertion": + this.visitAssertion(node); + break; + case "Backreference": + this.visitBackreference(node); + break; + case "CapturingGroup": + this.visitCapturingGroup(node); + break; + case "Character": + this.visitCharacter(node); + break; + case "CharacterClass": + this.visitCharacterClass(node); + break; + case "CharacterClassRange": + this.visitCharacterClassRange(node); + break; + case "CharacterSet": + this.visitCharacterSet(node); + break; + case "Flags": + this.visitFlags(node); + break; + case "Group": + this.visitGroup(node); + break; + case "Pattern": + this.visitPattern(node); + break; + case "Quantifier": + this.visitQuantifier(node); + break; + case "RegExpLiteral": + this.visitRegExpLiteral(node); + break; + default: + throw new Error(`Unknown type: ${node.type}`); + } + } + visitAlternative(node) { + if (this._handlers.onAlternativeEnter) { + this._handlers.onAlternativeEnter(node); + } + node.elements.forEach(this.visit, this); + if (this._handlers.onAlternativeLeave) { + this._handlers.onAlternativeLeave(node); + } + } + visitAssertion(node) { + if (this._handlers.onAssertionEnter) { + this._handlers.onAssertionEnter(node); + } + if (node.kind === "lookahead" || node.kind === "lookbehind") { + node.alternatives.forEach(this.visit, this); + } + if (this._handlers.onAssertionLeave) { + this._handlers.onAssertionLeave(node); + } + } + visitBackreference(node) { + if (this._handlers.onBackreferenceEnter) { + this._handlers.onBackreferenceEnter(node); + } + if (this._handlers.onBackreferenceLeave) { + this._handlers.onBackreferenceLeave(node); + } + } + visitCapturingGroup(node) { + if (this._handlers.onCapturingGroupEnter) { + this._handlers.onCapturingGroupEnter(node); + } + node.alternatives.forEach(this.visit, this); + if (this._handlers.onCapturingGroupLeave) { + this._handlers.onCapturingGroupLeave(node); + } + } + visitCharacter(node) { + if (this._handlers.onCharacterEnter) { + this._handlers.onCharacterEnter(node); + } + if (this._handlers.onCharacterLeave) { + this._handlers.onCharacterLeave(node); + } + } + visitCharacterClass(node) { + if (this._handlers.onCharacterClassEnter) { + this._handlers.onCharacterClassEnter(node); + } + node.elements.forEach(this.visit, this); + if (this._handlers.onCharacterClassLeave) { + this._handlers.onCharacterClassLeave(node); + } + } + visitCharacterClassRange(node) { + if (this._handlers.onCharacterClassRangeEnter) { + this._handlers.onCharacterClassRangeEnter(node); + } + this.visitCharacter(node.min); + this.visitCharacter(node.max); + if (this._handlers.onCharacterClassRangeLeave) { + this._handlers.onCharacterClassRangeLeave(node); + } + } + visitCharacterSet(node) { + if (this._handlers.onCharacterSetEnter) { + this._handlers.onCharacterSetEnter(node); + } + if (this._handlers.onCharacterSetLeave) { + this._handlers.onCharacterSetLeave(node); + } + } + visitFlags(node) { + if (this._handlers.onFlagsEnter) { + this._handlers.onFlagsEnter(node); + } + if (this._handlers.onFlagsLeave) { + this._handlers.onFlagsLeave(node); + } + } + visitGroup(node) { + if (this._handlers.onGroupEnter) { + this._handlers.onGroupEnter(node); + } + node.alternatives.forEach(this.visit, this); + if (this._handlers.onGroupLeave) { + this._handlers.onGroupLeave(node); + } + } + visitPattern(node) { + if (this._handlers.onPatternEnter) { + this._handlers.onPatternEnter(node); + } + node.alternatives.forEach(this.visit, this); + if (this._handlers.onPatternLeave) { + this._handlers.onPatternLeave(node); + } + } + visitQuantifier(node) { + if (this._handlers.onQuantifierEnter) { + this._handlers.onQuantifierEnter(node); + } + this.visit(node.element); + if (this._handlers.onQuantifierLeave) { + this._handlers.onQuantifierLeave(node); + } + } + visitRegExpLiteral(node) { + if (this._handlers.onRegExpLiteralEnter) { + this._handlers.onRegExpLiteralEnter(node); + } + this.visitPattern(node.pattern); + this.visitFlags(node.flags); + if (this._handlers.onRegExpLiteralLeave) { + this._handlers.onRegExpLiteralLeave(node); + } + } +} + +function parseRegExpLiteral(source, options) { + return new RegExpParser(options).parseLiteral(String(source)); +} +function validateRegExpLiteral(source, options) { + return new RegExpValidator(options).validateLiteral(source); +} +function visitRegExpAST(node, handlers) { + new RegExpVisitor(handlers).visit(node); +} + +export { ast as AST, RegExpParser, RegExpValidator, parseRegExpLiteral, validateRegExpLiteral, visitRegExpAST }; +//# sourceMappingURL=index.mjs.map diff --git a/packages/json-validator/src/index.ts b/packages/json-validator/src/index.ts new file mode 100644 index 00000000..0e9b6092 --- /dev/null +++ b/packages/json-validator/src/index.ts @@ -0,0 +1,25 @@ +/* + * index.ts + * + * Copyright (C) 2024 by Posit Software, PBC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export * from "./validator"; \ No newline at end of file diff --git a/packages/json-validator/src/regexp.js b/packages/json-validator/src/regexp.js new file mode 100644 index 00000000..c7d7d69c --- /dev/null +++ b/packages/json-validator/src/regexp.js @@ -0,0 +1,66 @@ +/* + * regexp.js (NB this is javascript and not typescript) + * + * Routines to manipulate regular expressions. + * + * Copyright (C) 2021-2022 Posit Software, PBC + * + */ + +import * as regexpp from "./external/regexpp.mjs"; + +function prefixesFromParse(parse) { + if (parse.type === "Pattern" || parse.type === "CapturingGroup") { + const alternatives = parse.alternatives.map(prefixesFromParse); + return `(${alternatives.join("|")})`; + } else if (parse.type === "Alternative") { + const result = []; + for (let i = 0; i < parse.elements.length; ++i) { + const thisRe = []; + for (let j = 0; j < i; ++j) { + thisRe.push(parse.elements[j].raw); + } + thisRe.push(prefixesFromParse(parse.elements[i])); + result.push(thisRe.join("")); + } + return `(${result.join("|")})`; + } else if (parse.type === "RegExpLiteral") { + return prefixesFromParse(parse.pattern); + } else if (parse.type === "Character") { + return `${parse.raw}?`; + } else if (parse.type === "Quantifier") { + if (parse.min === 0 && parse.max === 1) { + // this is a ? quantifier + return prefixesFromParse(parse.element); + } + if (parse.min === 1 && parse.max === Infinity) { + // this is the + quantifier + return `(${parse.element.raw}*)` + prefixesFromParse(parse.element); + } + if (parse.min === 0 && parse.max === Infinity) { + // this is the kleene star + // prefixes(p+) = prefixes(p*) + return `(${parse.element.raw}*)` + prefixesFromParse(parse.element); + } else { + throw new Error( + `Internal Error, can't handle quantifiers min=${parse.min} max=${parse.max}` + ); + } + } else if (parse.type === "CharacterSet") { + return `${parse.raw}?`; + } else if (parse.type === "CharacterClass") { + return `${parse.raw}?`; + } + throw new Error(`Internal Error, don't know how to handle ${parse.type}`); +} + +export function prefixes(regexp) { + regexp = regexp.source; + regexp = regexp.slice(1, -1); + + return new RegExp( + "^" + + prefixesFromParse(regexpp.parseRegExpLiteral(new RegExp(regexp))) + + "$" + ); +} diff --git a/packages/json-validator/src/resolve.ts b/packages/json-validator/src/resolve.ts new file mode 100644 index 00000000..2cbc0c63 --- /dev/null +++ b/packages/json-validator/src/resolve.ts @@ -0,0 +1,93 @@ +/* + * resolve.ts + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { getSchemaDefinition } from "./schema"; + +import { ConcreteSchema, schemaCall } from "./types"; + +export function maybeResolveSchema( + schema: ConcreteSchema, +): ConcreteSchema | true | false | undefined { + try { + return resolveSchema(schema); + } catch (_e) { + return undefined; + } +} + +export function resolveSchema( + schema: ConcreteSchema | false | true, + visit?: (schema: ConcreteSchema) => void, + hasRef?: (schema: ConcreteSchema) => boolean, + next?: (schema: ConcreteSchema) => ConcreteSchema, +): ConcreteSchema | false | true { + if (schema === false || schema === true) { + return schema; + } + if (hasRef === undefined) { + hasRef = (cursor: ConcreteSchema) => { + return schemaCall(cursor, { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ref: (_s) => true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + }, (_s) => false); + }; + } + if (!hasRef(schema)) { + return schema; + } + if (visit === undefined) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + visit = (_schema: ConcreteSchema) => {}; + } + if (next === undefined) { + next = (cursor: ConcreteSchema) => { + const result = schemaCall(cursor, { + ref: (s) => getSchemaDefinition(s.$ref), + }); + if (result === undefined) { + throw new Error( + "couldn't resolve schema ${JSON.stringify(cursor)}", + ); + } + return result; + }; + } + + // this is on the chancy side of clever, but we're going to be extra + // careful here and use the cycle-detecting trick. This code runs + // in the IDE and I _really_ don't want to accidentally freeze them. + // + // I'm sufficiently dismayed by badly-written emacs modes that randomly + // freeze on me from some unforeseen looping condition that I want + // to go out of my way to avoid this for our users. + + let cursor1: ConcreteSchema = schema; + let cursor2: ConcreteSchema = schema; + let stopped = false; + do { + cursor1 = next(cursor1); + visit(cursor1); + // we don't early exit here. instead, we stop cursor2 and let cursor1 catch up. + // This way, visit(cursor1) covers everything in order. + if (hasRef(cursor2)) { + cursor2 = next(cursor2); + } else { + stopped = true; + } + // move cursor2 twice as fast to detect cycles. + if (hasRef(cursor2)) { + cursor2 = next(cursor2); + } else { + stopped = true; + } + if (!stopped && cursor1 === cursor2) { + throw new Error(`reference cycle detected at ${JSON.stringify(cursor1)}`); + } + } while (hasRef(cursor1)); + + return cursor1; +} diff --git a/packages/json-validator/src/schema-navigation.ts b/packages/json-validator/src/schema-navigation.ts new file mode 100644 index 00000000..cd619b50 --- /dev/null +++ b/packages/json-validator/src/schema-navigation.ts @@ -0,0 +1,191 @@ +/* + * schema-navigation.ts + * + * functions to navigate schema + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { resolveSchema } from "./resolve"; + +import { prefixes } from "./regexp.js"; + +import { schemaType } from "./types"; + +// NB we have _three_ schema navigation functions which behave +// differently and are needed in different cases + +// navigateSchemaBySchemaPath is used to resolve inner schema in error +// messages. it navigates to sets of schema from the schema path given +// by ajv + +// navigateSchemaByInstancePath is used to resolve inner schema via possible +// instance paths. It navigates to sets of schema from an _instance path_, +// returning the set of schema that could be navigated to by the particular +// sequence of keys (and array offsets) +export function navigateSchemaByInstancePath( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + path: (number | string)[], + allowPartialMatches?: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const inner = (subSchema: any, index: number): any[] => { + subSchema = resolveSchema(subSchema); + if (index === path.length) { + return [subSchema]; + } + const st = schemaType(subSchema); + if (st === "object") { + const key = path[index]; + if (typeof key === "number") { + // in pathological cases, we may end up with a number key here. + // ignore it. + return []; + } + // does it match a properties key exactly? use it + if (subSchema.properties && subSchema.properties[key]) { + return inner(subSchema.properties[key], index + 1); + } + // does the key match a regular expression in a patternProperties key? use it + const patternPropMatch = matchPatternProperties( + subSchema, + key, + allowPartialMatches !== undefined && + allowPartialMatches && + index === path.length - 1, // allow prefix matches only if it's the last entry + ); + if (patternPropMatch) { + return inner(patternPropMatch, index + 1); + } + + // because we're using this in an autocomplete scenario, there's the "last entry is a prefix of a + // valid key" special case. + if (index !== path.length - 1) { + return []; + } + const completions = Object.getOwnPropertyNames(subSchema.properties || {}) + .filter( + (name) => name.startsWith(key), + ); + if (completions.length === 0) { + return []; + } + return [subSchema]; + } else if (st === "array") { + if (subSchema.items === undefined) { + // no items schema, can't navigate to expected schema + return []; + } + // don't index into array with a string path. + if (typeof path[index] === "string") { + return []; + } + return inner(subSchema.items, index + 1); + } else if (st === "anyOf") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return subSchema.anyOf.map((ss: any) => inner(ss, index)); + } else if (st === "allOf") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return subSchema.allOf.map((ss: any) => inner(ss, index)); + } else { + // if path wanted to navigate deeper but this is a YAML + // "terminal" (not a compound type) then this is not a valid + // schema to complete on. + return []; + } + }; + return inner(schema, 0).flat(Infinity); +} + +// navigateSchemaBySchemaPathSingle returns always a single schema. It is used to +// walk the actual concrete schemas ("take _this specific anyOf_ +// entry, then that specific key", and give me the resulting schema") +export function navigateSchemaBySchemaPathSingle( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + path: (number | string)[], +// eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const ensurePathFragment = ( + fragment: number | string, + expected: number | string, + ) => { + if (fragment !== expected) { + throw new Error( + `navigateSchemaBySchemaPathSingle: ${fragment} !== ${expected}`, + ); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const inner = (subschema: any, index: number): any => { + subschema = resolveSchema(subschema); + if (subschema === undefined) { + throw new Error( + `navigateSchemaBySchemaPathSingle: invalid path navigation`, + ); + } + if (index === path.length) { + return subschema; + } + const st = schemaType(subschema); + switch (st) { + case "anyOf": + ensurePathFragment(path[index], "anyOf"); + return inner(subschema.anyOf[path[index + 1]], index + 2); + case "allOf": + ensurePathFragment(path[index], "allOf"); + return inner(subschema.allOf[path[index + 1]], index + 2); + case "array": + ensurePathFragment(path[index], "array"); + return inner(subschema.arrayOf.schema, index + 2); + case "object": + ensurePathFragment(path[index], "object"); + if (path[index + 1] === "properties") { + return inner(subschema.properties[path[index + 2]], index + 3); + } else if (path[index + 1] === "patternProperties") { + return inner(subschema.patternProperties[path[index + 2]], index + 3); + } else if (path[index + 1] === "additionalProperties") { + return inner(subschema.additionalProperties, index + 2); + } else { + throw new Error( + `navigateSchemaBySchemaPathSingle: bad path fragment ${ + path[index] + } in object navigation`, + ); + } + default: + throw new Error( + `navigateSchemaBySchemaPathSingle: can't navigate schema type ${st}`, + ); + } + }; + return inner(schema, 0); +} + +function matchPatternProperties( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + key: string, + matchThroughPrefixes: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any | false { + for ( + const [regexpStr, subschema] of Object.entries( + schema.patternProperties || {}, + ) + ) { + let pattern: RegExp; + if (matchThroughPrefixes) { + pattern = prefixes(new RegExp(regexpStr)) as RegExp; + } else { + pattern = new RegExp(regexpStr); + } + if (key.match(pattern)) { + return subschema; + } + } + return false; +} diff --git a/packages/json-validator/src/schema-utils.ts b/packages/json-validator/src/schema-utils.ts new file mode 100644 index 00000000..369dc8fd --- /dev/null +++ b/packages/json-validator/src/schema-utils.ts @@ -0,0 +1,324 @@ +/* +* schema-utils.ts +* +* Copyright (C) 2022 Posit Software, PBC +* +*/ + +import { navigateSchemaBySchemaPathSingle } from "./schema-navigation"; + +import { + AllOfSchema, + AnyOfSchema, + ArraySchema, + Completion, + ConcreteSchema, + EnumSchema, + ObjectSchema, + RefSchema, + Schema, + SchemaCall, + schemaCall, + schemaDispatch, + schemaDocString, +} from "./types"; + +import { resolveSchema } from "./resolve"; + +export function resolveDescription(s: string | RefSchema): string { + if (typeof s === "string") { + return s; + } + const valueS = resolveSchema(s); + if (valueS === false || valueS === true) { + return ""; + } + if (valueS.documentation === undefined) { + return ""; + } + if (typeof valueS.documentation === "string") { + return valueS.documentation; + } + + if (valueS.documentation.short) { + return valueS.documentation.short; + } else { + return ""; + } +} + +export function schemaCompletions(s: Schema): Completion[] { + if (s === true || s === false) { + return []; + } + + // first resolve through $ref + let schema = resolveSchema(s); + + // then resolve through "complete-from" schema tags + schema = resolveSchema( + schema, + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + (_schema: ConcreteSchema) => {}, // visit + (schema: ConcreteSchema) => { + return (schema.tags !== undefined) && + (schema.tags["complete-from"] !== undefined); + }, + (schema: ConcreteSchema) => { + return navigateSchemaBySchemaPathSingle( + schema, + schema.tags!["complete-from"] as ((number | string)[]), + ); + }, + ); + + if (schema === true || schema === false) { + return []; + } + + // TODO this is slightly inefficient since recursions call + // normalize() multiple times + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const normalize = (completions: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (completions || []).map((c: any) => { + if (typeof c === "string") { + return { + type: "value", + display: c, + value: c, + description: "", + suggest_on_accept: false, + schema, + }; + } + return { + ...c, + description: resolveDescription(c.description), + schema, + }; + }); + return result; + }; + + if (schema.completions && schema.completions.length) { + return normalize(schema.completions); + } + + if ( + schema.tags && schema.tags.completions + ) { + if ( + Array.isArray(schema.tags.completions) && + schema.tags.completions.length + ) { + return normalize(schema.tags.completions); + } else { + return normalize( + Object.values(schema.tags.completions as Record), + ); + } + } + + return schemaCall(schema, { + array: (s) => { + if (s.items) { + return schemaCompletions(s.items); + } else { + return []; + } + }, + anyOf: (s) => { + return s.anyOf.map(schemaCompletions).flat(); + }, + allOf: (s) => { + return s.allOf.map(schemaCompletions).flat(); + }, + "object": (s) => { + // we actually mutate the schema here to avoid recomputing. + s.cachedCompletions = getObjectCompletions(s); + return normalize(s.cachedCompletions); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + }, (_) => []); +} + +function getObjectCompletions(s: ConcreteSchema): Completion[] { + const completionsParam: string[] = + (s.tags && s.tags.completions as string[]) || []; + return schemaCall(s, { + "object": (schema) => { + const properties = schema.properties; + const objectKeys = completionsParam.length + ? completionsParam + : Object.getOwnPropertyNames(properties); + const completions: Completion[] = []; + for (const k of objectKeys) { + const schema = properties && properties[k]; + const maybeDescriptions: (undefined | string | { $ref: string })[] = []; + let hidden = false; + if (schema !== undefined && schema !== true && schema !== false) { + // if a ref schema has documentation, use that directly. + if (schema.documentation) { + maybeDescriptions.push(schemaDocString(schema.documentation)); + } else { + // in the case of recursive schemas, a back reference to a schema + // that hasn't been registered yet is bound to fail. In that + // case, maybeResolveSchema will return undefined, and we + // potentially store a special description entry, deferring the + // resolution to runtime. + let described = false; + const visitor = (schema: Schema) => { + if (schema === false || schema === true) { + return; + } + if (schema.hidden) { + hidden = true; + } + if (described) { + return; + } + if (schema.documentation) { + maybeDescriptions.push(schemaDocString(schema.documentation)); + described = true; + } + }; + try { + resolveSchema(schema, visitor); + } catch (_e) { + // TODO catch only the lookup exception + } + if (!described) { + schemaDispatch(schema, { + ref: (schema) => maybeDescriptions.push({ $ref: schema.$ref }), + }); + } + } + } + if (hidden) { + continue; + } + let description: (string | { $ref: string }) = ""; + for (const md of maybeDescriptions) { + if (md !== undefined) { + description = md; + break; + } + } + completions.push({ + type: "key", + display: "", // attempt to not show completion title. + value: `${k}: `, + description, + suggest_on_accept: true, + }); + } + return completions; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + }, (_) => + completionsParam.map((c) => ({ + type: "value", + display: "", + value: c, + description: "", + suggest_on_accept: false, + }))); +} + +export function possibleSchemaKeys(schema: Schema): string[] { + const precomputedCompletions = schemaCompletions(schema).filter((c) => + c.type === "key" + ).map((c) => c.value.split(":")[0]); + if (precomputedCompletions.length) { + return precomputedCompletions; + } + + // FIXME we likely got unlucky and were handed an unnamed schema + // from inside an ajv error. + + const results: string[] = []; + // we do a best-effort thing here. + walkSchema(schema, { + "object": (s: ObjectSchema) => { + results.push(...Object.keys(s.properties || {})); + return true; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + "array": (_s: ArraySchema) => true, + }); + return results; +} + +export function possibleSchemaValues(schema: Schema): string[] { + // FIXME we likely got unlucky and were handed an unnamed schema + // from inside an ajv error. + + const results: string[] = []; + // we do a best-effort thing here. + walkSchema(schema, { + "enum": (s: EnumSchema) => { + results.push(...s["enum"].map(String)); + return true; + }, + // don't recurse into anything that introduces instancePath values + // eslint-disable-next-line @typescript-eslint/no-unused-vars + "array": (_s: ArraySchema) => true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + "object": (_s: ObjectSchema) => true, + }); + return results; +} + +export function walkSchema( + schema: Schema, + f: ((a: Schema) => boolean | void) | SchemaCall, +) { + const recur = { + "anyOf": (ss: AnyOfSchema) => { + for (const s of ss.anyOf) { + walkSchema(s, f); + } + }, + "allOf": (ss: AllOfSchema) => { + for (const s of ss.allOf) { + walkSchema(s, f); + } + }, + "array": (x: ArraySchema) => { + if (x.items) { + walkSchema(x.items, f); + } + }, + "object": (x: ObjectSchema) => { + if (x.properties) { + for (const ss of Object.values(x.properties)) { + walkSchema(ss, f); + } + } + if (x.patternProperties) { + for (const ss of Object.values(x.patternProperties)) { + walkSchema(ss, f); + } + } + if (x.propertyNames) { + walkSchema(x.propertyNames, f); + } + }, + }; + + if (typeof f === "function") { + if (f(schema) === true) { + return; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + if (schemaCall(schema, f, (_: Schema) => false) === true) { + return; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + schemaCall(schema, recur, (_: Schema) => false); +} diff --git a/packages/json-validator/src/schema.ts b/packages/json-validator/src/schema.ts new file mode 100644 index 00000000..8ccbc114 --- /dev/null +++ b/packages/json-validator/src/schema.ts @@ -0,0 +1,128 @@ +/* + * schema.ts + * + * JSON Schema core definitions + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { + AllOfSchema, + AnyOfSchema, + ConcreteSchema, + Schema, + schemaType, +} from "./types"; + +export function schemaAccepts(schema: Schema, testType: string): boolean { + const t = schemaType(schema); + if (t === testType) { + return true; + } + switch (t) { + case "anyOf": + return (schema as AnyOfSchema).anyOf.some((s: Schema) => + schemaAccepts(s, testType) + ); + case "allOf": + return (schema as AllOfSchema).allOf.every((s: Schema) => + schemaAccepts(s, testType) + ); + } + return false; +} + +export function schemaAcceptsScalar(schema: Schema): boolean { + const t = schemaType(schema); + if (["object", "array"].indexOf(t) !== -1) { + return false; + } + switch (t) { + case "anyOf": + return (schema as AnyOfSchema).anyOf.some((s: Schema) => + schemaAcceptsScalar(s) + ); + case "allOf": + return (schema as AllOfSchema).allOf.every((s: Schema) => + schemaAcceptsScalar(s) + ); + } + return true; +} + +export function schemaExhaustiveCompletions(schema: Schema): boolean { + switch (schemaType(schema)) { + case "false": + return true; + case "true": + return true; + case "anyOf": + return (schema as AnyOfSchema).anyOf.every(schemaExhaustiveCompletions); + case "allOf": + return (schema as AllOfSchema).allOf.every(schemaExhaustiveCompletions); + case "array": + return true; + case "object": + return true; + default: + return (schema as ConcreteSchema).exhaustiveCompletions || false; + } +} + +const definitionsObject: Record = {}; + +export function hasSchemaDefinition(key: string): boolean { + return definitionsObject[key] !== undefined; +} + +export function getSchemaDefinition(key: string): ConcreteSchema { + if (definitionsObject[key] === undefined) { + throw new Error(`Schema ${key} not found.`); + } + return definitionsObject[key]; +} + +export function setSchemaDefinition(schema: ConcreteSchema) { + if (schema.$id === undefined) { + throw new Error( + "setSchemaDefinition needs $id", + ); + } + // FIXME it's possible that without ajv we actually want to reset + // schema definitions + if (definitionsObject[schema.$id] === undefined) { + definitionsObject[schema.$id] = schema; + } +} + +export function getSchemaDefinitionsObject(): Record< + string, + ConcreteSchema +> { + return Object.assign({}, definitionsObject); +} + +export function expandAliasesFrom( + lst: string[], + defs: Record, +): string[] { + const aliases = defs; + const result = []; + + lst = lst.slice(); + for (let i = 0; i < lst.length; ++i) { + const el = lst[i]; + if (el.startsWith("$")) { + const v = aliases[el.slice(1)]; + if (v === undefined) { + throw new Error( + `${el} doesn't have an entry in the aliases map`, + ); + } + lst.push(...v); + } else { + result.push(el); + } + } + return result; +} diff --git a/packages/json-validator/src/semaphore.ts b/packages/json-validator/src/semaphore.ts new file mode 100644 index 00000000..13138d9a --- /dev/null +++ b/packages/json-validator/src/semaphore.ts @@ -0,0 +1,50 @@ +/* +* semaphore.ts +* +* Copyright (C) 2021-224 Posit Software, PBC +* +*/ + +export class Semaphore { + value: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tasks: any[]; + + constructor(value: number) { + this.value = value; + this.tasks = []; + } + + release() { + this.value += 1; + if (this.tasks.length) { + const { resolve } = this.tasks.pop(); + resolve(); + } + } + + async acquire() { + if (this.value > 0) { + this.value -= 1; + return; + } + const result = new Promise((resolve, reject) => { + this.tasks.push({ resolve, reject }); + }); + + await result; + await this.acquire(); + } + + async runExclusive(fun: () => Promise) { + await this.acquire(); + try { + const result = await fun(); + this.release(); + return result; + } catch (e) { + this.release(); + throw e; + } + } +} diff --git a/packages/json-validator/src/state.ts b/packages/json-validator/src/state.ts new file mode 100644 index 00000000..77551786 --- /dev/null +++ b/packages/json-validator/src/state.ts @@ -0,0 +1,69 @@ +/* + * state.ts + * + * Helpers to manage the global state required by the yaml intelligence + * code + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { Semaphore } from "./semaphore"; + +export function makeInitializer( + thunk: () => Promise, +): () => Promise { + let initStarted = false; + const hasInitSemaphore = new Semaphore(0); + + return async () => { + if (initStarted) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + await hasInitSemaphore.runExclusive(async () => {}); + return; + } + initStarted = true; + await thunk(); + hasInitSemaphore.release(); + }; +} + +let initializer: () => Promise = () => { + // can't call "err" here because we don't know if we're in the IDE or CLI + // this should be an internal error anyway. + throw new Error("initializer not set!!"); +}; + +export async function initState() { + await initializer(); +} + +// the logic for which initializer will ultimately be set is relatively +// hairy. We don't fundamentally know if we're +// being called from the CLI or the IDE, and the CLI has potentially +// many entry points. In addition, the CLI itself can have a number +// of different initializers depending on the command being called: +// +// - quarto build-js uses an initializer that skips precompiled modules +// +// - Some of the test suite uses an initializer with precompiled +// modules and includes tree-sitter (so the behavior is as close to +// the IDE as possible.) +// +// - quarto render, quarto preview, etc all want an initializer with +// precompiled modules and no tree-sitter (for performance reasons +// etc) +// +// The solution, then, is "first to call setInitializer decides the +// initializer". This way, quarto's render.ts modules (and others) can +// safely always set the last initializer. If inside the test suite +// for yaml-intelligence in the IDE, a different initializer will +// already have been set and will not be further touched. + +let hasSet = false; +export function setInitializer(init: () => Promise) { + if (hasSet) { + return; + } + initializer = makeInitializer(init); + hasSet = true; +} diff --git a/packages/json-validator/src/types.ts b/packages/json-validator/src/types.ts new file mode 100644 index 00000000..5353a852 --- /dev/null +++ b/packages/json-validator/src/types.ts @@ -0,0 +1,362 @@ +/* + * types.ts + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { TidyverseError } from "tidyverse-errors"; +import { ErrorLocation, MappedString } from "mapped-string"; + +import { + AnnotatedParse, + JSONValue +} from "annotated-json"; + +export interface ValidatedParseResult { + result: JSONValue; + errors: LocalizedError[]; +} + +export type ValidatorErrorHandlerFunction = ( + error: LocalizedError, + parse: AnnotatedParse, + /* this is the _outer_ schema, which failed the validation of + * parse.result. error also holds error.schema, which is a subschema + * of the outer schema which failed the validation of + * error.violatingObject (a subobject of parse.result). + */ + schema: Schema, +) => LocalizedError | null; + +export interface YAMLSchemaT { + schema: Schema; + + errorHandlers: ValidatorErrorHandlerFunction[]; + + addHandler( + handler: ValidatorErrorHandlerFunction, + ): void; + + transformErrors( + annotation: AnnotatedParse, + errors: LocalizedError[], + ): LocalizedError[]; + + validateParse( + src: MappedString, + annotation: AnnotatedParse, + ): Promise; + + reportErrorsInSource( + result: ValidatedParseResult, + _src: MappedString, + message: string, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: (a: string) => any, + log: (a: TidyverseError) => unknown, + ): ValidatedParseResult; +} + +export type Schema = + | FalseSchema + | TrueSchema + | BooleanSchema + | NumberSchema + | StringSchema + | NullSchema + | EnumSchema + | AnySchema + | AnyOfSchema + | AllOfSchema + | ArraySchema + | ObjectSchema + | RefSchema; + +export type ConcreteSchema = + | AnySchema + | BooleanSchema + | NumberSchema + | StringSchema + | NullSchema + | EnumSchema + | AnyOfSchema + | AllOfSchema + | ArraySchema + | ObjectSchema + | RefSchema; + +export type SchemaType = + | "any" + | "false" + | "true" + | "boolean" + | "number" + | "integer" + | "string" + | "null" + | "enum" + | "anyOf" + | "allOf" + | "array" + | "object" + | "ref"; + +export type InstancePath = (string | number)[]; +export type SchemaPath = (string | number)[]; + +export interface ValidationError { + value: AnnotatedParse; + schema: Schema; + message: string; + instancePath: InstancePath; + schemaPath: SchemaPath; +} + +export interface LocalizedError { + violatingObject: AnnotatedParse; + schema: Schema; // this is the *localized* schema, aka the schema that violatingObject failed. + message: string; + instancePath: InstancePath; + schemaPath: SchemaPath; + source: MappedString; + location: ErrorLocation; + niceError: TidyverseError; +} + +export interface Completion { + display: string; + type: "key" | "value"; + value: string; + description: string | { $ref: string }; + // deno-lint-ignore camelcase + suggest_on_accept: boolean; + + // `schema` stores the concrete schema that yielded the completion. + // We need to carry it explicitly because of combinators like anyOf + schema?: Schema; + + // the manually-generated documentation for the completion, if it exists + documentation?: string; +} + +export type FalseSchema = false; // the actual "false" value +export type TrueSchema = true; // the actual "true" value + +// these are not part of JSON Schema, but they're very useful so we'll +// have them. +export interface SchemaAnnotations { + // used to resolve schemas by reference + "$id"?: string; + + // when true, autocompletion suggests next completion automatically + exhaustiveCompletions?: boolean; + + // used to generate completions and HTML docs + documentation?: SchemaDocumentation; + + // used to autogenerate error message + description?: string; + + // schema-defined error message when schema fails + errorMessage?: string; + + // when true, don't show on completions or documentation + hidden?: boolean; + + // controls generation of completions + completions?: string[]; // from the schema defn + + // stores precomputed completions at runtime + cachedCompletions?: Completion[]; + + // arbitrary tags used for a variety of reasons + tags?: Record; + + // used internally for debugging + _internalId?: number; +} + +export type SchemaDocumentation = string | { + short?: string; + long?: string; +}; + +export interface BooleanSchema extends SchemaAnnotations { + "type": "boolean"; +} + +// this is not JSON schema, but makes our life easier. +export interface AnySchema extends SchemaAnnotations { + "type": "any"; +} + +export interface NumberSchema extends SchemaAnnotations { + "type": "number" | "integer"; + minimum?: number; + exclusiveMinimum?: number; + maximum?: number; + exclusiveMaximum?: number; +} + +export interface StringSchema extends SchemaAnnotations { + "type": "string"; + pattern?: string; + compiledPattern?: RegExp; +} + +export interface NullSchema extends SchemaAnnotations { + "type": "null"; +} + +export interface EnumSchema extends SchemaAnnotations { + "type": "enum"; + "enum": JSONValue[]; +} + +export interface AnyOfSchema extends SchemaAnnotations { + "type": "anyOf"; + anyOf: Schema[]; +} + +export interface AllOfSchema extends SchemaAnnotations { + "type": "allOf"; + allOf: Schema[]; +} + +export interface ArraySchema extends SchemaAnnotations { + "type": "array"; + items?: Schema; + minItems?: number; + maxItems?: number; +} + +export interface ObjectSchema extends SchemaAnnotations { + "type": "object"; + properties?: { [key: string]: Schema }; + patternProperties?: { [key: string]: Schema }; + compiledPatterns?: { [key: string]: RegExp }; + required?: string[]; + additionalProperties?: Schema; + propertyNames?: Schema; + + closed?: boolean; // this is not part of JSON schema, but makes error reporting that much easier. +} + +export interface RefSchema extends SchemaAnnotations { + "type": "ref"; + "$ref": string; +} + +export interface ValidationTraceNode { + edge: number | string; + errors: ValidationError[]; + children: ValidationTraceNode[]; +} + +export function schemaType(schema: Schema): SchemaType { + if (schema === false) { + return "false"; + } + if (schema === true) { + return "true"; + } + return schema.type; +} + +interface SchemaDispatch { + "any"?: (x: AnySchema) => unknown; + "false"?: (x: FalseSchema) => unknown; + "true"?: (x: TrueSchema) => unknown; + "boolean"?: (x: BooleanSchema) => unknown; + "number"?: (x: NumberSchema) => unknown; + "integer"?: (x: NumberSchema) => unknown; + "string"?: (x: StringSchema) => unknown; + "null"?: (x: NullSchema) => unknown; + "enum"?: (x: EnumSchema) => unknown; + "anyOf"?: (x: AnyOfSchema) => unknown; + "allOf"?: (x: AllOfSchema) => unknown; + "array"?: (x: ArraySchema) => unknown; + "object"?: (x: ObjectSchema) => unknown; + "ref"?: (x: RefSchema) => unknown; +} + +export interface SchemaCall { + "any"?: (x: AnySchema) => T; + "false"?: (x: FalseSchema) => T; + "true"?: (x: TrueSchema) => T; + "boolean"?: (x: BooleanSchema) => T; + "number"?: (x: NumberSchema) => T; + "integer"?: (x: NumberSchema) => T; + "string"?: (x: StringSchema) => T; + "null"?: (x: NullSchema) => T; + "enum"?: (x: EnumSchema) => T; + "anyOf"?: (x: AnyOfSchema) => T; + "allOf"?: (x: AllOfSchema) => T; + "array"?: (x: ArraySchema) => T; + "object"?: (x: ObjectSchema) => T; + "ref"?: (x: RefSchema) => T; +} + +// FIXME these functions should be in a separate file + +export function schemaDispatch(s: Schema, d: SchemaDispatch): void { + const st: SchemaType = schemaType(s); + // TypeScript can't realize that this + // dispatch is safe (because it can't associate the return of st with s). + // TODO https://www.typescriptlang.org/docs/handbook/2/conditional-types.html + // + if (d[st]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (d[st]! as ((x: any) => unknown))(s as any); + } +} + +export function schemaCall( + s: Schema, + d: SchemaCall, + other?: (s: Schema) => T, +): T { + const st: SchemaType = schemaType(s); + // TypeScript can't realize that this + // dispatch is safe (because it can't associate the return of st with s). + // TODO https://www.typescriptlang.org/docs/handbook/2/conditional-types.html + // TODO https://www.typescriptlang.org/docs/handbook/2/types-from-types.html + // + if (d[st]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (d[st]! as ((x: any) => T))(s as any); + } + if (other) { + return other(s); + } + // TODO this should be InternalError but we don't want + // to introduce a dependency on error.ts + throw new Error(`Internal Error: Dispatch failed for type ${st}`); +} + +// note that we intentionally never use d.long, since that's +// reserved for the webpage documentation stuff. +export function schemaDocString(d: SchemaDocumentation): string { + if (typeof d === "string") { + return d; + } + if (d.short) { + return d.short; + } + return ""; +} + +export function schemaDescription(schema: Schema): string { + if (schema === true) { + return `be anything`; + } else if (schema === false) { + return `be no possible value`; + // this is clunky phrasing because + // of `be ...` requirement for + // descriptions + } else { + return schema.description || `be ${schemaType(schema)}`; + } +} + diff --git a/packages/json-validator/src/validator-queue.ts b/packages/json-validator/src/validator-queue.ts new file mode 100644 index 00000000..a74ad086 --- /dev/null +++ b/packages/json-validator/src/validator-queue.ts @@ -0,0 +1,82 @@ +/* +* validator-queue.ts +* +* Copyright (C) 2022 Posit Software, PBC +* +*/ + +import { YAMLSchema } from "./yaml-schema"; + +import { setDefaultErrorHandlers } from "./errors"; + +import { ValidatorErrorHandlerFunction } from "./types"; + +import { RefSchema, Schema, schemaType } from "./types"; + +const yamlValidators: Record = {}; + +function getSchemaName(schema: Schema): string { + if (schema === true || schema === false) { + throw new Error("Expected schema to be named"); + } + + let schemaName = schema["$id"]; + if (schemaName !== undefined) { + return schemaName; + } + + if (schemaType(schema) === "ref") { + schemaName = (schema as RefSchema)["$ref"]; + } + if (schemaName !== undefined) { + return schemaName; + } + + throw new Error("Expected schema to be named"); +} + +function getValidator(schema: Schema): YAMLSchema { + const schemaName = getSchemaName(schema); // name of schema so we can look it up on the validator cache + if (yamlValidators[schemaName]) { + return yamlValidators[schemaName]; + } + + const validator = new YAMLSchema(schema); + + yamlValidators[schemaName] = validator; + + setDefaultErrorHandlers(validator); + + return validator; +} + +export type WithValidatorFun = (validator: YAMLSchema) => Promise; +export async function withValidator( + schema: Schema, + fun: WithValidatorFun, +): Promise { + let result: T | undefined; + let error; + try { + const validator = getValidator(schema); + result = await fun(validator); + } catch (e) { + error = e; + } + + if (error !== undefined) { + throw error; + } + + return result! as T; +} + +export function addValidatorErrorHandler( + schema: Schema, + handler: ValidatorErrorHandlerFunction, +) { + // deno-lint-ignore require-await + return withValidator(schema, async (validator) => { + validator.addHandler(handler); + }); +} diff --git a/packages/json-validator/src/validator.ts b/packages/json-validator/src/validator.ts new file mode 100644 index 00000000..8af3a4e9 --- /dev/null +++ b/packages/json-validator/src/validator.ts @@ -0,0 +1,688 @@ +/* + * validator.ts + * + * main validator class. + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { + AnnotatedParse, JSONValue +} from "annotated-json"; + +import { + AllOfSchema, + AnyOfSchema, + AnySchema, + ArraySchema, + BooleanSchema, + EnumSchema, + LocalizedError, + NullSchema, + NumberSchema, + ObjectSchema, + RefSchema, + Schema, + schemaCall, + schemaType, + StringSchema, + ValidationError, + ValidationTraceNode, +} from "./types"; + +import { resolveSchema } from "./resolve"; + +import { MappedString } from "mapped-string"; +import { createLocalizedError } from "./errors"; + +//////////////////////////////////////////////////////////////////////////////// + +class ValidationContext { + instancePath: (number | string)[]; + root: ValidationTraceNode; + nodeStack: ValidationTraceNode[]; + + currentNode: ValidationTraceNode; + + constructor() { + this.instancePath = []; + this.currentNode = { edge: "#", errors: [], children: [] }; + this.nodeStack = [this.currentNode]; + this.root = this.currentNode; + } + + error(value: AnnotatedParse, schema: Schema, message: string) { + this.currentNode.errors.push({ + value, + schema, + message, + instancePath: this.instancePath.slice(), + schemaPath: this.nodeStack.map((node) => node.edge), + }); + } + + pushSchema(schemaPath: number | string) { + const newNode = { + edge: schemaPath, + errors: [], + children: [], + }; + this.currentNode.children.push(newNode); + this.currentNode = newNode; + this.nodeStack.push(newNode); + } + popSchema(success: boolean) { + this.nodeStack.pop(); + this.currentNode = this.nodeStack[this.nodeStack.length - 1]; + if (success) { + this.currentNode.children.pop(); + } + return success; + } + + pushInstance(instance: number | string) { + this.instancePath.push(instance); + } + popInstance() { + this.instancePath.pop(); + } + + withSchemaPath(schemaPath: number | string, chunk: () => boolean): boolean { + this.pushSchema(schemaPath); + return this.popSchema(chunk()); + } + + validate( + schema: Schema, + source: MappedString, + value: AnnotatedParse, + pruneErrors = true, + ): LocalizedError[] { + if (validateGeneric(value, schema, this)) { + // validation passed, don't collect errors + return []; + } + return this.collectErrors(schema, source, value, pruneErrors); + } + + // if pruneErrors is false, we return all errors. This is typically + // hard to interpret directly because of anyOf errors. + // + // it's possible that the best API is for LocalizedErrors to explicitly nest + // so that anyOf errors are reported in their natural structure. + // + // if pruneErrors is true, then we only report one of the anyOf + // errors, avoiding most issues. (`patternProperties` can still + // cause error overlap and potential confusion, and we need those + // because of pandoc properties..) + collectErrors( + _schema: Schema, + source: MappedString, + _value: AnnotatedParse, + pruneErrors = true, + ): LocalizedError[] { + const inner = (node: ValidationTraceNode) => { + const result: ValidationError[] = []; + if (node.edge === "anyOf" && pruneErrors) { + // heuristic: + // if one error says "you're missing a required field" + // and another error says "one of your fields is not allowed" + // + // we assume that the error about a missing required field is better, because + // that implies that the schema with a missing required field + // allowed the field that was disallowed by + // the other schema, and we prefer schemas that are "partially correct" + + // more generally, it seems that we want to weigh our decisions + // towards schema that have validated large parts of the overall object. + // we don't have a way to record that right now, though. + const innerResults: ValidationError[][] = node.children.map(inner); + + const isRequiredError = (e: ValidationError) => + e.schemaPath.indexOf("required") === e.schemaPath.length - 1; + const isPropertyNamesError = (e: ValidationError) => + e.schemaPath.indexOf("propertyNames") !== -1; + if ( + innerResults.some((el) => el.length && isRequiredError(el[0])) && + innerResults.some((el) => el.length && isPropertyNamesError(el[0])) + ) { + return innerResults.filter((r) => { + return r.length && r[0].schemaPath.slice(-1)[0] === "required"; + })[0]!; + } + + // As a last resort, we sort suggestions based on "quality" + const errorTypeQuality = (e: ValidationError): number => { + const t = e.schemaPath.slice().reverse(); + if (typeof e.schema === "object") { + if ( + e.schema.tags && e.schema.tags["error-importance"] && + typeof e.schema.tags["error-importance"] === "number" + ) { + return e.schema.tags["error-importance"]; + } + } + if (e.schemaPath.indexOf("propertyNames") !== -1) { + // suggesting invalid property names is bad if there are other errors to report + return 10; + } + if (t[0] === "required") { + return 0; // we slightly prefer reporting "required" fields. + } + if (t[0] === "type") { + if (t[1] === "null") { + return 10; // suggesting a null value is bad. + } + return 1; + } + return 1; + }; + + const errorComparator = (a: number[], b: number[]): number => { + for (let i = 0; i < a.length; ++i) { + if (a[i] < b[i]) { + return -1; + } + if (a[i] > b[i]) { + return 1; + } + } + return 0; + }; + + // prune all but the anyOf error which reports + // - the least bad overall error in the group + // - or the error with the smallest total span (presumably showing the error that is the + // easiest to fix) + let bestResults: ValidationError[] = []; + let bestError = [Infinity, Infinity]; + for (const resultGroup of innerResults) { + let maxQuality = -Infinity; + let totalSpan = 0; + for (const result of resultGroup) { + totalSpan += result.value.end - result.value.start; + maxQuality = Math.max(maxQuality, errorTypeQuality(result)); + } + const thisError = [maxQuality, totalSpan]; + if (errorComparator(thisError, bestError) === -1) { + bestError = thisError; + bestResults = resultGroup; + } + } + + return bestResults; + } else { + result.push(...node.errors); + for (const child of node.children) { + result.push(...inner(child)); + } + return result; + } + }; + const errors = inner(this.root); + + const result = errors.map((validationError) => + createLocalizedError({ + violatingObject: validationError.value, + instancePath: validationError.instancePath, + schemaPath: validationError.schemaPath, + schema: validationError.schema, + message: validationError.message, + source, + }) + ); + + return result; + } +} + +function validateGeneric( + value: AnnotatedParse, + s: Schema, + context: ValidationContext, +): boolean { + s = resolveSchema(s); + const st = schemaType(s); + return context.withSchemaPath(st, () => + schemaCall(s, { + "false": (schema: false) => { + context.error(value, schema, "false"); + return false; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + "true": (_: true) => true, + "any": (schema: AnySchema) => validateAny(value, schema, context), + "boolean": (schema: BooleanSchema) => + validateBoolean(value, schema, context), + "number": (schema: NumberSchema) => + validateNumber(value, schema, context), + "string": (schema: StringSchema) => + validateString(value, schema, context), + "null": ((schema: NullSchema) => validateNull(value, schema, context)), + "enum": ((schema: EnumSchema) => validateEnum(value, schema, context)), + "anyOf": (schema: AnyOfSchema) => validateAnyOf(value, schema, context), + "allOf": (schema: AllOfSchema) => validateAllOf(value, schema, context), + "array": (schema: ArraySchema) => validateArray(value, schema, context), + "object": (schema: ObjectSchema) => + validateObject(value, schema, context), + "ref": (schema: RefSchema) => + validateGeneric(value, resolveSchema(schema), context), + })); +} + +function typeIsValid( + value: AnnotatedParse, + schema: Schema, + context: ValidationContext, + valid: boolean, +): boolean { + if (!valid) { + return context.withSchemaPath( + "type", + () => { + context.error(value, schema, "type mismatch"); + return false; + }, + ); + } + return valid; +} + +function validateAny( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _value: AnnotatedParse, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _schema: AnySchema, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: ValidationContext, +): boolean { + return true; +} + +function validateBoolean( + value: AnnotatedParse, + schema: BooleanSchema, + context: ValidationContext, +): boolean { + return typeIsValid(value, schema, context, typeof value.result === "boolean"); +} + +function validateNumber( + value: AnnotatedParse, + schema: NumberSchema, + context: ValidationContext, +) { + if (!typeIsValid(value, schema, context, typeof value.result === "number")) { + return false; + } + let result = true; + if (schema.minimum !== undefined) { + result = context.withSchemaPath( + "minimum", + () => { + const v = value.result as number; + if (!(v >= schema.minimum!)) { + context.error( + value, + schema, + `value ${value.result} is less than required minimum ${schema.minimum}`, + ); + return false; + } + return true; + }, + ); + } + if (schema.maximum !== undefined) { + result = context.withSchemaPath( + "maximum", + () => { + const v = value.result as number; + if (!(v <= schema.maximum!)) { + context.error( + value, + schema, + `value ${value.result} is greater than required maximum ${schema.maximum}`, + ); + return false; + } + return true; + }, + ); + } + if (schema.exclusiveMinimum !== undefined) { + result = context.withSchemaPath( + "exclusiveMinimum", + () => { + const v = value.result as number; + if (!(v > schema.exclusiveMinimum!)) { + context.error( + value, + schema, + `value ${value.result} is less than or equal to required (exclusive) minimum ${schema.exclusiveMinimum}`, + ); + return false; + } + return true; + }, + ); + } + if (schema.exclusiveMaximum !== undefined) { + result = context.withSchemaPath( + "exclusiveMaximum", + () => { + const v = value.result as number; + if (!(v < schema.exclusiveMaximum!)) { + context.error( + value, + schema, + `value ${value.result} is greater than or equal to required (exclusive) maximum ${schema.exclusiveMaximum}`, + ); + return false; + } + return true; + }, + ); + } + return result; +} + +function validateString( + value: AnnotatedParse, + schema: StringSchema, + context: ValidationContext, +) { + if (!typeIsValid(value, schema, context, typeof value.result === "string")) { + return false; + } + if (schema.pattern !== undefined) { + if (schema.compiledPattern === undefined) { + schema.compiledPattern = new RegExp(schema.pattern); + } + + // typescript doesn't see the typeIsValid check above. + if (!(value.result as string).match(schema.compiledPattern)) { + return context.withSchemaPath( + "pattern", + () => { + context.error(value, schema, `value doesn't match pattern`); + return false; + }, + ); + } + } + return true; +} + +function validateNull( + value: AnnotatedParse, + schema: NullSchema, + context: ValidationContext, +) { + if (!typeIsValid(value, schema, context, value.result === null)) { + return false; + } + return true; +} + +function validateEnum( + value: AnnotatedParse, + schema: EnumSchema, + context: ValidationContext, +) { + // FIXME do we do deepEquals here? that's the correct thing + // but probably won't come up for quarto, and it's slow and adds a dependency + for (const enumValue of schema["enum"]) { + if (enumValue === value.result) { + return true; + } + } + // didn't pass validation + context.error(value, schema, `must match one of the values`); + return false; +} + +function validateAnyOf( + value: AnnotatedParse, + schema: AnyOfSchema, + context: ValidationContext, +) { + let passingSchemas = 0; + for (let i = 0; i < schema.anyOf.length; ++i) { + const subSchema = schema.anyOf[i]; + context.withSchemaPath(i, () => { + if (validateGeneric(value, subSchema, context)) { + passingSchemas++; + return true; + } + return false; + }); + } + return passingSchemas > 0; +} + +function validateAllOf( + value: AnnotatedParse, + schema: AllOfSchema, + context: ValidationContext, +) { + let passingSchemas = 0; + for (let i = 0; i < schema.allOf.length; ++i) { + const subSchema = schema.allOf[i]; + context.withSchemaPath(i, () => { + if (validateGeneric(value, subSchema, context)) { + passingSchemas++; + return true; + } + return false; + }); + } + return passingSchemas === schema.allOf.length; +} + +function validateArray( + value: AnnotatedParse, + schema: ArraySchema, + context: ValidationContext, +) { + let result = true; + if (!typeIsValid(value, schema, context, Array.isArray(value.result))) { + return false; + } + const length = (value.result as JSONValue[]).length; + if ( + schema.minItems !== undefined && + length < schema.minItems + ) { + context.withSchemaPath( + "minItems", + () => { + context.error( + value, + schema, + `array should have at least ${schema.minItems} items but has ${length} items instead`, + ); + return false; + }, + ); + result = false; + } + if (schema.maxItems !== undefined && length > schema.maxItems) { + context.withSchemaPath( + "maxItems", + () => { + context.error( + value, + schema, + `array should have at most ${schema.maxItems} items but has ${length} items instead`, + ); + return false; + }, + ); + result = false; + } + if (schema.items !== undefined) { + result = context.withSchemaPath("items", () => { + let result = true; + for (let i = 0; i < value.components.length; ++i) { + context.pushInstance(i); + result = validateGeneric(value.components[i], schema.items!, context) && + result; + context.popInstance(); + } + return result; + }) && result; + } + return result; +} + +function validateObject( + value: AnnotatedParse, + schema: ObjectSchema, + context: ValidationContext, +) { + const isObject = (typeof value.result === "object") && + !Array.isArray(value.result) && (value.result !== null); + if (!typeIsValid(value, schema, context, isObject)) { + return false; + } + let result = true; + const ownProperties: Set = new Set( + Object.getOwnPropertyNames(value.result), + ); + + const objResult = value.result as { [key: string]: JSONValue }; + + const locate = ( + key: string, + keyOrValue: "key" | "value" = "value", + ): AnnotatedParse => { + for (let i = 0; i < value.components.length; i += 2) { + if (String(value.components[i].result) === key) { + if (keyOrValue === "value") { + return value.components[i + 1]; + } else { + return value.components[i]; + } + } + } + throw new Error(`Couldn't locate key ${key}`); + }; + const inspectedProps: Set = new Set(); + if (schema.closed) { + result = context.withSchemaPath("closed", () => { + if (schema.properties === undefined) { + throw new Error("Closed schemas need properties"); + } + let innerResult = true; + for (const key of ownProperties) { + if (!schema.properties[key]) { + context.error( + locate(key, "key"), + schema, + `object has invalid field ${key}`, + ); + innerResult = false; + } + } + return innerResult; + }) && result; + } + if (schema.properties !== undefined) { + result = context.withSchemaPath("properties", () => { + let result = true; + for (const [key, subSchema] of Object.entries(schema.properties!)) { + if (ownProperties.has(key)) { + inspectedProps.add(key); + context.pushInstance(key); + result = context.withSchemaPath( + key, + () => validateGeneric(locate(key), subSchema, context), + ) && result; + context.popInstance(); + } + } + return result; + }) && result; + } + if (schema.patternProperties !== undefined) { + result = context.withSchemaPath("patternProperties", () => { + let result = true; + for ( + const [key, subSchema] of Object.entries(schema.patternProperties!) + ) { + if (schema.compiledPatterns === undefined) { + schema.compiledPatterns = {}; + } + if (schema.compiledPatterns[key] === undefined) { + schema.compiledPatterns[key] = new RegExp(key); + } + const regexp = schema.compiledPatterns[key]; + + for ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [objectKey, _val] of Object.entries(objResult) + ) { + if (objectKey.match(regexp)) { + inspectedProps.add(objectKey); + context.pushInstance(objectKey); + result = context.withSchemaPath( + key, + () => validateGeneric(locate(objectKey), subSchema, context), + ) && result; + context.popInstance(); + } + } + } + return result; + }) && result; + } + if (schema.additionalProperties !== undefined) { + result = context.withSchemaPath("additionalProperties", () => { + return Object.keys(objResult) + .filter((objectKey) => !inspectedProps.has(objectKey)) + .every((objectKey) => + validateGeneric( + locate(objectKey), + schema.additionalProperties!, + context, + ) + ); + }) && result; + } + if (schema.propertyNames !== undefined) { + result = context.withSchemaPath("propertyNames", () => { + return Array.from(ownProperties) + .every((key) => + validateGeneric(locate(key, "key"), schema.propertyNames!, context) + ); + }) && result; + } + if (schema.required !== undefined) { + result = context.withSchemaPath("required", () => { + let result = true; + for (const reqKey of schema.required!) { + if (!ownProperties.has(reqKey)) { + context.error( + value, + schema, + `object is missing required property ${reqKey}`, + ); + result = false; + } + } + return result; + }) && result; + } + return result; +} +export function validate( + value: AnnotatedParse, + schema: Schema, + source: MappedString, + pruneErrors = true, +): LocalizedError[] { + const context = new ValidationContext(); + + return context.validate(schema, source, value, pruneErrors); +} diff --git a/packages/json-validator/src/yaml-schema.ts b/packages/json-validator/src/yaml-schema.ts new file mode 100644 index 00000000..47a735ff --- /dev/null +++ b/packages/json-validator/src/yaml-schema.ts @@ -0,0 +1,127 @@ +/* + * yaml-schema.ts + * + * A class to manage YAML Schema validation and associated tasks like + * error localization + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { MappedString } from "mapped-string"; + +import { TidyverseError } from "tidyverse-errors"; +import { ValidatorErrorHandlerFunction } from "./types"; + +import { validate } from "./validator"; +import { ValidatedParseResult } from "./types"; + +import { AnnotatedParse } from "annotated-json"; +import { + LocalizedError, + Schema, +} from "./types"; + +//////////////////////////////////////////////////////////////////////////////// + +export class YAMLSchema { + schema: Schema; + + // These are schema-specific error transformers to yield custom + // error messages. + + errorHandlers: ValidatorErrorHandlerFunction[]; + constructor(schema: Schema) { + this.errorHandlers = []; + this.schema = schema; + } + + addHandler( + handler: ValidatorErrorHandlerFunction, + ) { + this.errorHandlers.push(handler); + } + + transformErrors( + annotation: AnnotatedParse, + errors: LocalizedError[], + ): LocalizedError[] { + return errors.map((error) => { + for (const handler of this.errorHandlers) { + const localError = handler(error, annotation, this.schema); + if (localError === null) { + return null; + } + error = localError; + } + return error; + }).filter((error) => error !== null) as LocalizedError[]; + } + + // deno-lint-ignore require-await + async validateParse( + src: MappedString, + annotation: AnnotatedParse, + pruneErrors = true, + ) { + const validationErrors = validate( + annotation, + this.schema, + src, + pruneErrors, + ); + + if (validationErrors.length) { + const localizedErrors = this.transformErrors( + annotation, + validationErrors, + ); + return { + result: annotation.result, + errors: localizedErrors, + }; + } else { + return { + result: annotation.result, + errors: [], + }; + } + } + + // NB this needs explicit params for "error" and "log" because it might + // get called from the IDE, where we lack quarto's "error" and "log" + // infra + reportErrorsInSource( + result: ValidatedParseResult, + _src: MappedString, + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: (a: string) => any, + log: (a: TidyverseError) => unknown, + ) { + if (result.errors.length) { + if (message.length) { + error(message); + } + for (const err of result.errors) { + log(err.niceError); + } + } + return result; + } + + // NB this needs explicit params for "error" and "log" because it might + // get called from the IDE, where we lack quarto's "error" and "log" + // infra + async validateParseWithErrors( + src: MappedString, + annotation: AnnotatedParse, + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: (a: string) => any, + log: (a: TidyverseError) => unknown, + ) { + const result = await this.validateParse(src, annotation); + this.reportErrorsInSource(result, src, message, error, log); + return result; + } +} diff --git a/packages/json-validator/tsconfig.json b/packages/json-validator/tsconfig.json new file mode 100644 index 00000000..693f005e --- /dev/null +++ b/packages/json-validator/tsconfig.json @@ -0,0 +1,9 @@ +{ + "exclude": ["node_modules", "dist"], + "extends": "tsconfig/base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "outDir": "./dist", + }, +} \ No newline at end of file diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index f170b179..41587cd2 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -8,7 +8,8 @@ "dependencies": { "tsconfig": "*", "typescript": "^5.4.2", - "ansi-colors": "^4.1.3" + "ansi-colors": "^4.1.3", + "tidyverse-errors": "*" }, "devDependencies": { } } diff --git a/packages/mapped-string/src/error.ts b/packages/mapped-string/src/error.ts index 9d9eaccf..e003c4ab 100644 --- a/packages/mapped-string/src/error.ts +++ b/packages/mapped-string/src/error.ts @@ -7,7 +7,7 @@ import { mappedIndexToLineCol } from "./mapped-text"; import { lines } from "./text"; import { MappedString, Range } from "./types"; -import * as colors from "ansi-colors"; +import { quotedStringColor } from "tidyverse-errors"; export class InternalError extends Error { constructor( @@ -65,10 +65,6 @@ export function asErrorEx(e: unknown) { } } -export function quotedStringColor(msg: string) { - return colors.blue(msg); -} - export function formatLineRange( text: string, firstLine: number, diff --git a/packages/mapped-string/src/index.ts b/packages/mapped-string/src/index.ts index 8df49580..383d6a72 100644 --- a/packages/mapped-string/src/index.ts +++ b/packages/mapped-string/src/index.ts @@ -25,4 +25,5 @@ export * from "./ranged-text"; export * from "./mapped-text"; export * from "./types"; -export * from "./error"; \ No newline at end of file +export * from "./error"; +export * from "./text"; \ No newline at end of file diff --git a/packages/tidyverse-errors/src/errors.ts b/packages/tidyverse-errors/src/errors.ts index 97f74515..6cc91af6 100644 --- a/packages/tidyverse-errors/src/errors.ts +++ b/packages/tidyverse-errors/src/errors.ts @@ -54,3 +54,28 @@ export function tidyverseError(msg: string) { return `${colors.red("x")} ${msg}`; } } + +export interface ErrorLocation { + start: { + line: number; + column: number; + }; + end: { + line: number; + column: number; + }; +} + +export interface TidyverseError { + heading: string; + error: string[]; + info: Record; // use tag for infos to only display one error of each tag + fileName?: string; + location?: ErrorLocation; + sourceContext?: string; +} + +export function quotedStringColor(msg: string) { + return colors.blue(msg); +} + From 2041f2254bdbbc008fc353fecde9bdba3190c17b Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 15:36:23 -0700 Subject: [PATCH 05/18] now with esbuild outputs --- packages/annotated-json/package.json | 3 +++ packages/json-validator/package.json | 4 ++++ packages/mapped-string/package.json | 3 +++ packages/mapped-string/tsconfig.json | 8 ++++++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index 968dbb57..7d112f62 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -11,6 +11,9 @@ "tsconfig": "*", "typescript": "^5.4.2" }, + "scripts": { + "build": "tsx build.ts" + }, "devDependencies": { } } diff --git a/packages/json-validator/package.json b/packages/json-validator/package.json index 33cc813e..c4d0c123 100644 --- a/packages/json-validator/package.json +++ b/packages/json-validator/package.json @@ -7,11 +7,15 @@ "types": "./src/index.ts", "dependencies": { "tsconfig": "*", + "build": "*", "typescript": "^5.4.2", "mapped-string": "*", "tidyverse-errors": "*", "annotated-json": "*" }, + "scripts": { + "build": "tsx build.ts" + }, "devDependencies": { } } diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index 41587cd2..ca7f68d7 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -5,6 +5,9 @@ "license": "MIT", "main": "./src/index.ts", "types": "./src/index.ts", + "scripts": { + "build": "tsc" + }, "dependencies": { "tsconfig": "*", "typescript": "^5.4.2", diff --git a/packages/mapped-string/tsconfig.json b/packages/mapped-string/tsconfig.json index aad794c2..6682bf9a 100644 --- a/packages/mapped-string/tsconfig.json +++ b/packages/mapped-string/tsconfig.json @@ -1,5 +1,9 @@ { - "exclude": ["node_modules"], + "exclude": ["node_modules", "dist"], "extends": "tsconfig/base.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "outDir": "./dist", + }, } From eef49249621cc4880ee12a1123667999afe0f1b1 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 15:52:41 -0700 Subject: [PATCH 06/18] now with more exports --- packages/json-validator/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/json-validator/src/index.ts b/packages/json-validator/src/index.ts index 0e9b6092..eb350ec7 100644 --- a/packages/json-validator/src/index.ts +++ b/packages/json-validator/src/index.ts @@ -22,4 +22,7 @@ * SOFTWARE. */ -export * from "./validator"; \ No newline at end of file +export * from "./validator"; +export * from "./types"; +export * from "./schema"; +export { initState, setInitializer } from "./state"; \ No newline at end of file From 61f2443e5a85a5dee1a7a35f2432941e3a50bd6f Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 13 May 2024 15:55:58 -0700 Subject: [PATCH 07/18] more --- packages/json-validator/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/json-validator/src/index.ts b/packages/json-validator/src/index.ts index eb350ec7..294f41ac 100644 --- a/packages/json-validator/src/index.ts +++ b/packages/json-validator/src/index.ts @@ -25,4 +25,5 @@ export * from "./validator"; export * from "./types"; export * from "./schema"; -export { initState, setInitializer } from "./state"; \ No newline at end of file +export { initState, setInitializer } from "./state"; +export { asMappedString } from "mapped-string"; \ No newline at end of file From 33324c9b441ff2fddb3fd39d8b5a29b2ae6c4853 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 10:31:46 -0400 Subject: [PATCH 08/18] make 0.1.1 a public version at @quarto/tidyverse-errors --- packages/tidyverse-errors/package.json | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/tidyverse-errors/package.json b/packages/tidyverse-errors/package.json index b8021154..cd17f9cf 100644 --- a/packages/tidyverse-errors/package.json +++ b/packages/tidyverse-errors/package.json @@ -1,7 +1,18 @@ { - "name": "tidyverse-errors", - "version": "0.1.0", - "private": true, + "name": "@quarto/tidyverse-errors", + "version": "0.1.1", + "description": "Format errors using the tidyverse style.", + "author": { + "name": "Posit PBC" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/quarto-dev/quarto.git" + }, "license": "MIT", "main": "./src/index.ts", "types": "./src/index.ts", @@ -12,4 +23,3 @@ }, "devDependencies": { } } - From dfdd60b736557cc2dfe6894be9d853eeacc6f80f Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 10:34:26 -0400 Subject: [PATCH 09/18] make 0.1.0 a public version at @quarto/mapped-string --- packages/mapped-string/package.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index ca7f68d7..2d8a454d 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -1,8 +1,19 @@ { "name": "mapped-string", "version": "0.1.0", - "private": true, + "description": "A string data structure with integrated source maps.", "license": "MIT", + "author": { + "name": "Posit PBC" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/quarto-dev/quarto.git" + }, "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { @@ -12,8 +23,7 @@ "tsconfig": "*", "typescript": "^5.4.2", "ansi-colors": "^4.1.3", - "tidyverse-errors": "*" + "@quarto/tidyverse-errors": "^0.1.1" }, "devDependencies": { } } - From 59b104bef124d231ad00b1401ec7ab5fd2c319d4 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 10:37:32 -0400 Subject: [PATCH 10/18] fix typo --- packages/mapped-string/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index 2d8a454d..43a1f806 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -1,5 +1,5 @@ { - "name": "mapped-string", + "name": "@quarto/mapped-string", "version": "0.1.0", "description": "A string data structure with integrated source maps.", "license": "MIT", From d6ad833dd58502af4cfb63cc716c33ad19f5967b Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 10:40:33 -0400 Subject: [PATCH 11/18] make 0.1.0 a public version at @quarto/annotated-json --- packages/annotated-json/package.json | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index 7d112f62..9e0396ff 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -1,13 +1,24 @@ { - "name": "annotated-json", + "name": "@quarto/annotated-json", "version": "0.1.0", - "private": true, + "description": "A data structure for storing and manipulation a JSON object together with source locations of its constituent parts.", + "author": { + "name": "Posit PBC" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/quarto-dev/quarto.git" + }, "license": "MIT", "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { - "mapped-string": "*", - "tidyverse-errors": "*", + "@quarto/mapped-string": "^0.1.0", + "@quarto/tidyverse-errors": "^0.1.1", "tsconfig": "*", "typescript": "^5.4.2" }, @@ -16,4 +27,3 @@ }, "devDependencies": { } } - From b55d0c1e17c62cbf836aef513451cf2901c050a5 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 10:42:58 -0400 Subject: [PATCH 12/18] make 0.1.0 a public version at @quarto/json-validator --- packages/json-validator/package.json | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/json-validator/package.json b/packages/json-validator/package.json index c4d0c123..251b9d1d 100644 --- a/packages/json-validator/package.json +++ b/packages/json-validator/package.json @@ -1,7 +1,18 @@ { - "name": "json-validator", + "name": "@quarto/json-validator", "version": "0.1.0", - "private": true, + "description": "A validation library for JSON objects with an emphasis on good error messages.", + "author": { + "name": "Posit PBC" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/quarto-dev/quarto.git" + }, "license": "MIT", "main": "./src/index.ts", "types": "./src/index.ts", @@ -9,13 +20,12 @@ "tsconfig": "*", "build": "*", "typescript": "^5.4.2", - "mapped-string": "*", - "tidyverse-errors": "*", - "annotated-json": "*" + "@quarto/mapped-string": "^0.1.0", + "@quarto/tidyverse-errors": "^0.1.1", + "@quarto/annotated-json": "^0.1.0" }, "scripts": { "build": "tsx build.ts" }, "devDependencies": { } } - From 8f4cb31a57cab96beeb95ba13ab748956b898262 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 12:14:37 -0400 Subject: [PATCH 13/18] updates --- packages/annotated-json/package.json | 6 +- packages/annotated-json/src/annotated-yaml.ts | 45 +- packages/annotated-json/src/types.ts | 4 +- packages/json-validator/README.md | 15 + packages/json-validator/package.json | 12 +- packages/json-validator/src/errors.ts | 124 +- .../json-validator/src/external/regexpp.mjs | 2089 ----------------- packages/json-validator/src/index.ts | 2 +- packages/json-validator/src/regexp.js | 2 +- packages/json-validator/src/types.ts | 7 +- packages/json-validator/src/validator.ts | 2 +- packages/json-validator/src/yaml-schema.ts | 2 +- packages/mapped-string/package.json | 4 +- packages/mapped-string/src/error.ts | 4 +- packages/tidyverse-errors/package.json | 2 +- 15 files changed, 107 insertions(+), 2213 deletions(-) create mode 100644 packages/json-validator/README.md delete mode 100644 packages/json-validator/src/external/regexpp.mjs diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index 9e0396ff..e96f992c 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/annotated-json", - "version": "0.1.0", + "version": "0.1.1", "description": "A data structure for storing and manipulation a JSON object together with source locations of its constituent parts.", "author": { "name": "Posit PBC" @@ -17,8 +17,8 @@ "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { - "@quarto/mapped-string": "^0.1.0", - "@quarto/tidyverse-errors": "^0.1.1", + "@quarto/mapped-string": "^0.1.1", + "@quarto/tidyverse-errors": "^0.1.2", "tsconfig": "*", "typescript": "^5.4.2" }, diff --git a/packages/annotated-json/src/annotated-yaml.ts b/packages/annotated-json/src/annotated-yaml.ts index 3504e100..ee04f650 100644 --- a/packages/annotated-json/src/annotated-yaml.ts +++ b/packages/annotated-json/src/annotated-yaml.ts @@ -14,12 +14,12 @@ import { MappedString, createSourceContext, EitherString -} from "mapped-string"; +} from "@quarto/mapped-string"; import { load as jsYamlParse } from "./external/js-yaml.js"; import { QuartoJSONSchema } from "./js-yaml-quarto-schema"; -import { tidyverseInfo } from "tidyverse-errors"; +import { tidyverseInfo } from "@quarto/tidyverse-errors"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type TreeSitterParse = any; @@ -99,7 +99,7 @@ export function readAnnotatedYamlFromMappedString( try { return buildJsYamlAnnotation(mappedSource); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { if (e.name === "YAMLError") { e.name = "YAML Parsing"; @@ -129,15 +129,13 @@ export function readAnnotatedYamlFromMappedString( start: m1.index! + 1, end: m1.index! + m1[0].length, }); - e.stack = `${e.reason} (${filename}, ${line + 1}:${ - column + 1 - })\n${sourceContext}`; + e.stack = `${e.reason} (${filename}, ${line + 1}:${column + 1 + })\n${sourceContext}`; e.message = e.stack; - e.message = `${e.message}\n${ - tidyverseInfo( - "Is it possible you missed a space after a colon in the key-value mapping?", - ) - }`; + e.message = `${e.message}\n${tidyverseInfo( + "Is it possible you missed a space after a colon in the key-value mapping?", + ) + }`; } else { const f = lineColToIndex(mappedSource.value); const location = { line: Number(m[1]) - 1, column: Number(m[2] - 1) }; @@ -150,24 +148,21 @@ export function readAnnotatedYamlFromMappedString( start: offset, end: offset + 1, }); - e.stack = `${e.reason} (${filename}, ${line + 1}:${ - column + 1 - })\n${sourceContext}`; + e.stack = `${e.reason} (${filename}, ${line + 1}:${column + 1 + })\n${sourceContext}`; e.message = e.stack; if ( mappedLines(mappedSource)[location.line].value.indexOf("!expr") !== - -1 && + -1 && e.reason.match(/bad indentation of a mapping entry/) ) { - e.message = `${e.message}\n${ - tidyverseInfo( - "YAML tags like !expr must be followed by YAML strings.", - ) - }\n${ - tidyverseInfo( + e.message = `${e.message}\n${tidyverseInfo( + "YAML tags like !expr must be followed by YAML strings.", + ) + }\n${tidyverseInfo( "Is it possible you need to quote the value you passed to !expr ?", ) - }`; + }`; } } e.stack = ""; @@ -603,7 +598,7 @@ export function locateCursor( kind: keyOrValue!, annotation: innermostAnnotation!, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { if (e.message === kInternalLocateError) { return { @@ -642,7 +637,7 @@ export function locateAnnotation( annotation.components[j].start, annotation.components[j].end, ).trim() === - value + value ) { // on last entry, we discriminate between key and value contexts if (i === position.length - 1) { @@ -707,7 +702,7 @@ export function navigate( return annotation; } else if ( ["sequence", "block_sequence", "flow_sequence"].indexOf(annotation.kind) !== - -1 + -1 ) { const searchKey = Number(path[pathIndex]); if ( diff --git a/packages/annotated-json/src/types.ts b/packages/annotated-json/src/types.ts index 31ae18a8..81127b80 100644 --- a/packages/annotated-json/src/types.ts +++ b/packages/annotated-json/src/types.ts @@ -7,7 +7,7 @@ * */ -import { MappedString } from "mapped-string"; +import { MappedString } from "@quarto/mapped-string"; // https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 export type JSONValue = @@ -30,4 +30,4 @@ export interface AnnotatedParse { components: AnnotatedParse[]; errors?: { start: number; end: number; message: string }[]; // this field is only populated at the top level -} \ No newline at end of file +} diff --git a/packages/json-validator/README.md b/packages/json-validator/README.md new file mode 100644 index 00000000..b80cb8b0 --- /dev/null +++ b/packages/json-validator/README.md @@ -0,0 +1,15 @@ +# json-validator + +`json-validator` is a validation library for JSON objects that emphasizes good error messages. +It is particularly suitable for validating objects and documents serialized in disk. +It can be used to validate, for example, YAML files (which are loaded as JSON objects). + +## Schemas + +This library uses a schema format that is similar but not identical to [JSON Schema](https://json-schema.org/). + +A full description of the differences is TBF but, notably, `json-validator` does not support JSON Schema's `oneOf` schema. +`oneOf` is not _monotonic_: it requires logical negation. +Generalized negation introduces fundamental difficulties for good error message generation. +(Not all of `json-validator`'s schemas are monotonic; forbidding particular keys in objects is a very useful feature, and +`json-validator` allows that.) diff --git a/packages/json-validator/package.json b/packages/json-validator/package.json index 251b9d1d..5c0a03a6 100644 --- a/packages/json-validator/package.json +++ b/packages/json-validator/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/json-validator", - "version": "0.1.0", + "version": "0.1.1", "description": "A validation library for JSON objects with an emphasis on good error messages.", "author": { "name": "Posit PBC" @@ -20,12 +20,10 @@ "tsconfig": "*", "build": "*", "typescript": "^5.4.2", - "@quarto/mapped-string": "^0.1.0", - "@quarto/tidyverse-errors": "^0.1.1", - "@quarto/annotated-json": "^0.1.0" - }, - "scripts": { - "build": "tsx build.ts" + "regexpp": "^3.2.0", + "@quarto/mapped-string": "^0.1.1", + "@quarto/tidyverse-errors": "^0.1.2", + "@quarto/annotated-json": "^0.1.1" }, "devDependencies": { } } diff --git a/packages/json-validator/src/errors.ts b/packages/json-validator/src/errors.ts index 971d03c5..a18681c7 100644 --- a/packages/json-validator/src/errors.ts +++ b/packages/json-validator/src/errors.ts @@ -13,19 +13,17 @@ import { YAMLSchemaT } from "./types"; import { quotedStringColor, TidyverseError } from "tidyverse-errors"; import { + editDistance, // this truly needs to be in a separate package mappedIndexToLineCol, MappedString, mappedString, Range, - ErrorLocation -} from "mapped-string"; + ErrorLocation, formatLineRange, lines +} from "@quarto/mapped-string"; import { possibleSchemaKeys, possibleSchemaValues } from "./schema-utils"; -// this truly needs to be in a separate package -import { editDistance } from "mapped-string"; - -import { AnnotatedParse, JSONValue } from "annotated-json"; +import { AnnotatedParse, JSONValue } from "@quarto/annotated-json"; import { InstancePath, @@ -38,9 +36,6 @@ import { schemaType, } from "./types"; -import { formatLineRange, lines } from "mapped-string"; - - //////////////////////////////////////////////////////////////////////////////// export function locationString(loc: ErrorLocation) { @@ -49,14 +44,12 @@ export function locationString(loc: ErrorLocation) { if (start.column === end.column) { return `(line ${start.line + 1}, column ${start.column + 1})`; } else { - return `(line ${start.line + 1}, columns ${start.column + 1}--${ - end.column + 1 - })`; + return `(line ${start.line + 1}, columns ${start.column + 1}--${end.column + 1 + })`; } } else { - return `(line ${start.line + 1}, column ${start.column + 1} through line ${ - end.line + 1 - }, column ${end.column + 1})`; + return `(line ${start.line + 1}, column ${start.column + 1} through line ${end.line + 1 + }, column ${end.column + 1})`; } } @@ -177,7 +170,7 @@ function navigate( return annotation; } else if ( ["sequence", "block_sequence", "flow_sequence"].indexOf(annotation.kind) !== - -1 + -1 ) { const searchKey = Number(path[pathIndex]); if ( @@ -347,28 +340,23 @@ function formatHeadingForValueError( if (empty) { return "YAML value is missing."; } else { - return `YAML value ${verbatimInput} must ${ - schemaDescription(error.schema) - }.`; + return `YAML value ${verbatimInput} must ${schemaDescription(error.schema) + }.`; } case "number": // array if (empty) { - return `Array entry ${lastFragment + 1} is empty but it must instead ${ - schemaDescription(error.schema) - }.`; + return `Array entry ${lastFragment + 1} is empty but it must instead ${schemaDescription(error.schema) + }.`; } else { - return `Array entry ${ - lastFragment + 1 - } with value ${verbatimInput} failed to ${ - schemaDescription(error.schema) - }.`; + return `Array entry ${lastFragment + 1 + } with value ${verbatimInput} failed to ${schemaDescription(error.schema) + }.`; } case "string": { // object const formatLastFragment = '"' + colors.blue(lastFragment) + '"'; if (empty) { - return `Field ${formatLastFragment} has empty value but it must instead ${ - schemaDescription(error.schema) - }`; + return `Field ${formatLastFragment} has empty value but it must instead ${schemaDescription(error.schema) + }`; } else { if (verbatimInput.indexOf("\n") !== -1) { return `Field ${formatLastFragment} has value @@ -377,9 +365,8 @@ ${verbatimInput} The value must instead ${schemaDescription(error.schema)}.`; } else { - return `Field ${formatLastFragment} has value ${verbatimInput}, which must instead ${ - schemaDescription(error.schema) - }`; + return `Field ${formatLastFragment} has value ${verbatimInput}, which must instead ${schemaDescription(error.schema) + }`; } } } @@ -541,11 +528,10 @@ ${reindented} schema, ), error: [ - `${subject}is of type ${ - goodType( - error.violatingObject - .result, - ) + `${subject}is of type ${goodType( + error.violatingObject + .result, + ) }.`, ], info: {}, @@ -636,11 +622,10 @@ function checkForBadColon( const errorMessage = `The value ${verbatimInput} is a string.`; const suggestion1 = `In YAML, key-value pairs in objects must be separated by a space.`; - const suggestion2 = `Did you mean ${ - quotedStringColor( - quotedStringColor(getVerbatimInput(error)).replace(/:/g, ": "), - ) - } instead?`; + const suggestion2 = `Did you mean ${quotedStringColor( + quotedStringColor(getVerbatimInput(error)).replace(/:/g, ": "), + ) + } instead?`; const newError: TidyverseError = { heading: formatHeadingForValueError(error, parse, schema), error: [errorMessage], @@ -684,11 +669,10 @@ function checkForBadEquals( const errorMessage = `The value ${verbatimInput} is a string.`; const suggestion1 = `In YAML, key-value pairs in objects must be separated by a colon and a space.`; - const suggestion2 = `Did you mean ${ - quotedStringColor( - quotedStringColor(getVerbatimInput(error)).replace(/ *= */g, ": "), - ) - } instead?`; + const suggestion2 = `Did you mean ${quotedStringColor( + quotedStringColor(getVerbatimInput(error)).replace(/ *= */g, ": "), + ) + } instead?`; const newError: TidyverseError = { heading: formatHeadingForValueError(error, parse, schema), error: [errorMessage], @@ -785,7 +769,7 @@ function checkForNearbyRequired( } } }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars }, (_) => { throw new Error("required error on a non-object schema"); }); @@ -816,20 +800,16 @@ function checkForNearbyRequired( const suggestions = bestCorrection!.map((s: string) => colors.blue(s)); if (suggestions.length === 1) { - error.niceError.info[`did-you-mean-key`] = `Is ${ - suggestions[0] - } a typo of ${colors.blue(missingKey)}?`; + error.niceError.info[`did-you-mean-key`] = `Is ${suggestions[0] + } a typo of ${colors.blue(missingKey)}?`; } else if (suggestions.length === 2) { - error.niceError.info[`did-you-mean-key`] = `Is ${suggestions[0]} or ${ - suggestions[1] - } a typo of ${colors.blue(missingKey)}?`; + error.niceError.info[`did-you-mean-key`] = `Is ${suggestions[0]} or ${suggestions[1] + } a typo of ${colors.blue(missingKey)}?`; } else { - suggestions[suggestions.length - 1] = `or ${ - suggestions[suggestions.length - 1] - }`; - error.niceError.info[`did-you-mean-key`] = `Is one of ${ - suggestions.join(", ") - } a typo of ${colors.blue(missingKey)}?`; + suggestions[suggestions.length - 1] = `or ${suggestions[suggestions.length - 1] + }`; + error.niceError.info[`did-you-mean-key`] = `Is one of ${suggestions.join(", ") + } a typo of ${colors.blue(missingKey)}?`; } } @@ -893,20 +873,16 @@ function checkForNearbyCorrection( const suggestions = bestCorrection!.map((s: string) => colors.blue(s)); if (suggestions.length === 1) { - error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ - suggestions[0] - }?`; + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${suggestions[0] + }?`; } else if (suggestions.length === 2) { - error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ - suggestions[0] - } or ${suggestions[1]}?`; + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${suggestions[0] + } or ${suggestions[1]}?`; } else { - suggestions[suggestions.length - 1] = `or ${ - suggestions[suggestions.length - 1] - }`; - error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${ - suggestions.join(", ") - }?`; + suggestions[suggestions.length - 1] = `or ${suggestions[suggestions.length - 1] + }`; + error.niceError.info[`did-you-mean-${keyOrValue}`] = `Did you mean ${suggestions.join(", ") + }?`; } return error; @@ -994,7 +970,7 @@ export function createSourceContext( contextLines.push(content); contextLines.push( " ".repeat(prefixWidth + startColumn - 1) + - "~".repeat(endColumn - startColumn + 1), + "~".repeat(endColumn - startColumn + 1), ); } } diff --git a/packages/json-validator/src/external/regexpp.mjs b/packages/json-validator/src/external/regexpp.mjs deleted file mode 100644 index c8bae6c4..00000000 --- a/packages/json-validator/src/external/regexpp.mjs +++ /dev/null @@ -1,2089 +0,0 @@ -/*! @author Toru Nagashima */ - - -var ast = /*#__PURE__*/Object.freeze({ - __proto__: null -}); - -let largeIdStartRanges = undefined; -let largeIdContinueRanges = undefined; -function isIdStart(cp) { - if (cp < 0x41) - return false; - if (cp < 0x5b) - return true; - if (cp < 0x61) - return false; - if (cp < 0x7b) - return true; - return isLargeIdStart(cp); -} -function isIdContinue(cp) { - if (cp < 0x30) - return false; - if (cp < 0x3a) - return true; - if (cp < 0x41) - return false; - if (cp < 0x5b) - return true; - if (cp === 0x5f) - return true; - if (cp < 0x61) - return false; - if (cp < 0x7b) - return true; - return isLargeIdStart(cp) || isLargeIdContinue(cp); -} -function isLargeIdStart(cp) { - return isInRange(cp, largeIdStartRanges || (largeIdStartRanges = initLargeIdStartRanges())); -} -function isLargeIdContinue(cp) { - return isInRange(cp, largeIdContinueRanges || - (largeIdContinueRanges = initLargeIdContinueRanges())); -} -function initLargeIdStartRanges() { - return restoreRanges("4q 0 b 0 5 0 6 m 2 u 2 cp 5 b f 4 8 0 2 0 3m 4 2 1 3 3 2 0 7 0 2 2 2 0 2 j 2 2a 2 3u 9 4l 2 11 3 0 7 14 20 q 5 3 1a 16 10 1 2 2q 2 0 g 1 8 1 b 2 3 0 h 0 2 t u 2g c 0 p w a 1 5 0 6 l 5 0 a 0 4 0 o o 8 a 1i k 2 h 1p 1h 4 0 j 0 8 9 g f 5 7 3 1 3 l 2 6 2 0 4 3 4 0 h 0 e 1 2 2 f 1 b 0 9 5 5 1 3 l 2 6 2 1 2 1 2 1 w 3 2 0 k 2 h 8 2 2 2 l 2 6 2 1 2 4 4 0 j 0 g 1 o 0 c 7 3 1 3 l 2 6 2 1 2 4 4 0 v 1 2 2 g 0 i 0 2 5 4 2 2 3 4 1 2 0 2 1 4 1 4 2 4 b n 0 1h 7 2 2 2 m 2 f 4 0 r 2 6 1 v 0 5 7 2 2 2 m 2 9 2 4 4 0 x 0 2 1 g 1 i 8 2 2 2 14 3 0 h 0 6 2 9 2 p 5 6 h 4 n 2 8 2 0 3 6 1n 1b 2 1 d 6 1n 1 2 0 2 4 2 n 2 0 2 9 2 1 a 0 3 4 2 0 m 3 x 0 1s 7 2 z s 4 38 16 l 0 h 5 5 3 4 0 4 1 8 2 5 c d 0 i 11 2 0 6 0 3 16 2 98 2 3 3 6 2 0 2 3 3 14 2 3 3 w 2 3 3 6 2 0 2 3 3 e 2 1k 2 3 3 1u 12 f h 2d 3 5 4 h7 3 g 2 p 6 22 4 a 8 c 2 3 f h f h f c 2 2 g 1f 10 0 5 0 1w 2g 8 14 2 0 6 1x b u 1e t 3 4 c 17 5 p 1j m a 1g 2b 0 2m 1a i 6 1k t e 1 b 17 r z 16 2 b z 3 8 8 16 3 2 16 3 2 5 2 1 4 0 6 5b 1t 7p 3 5 3 11 3 5 3 7 2 0 2 0 2 0 2 u 3 1g 2 6 2 0 4 2 2 6 4 3 3 5 5 c 6 2 2 6 39 0 e 0 h c 2u 0 5 0 3 9 2 0 3 5 7 0 2 0 2 0 2 f 3 3 6 4 5 0 i 14 22g 1a 2 1a 2 3o 7 3 4 1 d 11 2 0 6 0 3 1j 8 0 h m a 6 2 6 2 6 2 6 2 6 2 6 2 6 2 6 fb 2 q 8 8 4 3 4 5 2d 5 4 2 2h 2 3 6 16 2 2l i v 1d f e9 533 1t g70 4 wc 1w 19 3 7g 4 f b 1 l 1a h u 3 27 14 8 3 2u 3 1g 3 8 17 c 2 2 2 3 2 m u 1f f 1d 1r 5 4 0 2 1 c r b m q s 8 1a t 0 h 4 2 9 b 4 2 14 o 2 2 7 l m 4 0 4 1d 2 0 4 1 3 4 3 0 2 0 p 2 3 a 8 2 d 5 3 5 3 5 a 6 2 6 2 16 2 d 7 36 u 8mb d m 5 1c 6it a5 3 2x 13 6 d 4 6 0 2 9 2 c 2 4 2 0 2 1 2 1 2 2z y a2 j 1r 3 1h 15 b 39 4 2 3q 11 p 7 p c 2g 4 5 3 5 3 5 3 2 10 b 2 p 2 i 2 1 2 e 3 d z 3e 1y 1g 7g s 4 1c 1c v e t 6 11 b t 3 z 5 7 2 4 17 4d j z 5 z 5 13 9 1f 4d 8m a l b 7 49 5 3 0 2 17 2 1 4 0 3 m b m a u 1u i 2 1 b l b p 1z 1j 7 1 1t 0 g 3 2 2 2 s 17 s 4 s 10 7 2 r s 1h b l b i e h 33 20 1k 1e e 1e e z 9p 15 7 1 27 s b 0 9 l 2z k s m d 1g 24 18 x o r z u 0 3 0 9 y 4 0 d 1b f 3 m 0 2 0 10 h 2 o 2d 6 2 0 2 3 2 e 2 9 8 1a 13 7 3 1 3 l 2 6 2 1 2 4 4 0 j 0 d 4 4f 1g j 3 l 2 v 1b l 1 2 0 55 1a 16 3 11 1b l 0 1o 16 e 0 20 q 6e 17 39 1r w 7 3 0 3 7 2 1 2 n g 0 2 0 2n 7 3 12 h 0 2 0 t 0 b 13 8 0 m 0 c 19 k 0 z 1k 7c 8 2 10 i 0 1e t 35 6 2 1 2 11 m 0 q 5 2 1 2 v f 0 94 i 5a 0 28 pl 2v 32 i 5f 24d tq 34i g6 6nu fs 8 u 36 t j 1b h 3 w k 6 i j5 1r 3l 22 6 0 1v c 1t 1 2 0 t 4qf 9 yd 17 8 6wo 7y 1e 2 i 3 9 az 1s5 2y 6 c 4 8 8 9 4mf 2c 2 1y 2 1 3 0 3 1 3 3 2 b 2 0 2 6 2 1s 2 3 3 7 2 6 2 r 2 3 2 4 2 0 4 6 2 9f 3 o 2 o 2 u 2 o 2 u 2 o 2 u 2 o 2 u 2 o 2 7 1th 18 b 6 h 0 aa 17 105 5g 1o 1v 8 0 xh 3 2 q 2 1 2 0 3 0 2 9 2 3 2 0 2 0 7 0 5 0 2 0 2 0 2 2 2 1 2 0 3 0 2 0 2 0 2 0 2 0 2 1 2 0 3 3 2 6 2 3 2 3 2 0 2 9 2 g 6 2 2 4 2 g 3et wyl z 378 c 65 3 4g1 f 5rk 2e8 f1 15v 3t6"); -} -function initLargeIdContinueRanges() { - return restoreRanges("53 0 g9 33 o 0 70 4 7e 18 2 0 2 1 2 1 2 0 21 a 1d u 7 0 2u 6 3 5 3 1 2 3 3 9 o 0 v q 2k a g 9 y 8 a 0 p 3 2 8 2 2 2 4 18 2 3c e 2 w 1j 2 2 h 2 6 b 1 3 9 i 2 1l 0 2 6 3 1 3 2 a 0 b 1 3 9 f 0 3 2 1l 0 2 4 5 1 3 2 4 0 l b 4 0 c 2 1l 0 2 7 2 2 2 2 l 1 3 9 b 5 2 2 1l 0 2 6 3 1 3 2 8 2 b 1 3 9 j 0 1o 4 4 2 2 3 a 0 f 9 h 4 1m 6 2 2 2 3 8 1 c 1 3 9 i 2 1l 0 2 6 2 2 2 3 8 1 c 1 3 9 h 3 1k 1 2 6 2 2 2 3 a 0 b 1 3 9 i 2 1z 0 5 5 2 0 2 7 7 9 3 1 1q 0 3 6 d 7 2 9 2g 0 3 8 c 5 3 9 1r 1 7 9 c 0 2 0 2 0 5 1 1e j 2 1 6 a 2 z a 0 2t j 2 9 d 3 5 2 2 2 3 6 4 3 e b 2 e jk 2 a 8 pt 2 u 2 u 1 v 1 1t v a 0 3 9 y 2 3 9 40 0 3b b 5 b b 9 3l a 1p 4 1m 9 2 s 3 a 7 9 n d 2 1 1s 4 1c g c 9 i 8 d 2 v c 3 9 19 d 1d j 9 9 7 9 3b 2 2 k 5 0 7 0 3 2 5j 1l 2 4 g0 1 k 0 3g c 5 0 4 b 2db 2 3y 0 2p v ff 5 2y 1 n7q 9 1y 0 5 9 x 1 29 1 7l 0 4 0 5 0 o 4 5 0 2c 1 1f h b 9 7 h e a t 7 q c 19 3 1c d g 9 c 0 b 9 1c d d 0 9 1 3 9 y 2 1f 0 2 2 3 1 6 1 2 0 16 4 6 1 6l 7 2 1 3 9 fmt 0 ki f h f 4 1 p 2 5d 9 12 0 ji 0 6b 0 46 4 86 9 120 2 2 1 6 3 15 2 5 0 4m 1 fy 3 9 9 aa 1 4a a 4w 2 1i e w 9 g 3 1a a 1i 9 7 2 11 d 2 9 6 1 19 0 d 2 1d d 9 3 2 b 2b b 7 0 4h b 6 9 7 3 1k 1 2 6 3 1 3 2 a 0 b 1 3 6 4 4 5d h a 9 5 0 2a j d 9 5y 6 3 8 s 1 2b g g 9 2a c 9 9 2c e 5 9 6r e 4m 9 1z 5 2 1 3 3 2 0 2 1 d 9 3c 6 3 6 4 0 t 9 15 6 2 3 9 0 a a 1b f ba 7 2 7 h 9 1l l 2 d 3f 5 4 0 2 1 2 6 2 0 9 9 1d 4 2 1 2 4 9 9 96 3 ewa 9 3r 4 1o 6 q 9 s6 0 2 1i 8 3 2a 0 c 1 f58 1 43r 4 4 5 9 7 3 6 v 3 45 2 13e 1d e9 1i 5 1d 9 0 f 0 n 4 2 e 11t 6 2 g 3 6 2 1 2 4 7a 6 a 9 bn d 15j 6 32 6 6 9 3o7 9 gvt3 6n"); -} -function isInRange(cp, ranges) { - let l = 0, r = (ranges.length / 2) | 0, i = 0, min = 0, max = 0; - while (l < r) { - i = ((l + r) / 2) | 0; - min = ranges[2 * i]; - max = ranges[2 * i + 1]; - if (cp < min) { - r = i; - } - else if (cp > max) { - l = i + 1; - } - else { - return true; - } - } - return false; -} -function restoreRanges(data) { - let last = 0; - return data.split(" ").map(s => (last += parseInt(s, 36) | 0)); -} - -class DataSet { - constructor(raw2018, raw2019, raw2020, raw2021) { - this._raw2018 = raw2018; - this._raw2019 = raw2019; - this._raw2020 = raw2020; - this._raw2021 = raw2021; - } - get es2018() { - return (this._set2018 || (this._set2018 = new Set(this._raw2018.split(" ")))); - } - get es2019() { - return (this._set2019 || (this._set2019 = new Set(this._raw2019.split(" ")))); - } - get es2020() { - return (this._set2020 || (this._set2020 = new Set(this._raw2020.split(" ")))); - } - get es2021() { - return (this._set2021 || (this._set2021 = new Set(this._raw2021.split(" ")))); - } -} -const gcNameSet = new Set(["General_Category", "gc"]); -const scNameSet = new Set(["Script", "Script_Extensions", "sc", "scx"]); -const gcValueSets = new DataSet("C Cased_Letter Cc Cf Close_Punctuation Cn Co Combining_Mark Connector_Punctuation Control Cs Currency_Symbol Dash_Punctuation Decimal_Number Enclosing_Mark Final_Punctuation Format Initial_Punctuation L LC Letter Letter_Number Line_Separator Ll Lm Lo Lowercase_Letter Lt Lu M Mark Math_Symbol Mc Me Mn Modifier_Letter Modifier_Symbol N Nd Nl No Nonspacing_Mark Number Open_Punctuation Other Other_Letter Other_Number Other_Punctuation Other_Symbol P Paragraph_Separator Pc Pd Pe Pf Pi Po Private_Use Ps Punctuation S Sc Separator Sk Sm So Space_Separator Spacing_Mark Surrogate Symbol Titlecase_Letter Unassigned Uppercase_Letter Z Zl Zp Zs cntrl digit punct", "", "", ""); -const scValueSets = new DataSet("Adlam Adlm Aghb Ahom Anatolian_Hieroglyphs Arab Arabic Armenian Armi Armn Avestan Avst Bali Balinese Bamu Bamum Bass Bassa_Vah Batak Batk Beng Bengali Bhaiksuki Bhks Bopo Bopomofo Brah Brahmi Brai Braille Bugi Buginese Buhd Buhid Cakm Canadian_Aboriginal Cans Cari Carian Caucasian_Albanian Chakma Cham Cher Cherokee Common Copt Coptic Cprt Cuneiform Cypriot Cyrillic Cyrl Deseret Deva Devanagari Dsrt Dupl Duployan Egyp Egyptian_Hieroglyphs Elba Elbasan Ethi Ethiopic Geor Georgian Glag Glagolitic Gonm Goth Gothic Gran Grantha Greek Grek Gujarati Gujr Gurmukhi Guru Han Hang Hangul Hani Hano Hanunoo Hatr Hatran Hebr Hebrew Hira Hiragana Hluw Hmng Hung Imperial_Aramaic Inherited Inscriptional_Pahlavi Inscriptional_Parthian Ital Java Javanese Kaithi Kali Kana Kannada Katakana Kayah_Li Khar Kharoshthi Khmer Khmr Khoj Khojki Khudawadi Knda Kthi Lana Lao Laoo Latin Latn Lepc Lepcha Limb Limbu Lina Linb Linear_A Linear_B Lisu Lyci Lycian Lydi Lydian Mahajani Mahj Malayalam Mand Mandaic Mani Manichaean Marc Marchen Masaram_Gondi Meetei_Mayek Mend Mende_Kikakui Merc Mero Meroitic_Cursive Meroitic_Hieroglyphs Miao Mlym Modi Mong Mongolian Mro Mroo Mtei Mult Multani Myanmar Mymr Nabataean Narb Nbat New_Tai_Lue Newa Nko Nkoo Nshu Nushu Ogam Ogham Ol_Chiki Olck Old_Hungarian Old_Italic Old_North_Arabian Old_Permic Old_Persian Old_South_Arabian Old_Turkic Oriya Orkh Orya Osage Osge Osma Osmanya Pahawh_Hmong Palm Palmyrene Pau_Cin_Hau Pauc Perm Phag Phags_Pa Phli Phlp Phnx Phoenician Plrd Prti Psalter_Pahlavi Qaac Qaai Rejang Rjng Runic Runr Samaritan Samr Sarb Saur Saurashtra Sgnw Sharada Shavian Shaw Shrd Sidd Siddham SignWriting Sind Sinh Sinhala Sora Sora_Sompeng Soyo Soyombo Sund Sundanese Sylo Syloti_Nagri Syrc Syriac Tagalog Tagb Tagbanwa Tai_Le Tai_Tham Tai_Viet Takr Takri Tale Talu Tamil Taml Tang Tangut Tavt Telu Telugu Tfng Tglg Thaa Thaana Thai Tibetan Tibt Tifinagh Tirh Tirhuta Ugar Ugaritic Vai Vaii Wara Warang_Citi Xpeo Xsux Yi Yiii Zanabazar_Square Zanb Zinh Zyyy", "Dogr Dogra Gong Gunjala_Gondi Hanifi_Rohingya Maka Makasar Medefaidrin Medf Old_Sogdian Rohg Sogd Sogdian Sogo", "Elym Elymaic Hmnp Nand Nandinagari Nyiakeng_Puachue_Hmong Wancho Wcho", "Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi"); -const binPropertySets = new DataSet("AHex ASCII ASCII_Hex_Digit Alpha Alphabetic Any Assigned Bidi_C Bidi_Control Bidi_M Bidi_Mirrored CI CWCF CWCM CWKCF CWL CWT CWU Case_Ignorable Cased Changes_When_Casefolded Changes_When_Casemapped Changes_When_Lowercased Changes_When_NFKC_Casefolded Changes_When_Titlecased Changes_When_Uppercased DI Dash Default_Ignorable_Code_Point Dep Deprecated Dia Diacritic Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Ext Extender Gr_Base Gr_Ext Grapheme_Base Grapheme_Extend Hex Hex_Digit IDC IDS IDSB IDST IDS_Binary_Operator IDS_Trinary_Operator ID_Continue ID_Start Ideo Ideographic Join_C Join_Control LOE Logical_Order_Exception Lower Lowercase Math NChar Noncharacter_Code_Point Pat_Syn Pat_WS Pattern_Syntax Pattern_White_Space QMark Quotation_Mark RI Radical Regional_Indicator SD STerm Sentence_Terminal Soft_Dotted Term Terminal_Punctuation UIdeo Unified_Ideograph Upper Uppercase VS Variation_Selector White_Space XIDC XIDS XID_Continue XID_Start space", "Extended_Pictographic", "", "EBase EComp EMod EPres ExtPict"); -function isValidUnicodeProperty(version, name, value) { - if (gcNameSet.has(name)) { - return version >= 2018 && gcValueSets.es2018.has(value); - } - if (scNameSet.has(name)) { - return ((version >= 2018 && scValueSets.es2018.has(value)) || - (version >= 2019 && scValueSets.es2019.has(value)) || - (version >= 2020 && scValueSets.es2020.has(value)) || - (version >= 2021 && scValueSets.es2021.has(value))); - } - return false; -} -function isValidLoneUnicodeProperty(version, value) { - return ((version >= 2018 && binPropertySets.es2018.has(value)) || - (version >= 2019 && binPropertySets.es2019.has(value)) || - (version >= 2021 && binPropertySets.es2021.has(value))); -} - -const Backspace = 0x08; -const CharacterTabulation = 0x09; -const LineFeed = 0x0a; -const LineTabulation = 0x0b; -const FormFeed = 0x0c; -const CarriageReturn = 0x0d; -const ExclamationMark = 0x21; -const DollarSign = 0x24; -const LeftParenthesis = 0x28; -const RightParenthesis = 0x29; -const Asterisk = 0x2a; -const PlusSign = 0x2b; -const Comma = 0x2c; -const HyphenMinus = 0x2d; -const FullStop = 0x2e; -const Solidus = 0x2f; -const DigitZero = 0x30; -const DigitOne = 0x31; -const DigitSeven = 0x37; -const DigitNine = 0x39; -const Colon = 0x3a; -const LessThanSign = 0x3c; -const EqualsSign = 0x3d; -const GreaterThanSign = 0x3e; -const QuestionMark = 0x3f; -const LatinCapitalLetterA = 0x41; -const LatinCapitalLetterB = 0x42; -const LatinCapitalLetterD = 0x44; -const LatinCapitalLetterF = 0x46; -const LatinCapitalLetterP = 0x50; -const LatinCapitalLetterS = 0x53; -const LatinCapitalLetterW = 0x57; -const LatinCapitalLetterZ = 0x5a; -const LowLine = 0x5f; -const LatinSmallLetterA = 0x61; -const LatinSmallLetterB = 0x62; -const LatinSmallLetterC = 0x63; -const LatinSmallLetterD = 0x64; -const LatinSmallLetterF = 0x66; -const LatinSmallLetterG = 0x67; -const LatinSmallLetterI = 0x69; -const LatinSmallLetterK = 0x6b; -const LatinSmallLetterM = 0x6d; -const LatinSmallLetterN = 0x6e; -const LatinSmallLetterP = 0x70; -const LatinSmallLetterR = 0x72; -const LatinSmallLetterS = 0x73; -const LatinSmallLetterT = 0x74; -const LatinSmallLetterU = 0x75; -const LatinSmallLetterV = 0x76; -const LatinSmallLetterW = 0x77; -const LatinSmallLetterX = 0x78; -const LatinSmallLetterY = 0x79; -const LatinSmallLetterZ = 0x7a; -const LeftSquareBracket = 0x5b; -const ReverseSolidus = 0x5c; -const RightSquareBracket = 0x5d; -const CircumflexAccent = 0x5e; -const LeftCurlyBracket = 0x7b; -const VerticalLine = 0x7c; -const RightCurlyBracket = 0x7d; -const ZeroWidthNonJoiner = 0x200c; -const ZeroWidthJoiner = 0x200d; -const LineSeparator = 0x2028; -const ParagraphSeparator = 0x2029; -const MinCodePoint = 0x00; -const MaxCodePoint = 0x10ffff; -function isLatinLetter(code) { - return ((code >= LatinCapitalLetterA && code <= LatinCapitalLetterZ) || - (code >= LatinSmallLetterA && code <= LatinSmallLetterZ)); -} -function isDecimalDigit(code) { - return code >= DigitZero && code <= DigitNine; -} -function isOctalDigit(code) { - return code >= DigitZero && code <= DigitSeven; -} -function isHexDigit(code) { - return ((code >= DigitZero && code <= DigitNine) || - (code >= LatinCapitalLetterA && code <= LatinCapitalLetterF) || - (code >= LatinSmallLetterA && code <= LatinSmallLetterF)); -} -function isLineTerminator(code) { - return (code === LineFeed || - code === CarriageReturn || - code === LineSeparator || - code === ParagraphSeparator); -} -function isValidUnicode(code) { - return code >= MinCodePoint && code <= MaxCodePoint; -} -function digitToInt(code) { - if (code >= LatinSmallLetterA && code <= LatinSmallLetterF) { - return code - LatinSmallLetterA + 10; - } - if (code >= LatinCapitalLetterA && code <= LatinCapitalLetterF) { - return code - LatinCapitalLetterA + 10; - } - return code - DigitZero; -} -function isLeadSurrogate(code) { - return code >= 0xd800 && code <= 0xdbff; -} -function isTrailSurrogate(code) { - return code >= 0xdc00 && code <= 0xdfff; -} -function combineSurrogatePair(lead, trail) { - return (lead - 0xd800) * 0x400 + (trail - 0xdc00) + 0x10000; -} - -const legacyImpl = { - at(s, end, i) { - return i < end ? s.charCodeAt(i) : -1; - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - width(c) { - return 1; - }, -}; -const unicodeImpl = { - at(s, end, i) { - return i < end ? s.codePointAt(i) : -1; - }, - width(c) { - return c > 0xffff ? 2 : 1; - }, -}; -class Reader { - constructor() { - this._impl = legacyImpl; - this._s = ""; - this._i = 0; - this._end = 0; - this._cp1 = -1; - this._w1 = 1; - this._cp2 = -1; - this._w2 = 1; - this._cp3 = -1; - this._w3 = 1; - this._cp4 = -1; - } - get source() { - return this._s; - } - get index() { - return this._i; - } - get currentCodePoint() { - return this._cp1; - } - get nextCodePoint() { - return this._cp2; - } - get nextCodePoint2() { - return this._cp3; - } - get nextCodePoint3() { - return this._cp4; - } - reset(source, start, end, uFlag) { - this._impl = uFlag ? unicodeImpl : legacyImpl; - this._s = source; - this._end = end; - this.rewind(start); - } - rewind(index) { - const impl = this._impl; - this._i = index; - this._cp1 = impl.at(this._s, this._end, index); - this._w1 = impl.width(this._cp1); - this._cp2 = impl.at(this._s, this._end, index + this._w1); - this._w2 = impl.width(this._cp2); - this._cp3 = impl.at(this._s, this._end, index + this._w1 + this._w2); - this._w3 = impl.width(this._cp3); - this._cp4 = impl.at(this._s, this._end, index + this._w1 + this._w2 + this._w3); - } - advance() { - if (this._cp1 !== -1) { - const impl = this._impl; - this._i += this._w1; - this._cp1 = this._cp2; - this._w1 = this._w2; - this._cp2 = this._cp3; - this._w2 = impl.width(this._cp2); - this._cp3 = this._cp4; - this._w3 = impl.width(this._cp3); - this._cp4 = impl.at(this._s, this._end, this._i + this._w1 + this._w2 + this._w3); - } - } - eat(cp) { - if (this._cp1 === cp) { - this.advance(); - return true; - } - return false; - } - eat2(cp1, cp2) { - if (this._cp1 === cp1 && this._cp2 === cp2) { - this.advance(); - this.advance(); - return true; - } - return false; - } - eat3(cp1, cp2, cp3) { - if (this._cp1 === cp1 && this._cp2 === cp2 && this._cp3 === cp3) { - this.advance(); - this.advance(); - this.advance(); - return true; - } - return false; - } -} - -class RegExpSyntaxError extends SyntaxError { - constructor(source, uFlag, index, message) { - if (source) { - if (!source.startsWith("/")) { - source = `/${source}/${uFlag ? "u" : ""}`; - } - source = `: ${source}`; - } - super(`Invalid regular expression${source}: ${message}`); - this.index = index; - } -} - -function isSyntaxCharacter(cp) { - return (cp === CircumflexAccent || - cp === DollarSign || - cp === ReverseSolidus || - cp === FullStop || - cp === Asterisk || - cp === PlusSign || - cp === QuestionMark || - cp === LeftParenthesis || - cp === RightParenthesis || - cp === LeftSquareBracket || - cp === RightSquareBracket || - cp === LeftCurlyBracket || - cp === RightCurlyBracket || - cp === VerticalLine); -} -function isRegExpIdentifierStart(cp) { - return isIdStart(cp) || cp === DollarSign || cp === LowLine; -} -function isRegExpIdentifierPart(cp) { - return (isIdContinue(cp) || - cp === DollarSign || - cp === LowLine || - cp === ZeroWidthNonJoiner || - cp === ZeroWidthJoiner); -} -function isUnicodePropertyNameCharacter(cp) { - return isLatinLetter(cp) || cp === LowLine; -} -function isUnicodePropertyValueCharacter(cp) { - return isUnicodePropertyNameCharacter(cp) || isDecimalDigit(cp); -} -class RegExpValidator { - constructor(options) { - this._reader = new Reader(); - this._uFlag = false; - this._nFlag = false; - this._lastIntValue = 0; - this._lastMinValue = 0; - this._lastMaxValue = 0; - this._lastStrValue = ""; - this._lastKeyValue = ""; - this._lastValValue = ""; - this._lastAssertionIsQuantifiable = false; - this._numCapturingParens = 0; - this._groupNames = new Set(); - this._backreferenceNames = new Set(); - this._options = options || {}; - } - validateLiteral(source, start = 0, end = source.length) { - this._uFlag = this._nFlag = false; - this.reset(source, start, end); - this.onLiteralEnter(start); - if (this.eat(Solidus) && this.eatRegExpBody() && this.eat(Solidus)) { - const flagStart = this.index; - const uFlag = source.includes("u", flagStart); - this.validateFlags(source, flagStart, end); - this.validatePattern(source, start + 1, flagStart - 1, uFlag); - } - else if (start >= end) { - this.raise("Empty"); - } - else { - const c = String.fromCodePoint(this.currentCodePoint); - this.raise(`Unexpected character '${c}'`); - } - this.onLiteralLeave(start, end); - } - validateFlags(source, start = 0, end = source.length) { - const existingFlags = new Set(); - let global = false; - let ignoreCase = false; - let multiline = false; - let sticky = false; - let unicode = false; - let dotAll = false; - let hasIndices = false; - for (let i = start; i < end; ++i) { - const flag = source.charCodeAt(i); - if (existingFlags.has(flag)) { - this.raise(`Duplicated flag '${source[i]}'`); - } - existingFlags.add(flag); - if (flag === LatinSmallLetterG) { - global = true; - } - else if (flag === LatinSmallLetterI) { - ignoreCase = true; - } - else if (flag === LatinSmallLetterM) { - multiline = true; - } - else if (flag === LatinSmallLetterU && this.ecmaVersion >= 2015) { - unicode = true; - } - else if (flag === LatinSmallLetterY && this.ecmaVersion >= 2015) { - sticky = true; - } - else if (flag === LatinSmallLetterS && this.ecmaVersion >= 2018) { - dotAll = true; - } - else if (flag === LatinSmallLetterD && this.ecmaVersion >= 2022) { - hasIndices = true; - } - else { - this.raise(`Invalid flag '${source[i]}'`); - } - } - this.onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices); - } - validatePattern(source, start = 0, end = source.length, uFlag = false) { - this._uFlag = uFlag && this.ecmaVersion >= 2015; - this._nFlag = uFlag && this.ecmaVersion >= 2018; - this.reset(source, start, end); - this.consumePattern(); - if (!this._nFlag && - this.ecmaVersion >= 2018 && - this._groupNames.size > 0) { - this._nFlag = true; - this.rewind(start); - this.consumePattern(); - } - } - get strict() { - return Boolean(this._options.strict || this._uFlag); - } - get ecmaVersion() { - return this._options.ecmaVersion || 2022; - } - onLiteralEnter(start) { - if (this._options.onLiteralEnter) { - this._options.onLiteralEnter(start); - } - } - onLiteralLeave(start, end) { - if (this._options.onLiteralLeave) { - this._options.onLiteralLeave(start, end); - } - } - onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices) { - if (this._options.onFlags) { - this._options.onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices); - } - } - onPatternEnter(start) { - if (this._options.onPatternEnter) { - this._options.onPatternEnter(start); - } - } - onPatternLeave(start, end) { - if (this._options.onPatternLeave) { - this._options.onPatternLeave(start, end); - } - } - onDisjunctionEnter(start) { - if (this._options.onDisjunctionEnter) { - this._options.onDisjunctionEnter(start); - } - } - onDisjunctionLeave(start, end) { - if (this._options.onDisjunctionLeave) { - this._options.onDisjunctionLeave(start, end); - } - } - onAlternativeEnter(start, index) { - if (this._options.onAlternativeEnter) { - this._options.onAlternativeEnter(start, index); - } - } - onAlternativeLeave(start, end, index) { - if (this._options.onAlternativeLeave) { - this._options.onAlternativeLeave(start, end, index); - } - } - onGroupEnter(start) { - if (this._options.onGroupEnter) { - this._options.onGroupEnter(start); - } - } - onGroupLeave(start, end) { - if (this._options.onGroupLeave) { - this._options.onGroupLeave(start, end); - } - } - onCapturingGroupEnter(start, name) { - if (this._options.onCapturingGroupEnter) { - this._options.onCapturingGroupEnter(start, name); - } - } - onCapturingGroupLeave(start, end, name) { - if (this._options.onCapturingGroupLeave) { - this._options.onCapturingGroupLeave(start, end, name); - } - } - onQuantifier(start, end, min, max, greedy) { - if (this._options.onQuantifier) { - this._options.onQuantifier(start, end, min, max, greedy); - } - } - onLookaroundAssertionEnter(start, kind, negate) { - if (this._options.onLookaroundAssertionEnter) { - this._options.onLookaroundAssertionEnter(start, kind, negate); - } - } - onLookaroundAssertionLeave(start, end, kind, negate) { - if (this._options.onLookaroundAssertionLeave) { - this._options.onLookaroundAssertionLeave(start, end, kind, negate); - } - } - onEdgeAssertion(start, end, kind) { - if (this._options.onEdgeAssertion) { - this._options.onEdgeAssertion(start, end, kind); - } - } - onWordBoundaryAssertion(start, end, kind, negate) { - if (this._options.onWordBoundaryAssertion) { - this._options.onWordBoundaryAssertion(start, end, kind, negate); - } - } - onAnyCharacterSet(start, end, kind) { - if (this._options.onAnyCharacterSet) { - this._options.onAnyCharacterSet(start, end, kind); - } - } - onEscapeCharacterSet(start, end, kind, negate) { - if (this._options.onEscapeCharacterSet) { - this._options.onEscapeCharacterSet(start, end, kind, negate); - } - } - onUnicodePropertyCharacterSet(start, end, kind, key, value, negate) { - if (this._options.onUnicodePropertyCharacterSet) { - this._options.onUnicodePropertyCharacterSet(start, end, kind, key, value, negate); - } - } - onCharacter(start, end, value) { - if (this._options.onCharacter) { - this._options.onCharacter(start, end, value); - } - } - onBackreference(start, end, ref) { - if (this._options.onBackreference) { - this._options.onBackreference(start, end, ref); - } - } - onCharacterClassEnter(start, negate) { - if (this._options.onCharacterClassEnter) { - this._options.onCharacterClassEnter(start, negate); - } - } - onCharacterClassLeave(start, end, negate) { - if (this._options.onCharacterClassLeave) { - this._options.onCharacterClassLeave(start, end, negate); - } - } - onCharacterClassRange(start, end, min, max) { - if (this._options.onCharacterClassRange) { - this._options.onCharacterClassRange(start, end, min, max); - } - } - get source() { - return this._reader.source; - } - get index() { - return this._reader.index; - } - get currentCodePoint() { - return this._reader.currentCodePoint; - } - get nextCodePoint() { - return this._reader.nextCodePoint; - } - get nextCodePoint2() { - return this._reader.nextCodePoint2; - } - get nextCodePoint3() { - return this._reader.nextCodePoint3; - } - reset(source, start, end) { - this._reader.reset(source, start, end, this._uFlag); - } - rewind(index) { - this._reader.rewind(index); - } - advance() { - this._reader.advance(); - } - eat(cp) { - return this._reader.eat(cp); - } - eat2(cp1, cp2) { - return this._reader.eat2(cp1, cp2); - } - eat3(cp1, cp2, cp3) { - return this._reader.eat3(cp1, cp2, cp3); - } - raise(message) { - throw new RegExpSyntaxError(this.source, this._uFlag, this.index, message); - } - eatRegExpBody() { - const start = this.index; - let inClass = false; - let escaped = false; - for (;;) { - const cp = this.currentCodePoint; - if (cp === -1 || isLineTerminator(cp)) { - const kind = inClass ? "character class" : "regular expression"; - this.raise(`Unterminated ${kind}`); - } - if (escaped) { - escaped = false; - } - else if (cp === ReverseSolidus) { - escaped = true; - } - else if (cp === LeftSquareBracket) { - inClass = true; - } - else if (cp === RightSquareBracket) { - inClass = false; - } - else if ((cp === Solidus && !inClass) || - (cp === Asterisk && this.index === start)) { - break; - } - this.advance(); - } - return this.index !== start; - } - consumePattern() { - const start = this.index; - this._numCapturingParens = this.countCapturingParens(); - this._groupNames.clear(); - this._backreferenceNames.clear(); - this.onPatternEnter(start); - this.consumeDisjunction(); - const cp = this.currentCodePoint; - if (this.currentCodePoint !== -1) { - if (cp === RightParenthesis) { - this.raise("Unmatched ')'"); - } - if (cp === ReverseSolidus) { - this.raise("\\ at end of pattern"); - } - if (cp === RightSquareBracket || cp === RightCurlyBracket) { - this.raise("Lone quantifier brackets"); - } - const c = String.fromCodePoint(cp); - this.raise(`Unexpected character '${c}'`); - } - for (const name of this._backreferenceNames) { - if (!this._groupNames.has(name)) { - this.raise("Invalid named capture referenced"); - } - } - this.onPatternLeave(start, this.index); - } - countCapturingParens() { - const start = this.index; - let inClass = false; - let escaped = false; - let count = 0; - let cp = 0; - while ((cp = this.currentCodePoint) !== -1) { - if (escaped) { - escaped = false; - } - else if (cp === ReverseSolidus) { - escaped = true; - } - else if (cp === LeftSquareBracket) { - inClass = true; - } - else if (cp === RightSquareBracket) { - inClass = false; - } - else if (cp === LeftParenthesis && - !inClass && - (this.nextCodePoint !== QuestionMark || - (this.nextCodePoint2 === LessThanSign && - this.nextCodePoint3 !== EqualsSign && - this.nextCodePoint3 !== ExclamationMark))) { - count += 1; - } - this.advance(); - } - this.rewind(start); - return count; - } - consumeDisjunction() { - const start = this.index; - let i = 0; - this.onDisjunctionEnter(start); - do { - this.consumeAlternative(i++); - } while (this.eat(VerticalLine)); - if (this.consumeQuantifier(true)) { - this.raise("Nothing to repeat"); - } - if (this.eat(LeftCurlyBracket)) { - this.raise("Lone quantifier brackets"); - } - this.onDisjunctionLeave(start, this.index); - } - consumeAlternative(i) { - const start = this.index; - this.onAlternativeEnter(start, i); - // eslint-disable-next-line no-empty - while (this.currentCodePoint !== -1 && this.consumeTerm()) { - } - this.onAlternativeLeave(start, this.index, i); - } - consumeTerm() { - if (this._uFlag || this.strict) { - return (this.consumeAssertion() || - (this.consumeAtom() && this.consumeOptionalQuantifier())); - } - return ((this.consumeAssertion() && - (!this._lastAssertionIsQuantifiable || - this.consumeOptionalQuantifier())) || - (this.consumeExtendedAtom() && this.consumeOptionalQuantifier())); - } - consumeOptionalQuantifier() { - this.consumeQuantifier(); - return true; - } - consumeAssertion() { - const start = this.index; - this._lastAssertionIsQuantifiable = false; - if (this.eat(CircumflexAccent)) { - this.onEdgeAssertion(start, this.index, "start"); - return true; - } - if (this.eat(DollarSign)) { - this.onEdgeAssertion(start, this.index, "end"); - return true; - } - if (this.eat2(ReverseSolidus, LatinCapitalLetterB)) { - this.onWordBoundaryAssertion(start, this.index, "word", true); - return true; - } - if (this.eat2(ReverseSolidus, LatinSmallLetterB)) { - this.onWordBoundaryAssertion(start, this.index, "word", false); - return true; - } - if (this.eat2(LeftParenthesis, QuestionMark)) { - const lookbehind = this.ecmaVersion >= 2018 && this.eat(LessThanSign); - let negate = false; - if (this.eat(EqualsSign) || (negate = this.eat(ExclamationMark))) { - const kind = lookbehind ? "lookbehind" : "lookahead"; - this.onLookaroundAssertionEnter(start, kind, negate); - this.consumeDisjunction(); - if (!this.eat(RightParenthesis)) { - this.raise("Unterminated group"); - } - this._lastAssertionIsQuantifiable = !lookbehind && !this.strict; - this.onLookaroundAssertionLeave(start, this.index, kind, negate); - return true; - } - this.rewind(start); - } - return false; - } - consumeQuantifier(noConsume = false) { - const start = this.index; - let min = 0; - let max = 0; - let greedy = false; - if (this.eat(Asterisk)) { - min = 0; - max = Number.POSITIVE_INFINITY; - } - else if (this.eat(PlusSign)) { - min = 1; - max = Number.POSITIVE_INFINITY; - } - else if (this.eat(QuestionMark)) { - min = 0; - max = 1; - } - else if (this.eatBracedQuantifier(noConsume)) { - min = this._lastMinValue; - max = this._lastMaxValue; - } - else { - return false; - } - greedy = !this.eat(QuestionMark); - if (!noConsume) { - this.onQuantifier(start, this.index, min, max, greedy); - } - return true; - } - eatBracedQuantifier(noError) { - const start = this.index; - if (this.eat(LeftCurlyBracket)) { - this._lastMinValue = 0; - this._lastMaxValue = Number.POSITIVE_INFINITY; - if (this.eatDecimalDigits()) { - this._lastMinValue = this._lastMaxValue = this._lastIntValue; - if (this.eat(Comma)) { - this._lastMaxValue = this.eatDecimalDigits() - ? this._lastIntValue - : Number.POSITIVE_INFINITY; - } - if (this.eat(RightCurlyBracket)) { - if (!noError && this._lastMaxValue < this._lastMinValue) { - this.raise("numbers out of order in {} quantifier"); - } - return true; - } - } - if (!noError && (this._uFlag || this.strict)) { - this.raise("Incomplete quantifier"); - } - this.rewind(start); - } - return false; - } - consumeAtom() { - return (this.consumePatternCharacter() || - this.consumeDot() || - this.consumeReverseSolidusAtomEscape() || - this.consumeCharacterClass() || - this.consumeUncapturingGroup() || - this.consumeCapturingGroup()); - } - consumeDot() { - if (this.eat(FullStop)) { - this.onAnyCharacterSet(this.index - 1, this.index, "any"); - return true; - } - return false; - } - consumeReverseSolidusAtomEscape() { - const start = this.index; - if (this.eat(ReverseSolidus)) { - if (this.consumeAtomEscape()) { - return true; - } - this.rewind(start); - } - return false; - } - consumeUncapturingGroup() { - const start = this.index; - if (this.eat3(LeftParenthesis, QuestionMark, Colon)) { - this.onGroupEnter(start); - this.consumeDisjunction(); - if (!this.eat(RightParenthesis)) { - this.raise("Unterminated group"); - } - this.onGroupLeave(start, this.index); - return true; - } - return false; - } - consumeCapturingGroup() { - const start = this.index; - if (this.eat(LeftParenthesis)) { - let name = null; - if (this.ecmaVersion >= 2018) { - if (this.consumeGroupSpecifier()) { - name = this._lastStrValue; - } - } - else if (this.currentCodePoint === QuestionMark) { - this.raise("Invalid group"); - } - this.onCapturingGroupEnter(start, name); - this.consumeDisjunction(); - if (!this.eat(RightParenthesis)) { - this.raise("Unterminated group"); - } - this.onCapturingGroupLeave(start, this.index, name); - return true; - } - return false; - } - consumeExtendedAtom() { - return (this.consumeDot() || - this.consumeReverseSolidusAtomEscape() || - this.consumeReverseSolidusFollowedByC() || - this.consumeCharacterClass() || - this.consumeUncapturingGroup() || - this.consumeCapturingGroup() || - this.consumeInvalidBracedQuantifier() || - this.consumeExtendedPatternCharacter()); - } - consumeReverseSolidusFollowedByC() { - const start = this.index; - if (this.currentCodePoint === ReverseSolidus && - this.nextCodePoint === LatinSmallLetterC) { - this._lastIntValue = this.currentCodePoint; - this.advance(); - this.onCharacter(start, this.index, ReverseSolidus); - return true; - } - return false; - } - consumeInvalidBracedQuantifier() { - if (this.eatBracedQuantifier(true)) { - this.raise("Nothing to repeat"); - } - return false; - } - consumePatternCharacter() { - const start = this.index; - const cp = this.currentCodePoint; - if (cp !== -1 && !isSyntaxCharacter(cp)) { - this.advance(); - this.onCharacter(start, this.index, cp); - return true; - } - return false; - } - consumeExtendedPatternCharacter() { - const start = this.index; - const cp = this.currentCodePoint; - if (cp !== -1 && - cp !== CircumflexAccent && - cp !== DollarSign && - cp !== ReverseSolidus && - cp !== FullStop && - cp !== Asterisk && - cp !== PlusSign && - cp !== QuestionMark && - cp !== LeftParenthesis && - cp !== RightParenthesis && - cp !== LeftSquareBracket && - cp !== VerticalLine) { - this.advance(); - this.onCharacter(start, this.index, cp); - return true; - } - return false; - } - consumeGroupSpecifier() { - if (this.eat(QuestionMark)) { - if (this.eatGroupName()) { - if (!this._groupNames.has(this._lastStrValue)) { - this._groupNames.add(this._lastStrValue); - return true; - } - this.raise("Duplicate capture group name"); - } - this.raise("Invalid group"); - } - return false; - } - consumeAtomEscape() { - if (this.consumeBackreference() || - this.consumeCharacterClassEscape() || - this.consumeCharacterEscape() || - (this._nFlag && this.consumeKGroupName())) { - return true; - } - if (this.strict || this._uFlag) { - this.raise("Invalid escape"); - } - return false; - } - consumeBackreference() { - const start = this.index; - if (this.eatDecimalEscape()) { - const n = this._lastIntValue; - if (n <= this._numCapturingParens) { - this.onBackreference(start - 1, this.index, n); - return true; - } - if (this.strict || this._uFlag) { - this.raise("Invalid escape"); - } - this.rewind(start); - } - return false; - } - consumeCharacterClassEscape() { - const start = this.index; - if (this.eat(LatinSmallLetterD)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "digit", false); - return true; - } - if (this.eat(LatinCapitalLetterD)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "digit", true); - return true; - } - if (this.eat(LatinSmallLetterS)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "space", false); - return true; - } - if (this.eat(LatinCapitalLetterS)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "space", true); - return true; - } - if (this.eat(LatinSmallLetterW)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "word", false); - return true; - } - if (this.eat(LatinCapitalLetterW)) { - this._lastIntValue = -1; - this.onEscapeCharacterSet(start - 1, this.index, "word", true); - return true; - } - let negate = false; - if (this._uFlag && - this.ecmaVersion >= 2018 && - (this.eat(LatinSmallLetterP) || - (negate = this.eat(LatinCapitalLetterP)))) { - this._lastIntValue = -1; - if (this.eat(LeftCurlyBracket) && - this.eatUnicodePropertyValueExpression() && - this.eat(RightCurlyBracket)) { - this.onUnicodePropertyCharacterSet(start - 1, this.index, "property", this._lastKeyValue, this._lastValValue || null, negate); - return true; - } - this.raise("Invalid property name"); - } - return false; - } - consumeCharacterEscape() { - const start = this.index; - if (this.eatControlEscape() || - this.eatCControlLetter() || - this.eatZero() || - this.eatHexEscapeSequence() || - this.eatRegExpUnicodeEscapeSequence() || - (!this.strict && - !this._uFlag && - this.eatLegacyOctalEscapeSequence()) || - this.eatIdentityEscape()) { - this.onCharacter(start - 1, this.index, this._lastIntValue); - return true; - } - return false; - } - consumeKGroupName() { - const start = this.index; - if (this.eat(LatinSmallLetterK)) { - if (this.eatGroupName()) { - const groupName = this._lastStrValue; - this._backreferenceNames.add(groupName); - this.onBackreference(start - 1, this.index, groupName); - return true; - } - this.raise("Invalid named reference"); - } - return false; - } - consumeCharacterClass() { - const start = this.index; - if (this.eat(LeftSquareBracket)) { - const negate = this.eat(CircumflexAccent); - this.onCharacterClassEnter(start, negate); - this.consumeClassRanges(); - if (!this.eat(RightSquareBracket)) { - this.raise("Unterminated character class"); - } - this.onCharacterClassLeave(start, this.index, negate); - return true; - } - return false; - } - consumeClassRanges() { - const strict = this.strict || this._uFlag; - for (;;) { - const rangeStart = this.index; - if (!this.consumeClassAtom()) { - break; - } - const min = this._lastIntValue; - if (!this.eat(HyphenMinus)) { - continue; - } - this.onCharacter(this.index - 1, this.index, HyphenMinus); - if (!this.consumeClassAtom()) { - break; - } - const max = this._lastIntValue; - if (min === -1 || max === -1) { - if (strict) { - this.raise("Invalid character class"); - } - continue; - } - if (min > max) { - this.raise("Range out of order in character class"); - } - this.onCharacterClassRange(rangeStart, this.index, min, max); - } - } - consumeClassAtom() { - const start = this.index; - const cp = this.currentCodePoint; - if (cp !== -1 && cp !== ReverseSolidus && cp !== RightSquareBracket) { - this.advance(); - this._lastIntValue = cp; - this.onCharacter(start, this.index, this._lastIntValue); - return true; - } - if (this.eat(ReverseSolidus)) { - if (this.consumeClassEscape()) { - return true; - } - if (!this.strict && this.currentCodePoint === LatinSmallLetterC) { - this._lastIntValue = ReverseSolidus; - this.onCharacter(start, this.index, this._lastIntValue); - return true; - } - if (this.strict || this._uFlag) { - this.raise("Invalid escape"); - } - this.rewind(start); - } - return false; - } - consumeClassEscape() { - const start = this.index; - if (this.eat(LatinSmallLetterB)) { - this._lastIntValue = Backspace; - this.onCharacter(start - 1, this.index, this._lastIntValue); - return true; - } - if (this._uFlag && this.eat(HyphenMinus)) { - this._lastIntValue = HyphenMinus; - this.onCharacter(start - 1, this.index, this._lastIntValue); - return true; - } - let cp = 0; - if (!this.strict && - !this._uFlag && - this.currentCodePoint === LatinSmallLetterC && - (isDecimalDigit((cp = this.nextCodePoint)) || cp === LowLine)) { - this.advance(); - this.advance(); - this._lastIntValue = cp % 0x20; - this.onCharacter(start - 1, this.index, this._lastIntValue); - return true; - } - return (this.consumeCharacterClassEscape() || this.consumeCharacterEscape()); - } - eatGroupName() { - if (this.eat(LessThanSign)) { - if (this.eatRegExpIdentifierName() && this.eat(GreaterThanSign)) { - return true; - } - this.raise("Invalid capture group name"); - } - return false; - } - eatRegExpIdentifierName() { - if (this.eatRegExpIdentifierStart()) { - this._lastStrValue = String.fromCodePoint(this._lastIntValue); - while (this.eatRegExpIdentifierPart()) { - this._lastStrValue += String.fromCodePoint(this._lastIntValue); - } - return true; - } - return false; - } - eatRegExpIdentifierStart() { - const start = this.index; - const forceUFlag = !this._uFlag && this.ecmaVersion >= 2020; - let cp = this.currentCodePoint; - this.advance(); - if (cp === ReverseSolidus && - this.eatRegExpUnicodeEscapeSequence(forceUFlag)) { - cp = this._lastIntValue; - } - else if (forceUFlag && - isLeadSurrogate(cp) && - isTrailSurrogate(this.currentCodePoint)) { - cp = combineSurrogatePair(cp, this.currentCodePoint); - this.advance(); - } - if (isRegExpIdentifierStart(cp)) { - this._lastIntValue = cp; - return true; - } - if (this.index !== start) { - this.rewind(start); - } - return false; - } - eatRegExpIdentifierPart() { - const start = this.index; - const forceUFlag = !this._uFlag && this.ecmaVersion >= 2020; - let cp = this.currentCodePoint; - this.advance(); - if (cp === ReverseSolidus && - this.eatRegExpUnicodeEscapeSequence(forceUFlag)) { - cp = this._lastIntValue; - } - else if (forceUFlag && - isLeadSurrogate(cp) && - isTrailSurrogate(this.currentCodePoint)) { - cp = combineSurrogatePair(cp, this.currentCodePoint); - this.advance(); - } - if (isRegExpIdentifierPart(cp)) { - this._lastIntValue = cp; - return true; - } - if (this.index !== start) { - this.rewind(start); - } - return false; - } - eatCControlLetter() { - const start = this.index; - if (this.eat(LatinSmallLetterC)) { - if (this.eatControlLetter()) { - return true; - } - this.rewind(start); - } - return false; - } - eatZero() { - if (this.currentCodePoint === DigitZero && - !isDecimalDigit(this.nextCodePoint)) { - this._lastIntValue = 0; - this.advance(); - return true; - } - return false; - } - eatControlEscape() { - if (this.eat(LatinSmallLetterF)) { - this._lastIntValue = FormFeed; - return true; - } - if (this.eat(LatinSmallLetterN)) { - this._lastIntValue = LineFeed; - return true; - } - if (this.eat(LatinSmallLetterR)) { - this._lastIntValue = CarriageReturn; - return true; - } - if (this.eat(LatinSmallLetterT)) { - this._lastIntValue = CharacterTabulation; - return true; - } - if (this.eat(LatinSmallLetterV)) { - this._lastIntValue = LineTabulation; - return true; - } - return false; - } - eatControlLetter() { - const cp = this.currentCodePoint; - if (isLatinLetter(cp)) { - this.advance(); - this._lastIntValue = cp % 0x20; - return true; - } - return false; - } - eatRegExpUnicodeEscapeSequence(forceUFlag = false) { - const start = this.index; - const uFlag = forceUFlag || this._uFlag; - if (this.eat(LatinSmallLetterU)) { - if ((uFlag && this.eatRegExpUnicodeSurrogatePairEscape()) || - this.eatFixedHexDigits(4) || - (uFlag && this.eatRegExpUnicodeCodePointEscape())) { - return true; - } - if (this.strict || uFlag) { - this.raise("Invalid unicode escape"); - } - this.rewind(start); - } - return false; - } - eatRegExpUnicodeSurrogatePairEscape() { - const start = this.index; - if (this.eatFixedHexDigits(4)) { - const lead = this._lastIntValue; - if (isLeadSurrogate(lead) && - this.eat(ReverseSolidus) && - this.eat(LatinSmallLetterU) && - this.eatFixedHexDigits(4)) { - const trail = this._lastIntValue; - if (isTrailSurrogate(trail)) { - this._lastIntValue = combineSurrogatePair(lead, trail); - return true; - } - } - this.rewind(start); - } - return false; - } - eatRegExpUnicodeCodePointEscape() { - const start = this.index; - if (this.eat(LeftCurlyBracket) && - this.eatHexDigits() && - this.eat(RightCurlyBracket) && - isValidUnicode(this._lastIntValue)) { - return true; - } - this.rewind(start); - return false; - } - eatIdentityEscape() { - const cp = this.currentCodePoint; - if (this.isValidIdentityEscape(cp)) { - this._lastIntValue = cp; - this.advance(); - return true; - } - return false; - } - isValidIdentityEscape(cp) { - if (cp === -1) { - return false; - } - if (this._uFlag) { - return isSyntaxCharacter(cp) || cp === Solidus; - } - if (this.strict) { - return !isIdContinue(cp); - } - if (this._nFlag) { - return !(cp === LatinSmallLetterC || cp === LatinSmallLetterK); - } - return cp !== LatinSmallLetterC; - } - eatDecimalEscape() { - this._lastIntValue = 0; - let cp = this.currentCodePoint; - if (cp >= DigitOne && cp <= DigitNine) { - do { - this._lastIntValue = 10 * this._lastIntValue + (cp - DigitZero); - this.advance(); - } while ((cp = this.currentCodePoint) >= DigitZero && - cp <= DigitNine); - return true; - } - return false; - } - eatUnicodePropertyValueExpression() { - const start = this.index; - if (this.eatUnicodePropertyName() && this.eat(EqualsSign)) { - this._lastKeyValue = this._lastStrValue; - if (this.eatUnicodePropertyValue()) { - this._lastValValue = this._lastStrValue; - if (isValidUnicodeProperty(this.ecmaVersion, this._lastKeyValue, this._lastValValue)) { - return true; - } - this.raise("Invalid property name"); - } - } - this.rewind(start); - if (this.eatLoneUnicodePropertyNameOrValue()) { - const nameOrValue = this._lastStrValue; - if (isValidUnicodeProperty(this.ecmaVersion, "General_Category", nameOrValue)) { - this._lastKeyValue = "General_Category"; - this._lastValValue = nameOrValue; - return true; - } - if (isValidLoneUnicodeProperty(this.ecmaVersion, nameOrValue)) { - this._lastKeyValue = nameOrValue; - this._lastValValue = ""; - return true; - } - this.raise("Invalid property name"); - } - return false; - } - eatUnicodePropertyName() { - this._lastStrValue = ""; - while (isUnicodePropertyNameCharacter(this.currentCodePoint)) { - this._lastStrValue += String.fromCodePoint(this.currentCodePoint); - this.advance(); - } - return this._lastStrValue !== ""; - } - eatUnicodePropertyValue() { - this._lastStrValue = ""; - while (isUnicodePropertyValueCharacter(this.currentCodePoint)) { - this._lastStrValue += String.fromCodePoint(this.currentCodePoint); - this.advance(); - } - return this._lastStrValue !== ""; - } - eatLoneUnicodePropertyNameOrValue() { - return this.eatUnicodePropertyValue(); - } - eatHexEscapeSequence() { - const start = this.index; - if (this.eat(LatinSmallLetterX)) { - if (this.eatFixedHexDigits(2)) { - return true; - } - if (this._uFlag || this.strict) { - this.raise("Invalid escape"); - } - this.rewind(start); - } - return false; - } - eatDecimalDigits() { - const start = this.index; - this._lastIntValue = 0; - while (isDecimalDigit(this.currentCodePoint)) { - this._lastIntValue = - 10 * this._lastIntValue + digitToInt(this.currentCodePoint); - this.advance(); - } - return this.index !== start; - } - eatHexDigits() { - const start = this.index; - this._lastIntValue = 0; - while (isHexDigit(this.currentCodePoint)) { - this._lastIntValue = - 16 * this._lastIntValue + digitToInt(this.currentCodePoint); - this.advance(); - } - return this.index !== start; - } - eatLegacyOctalEscapeSequence() { - if (this.eatOctalDigit()) { - const n1 = this._lastIntValue; - if (this.eatOctalDigit()) { - const n2 = this._lastIntValue; - if (n1 <= 3 && this.eatOctalDigit()) { - this._lastIntValue = n1 * 64 + n2 * 8 + this._lastIntValue; - } - else { - this._lastIntValue = n1 * 8 + n2; - } - } - else { - this._lastIntValue = n1; - } - return true; - } - return false; - } - eatOctalDigit() { - const cp = this.currentCodePoint; - if (isOctalDigit(cp)) { - this.advance(); - this._lastIntValue = cp - DigitZero; - return true; - } - this._lastIntValue = 0; - return false; - } - eatFixedHexDigits(length) { - const start = this.index; - this._lastIntValue = 0; - for (let i = 0; i < length; ++i) { - const cp = this.currentCodePoint; - if (!isHexDigit(cp)) { - this.rewind(start); - return false; - } - this._lastIntValue = 16 * this._lastIntValue + digitToInt(cp); - this.advance(); - } - return true; - } -} - -const DummyPattern = {}; -const DummyFlags = {}; -const DummyCapturingGroup = {}; -class RegExpParserState { - constructor(options) { - this._node = DummyPattern; - this._flags = DummyFlags; - this._backreferences = []; - this._capturingGroups = []; - this.source = ""; - this.strict = Boolean(options && options.strict); - this.ecmaVersion = (options && options.ecmaVersion) || 2022; - } - get pattern() { - if (this._node.type !== "Pattern") { - throw new Error("UnknownError"); - } - return this._node; - } - get flags() { - if (this._flags.type !== "Flags") { - throw new Error("UnknownError"); - } - return this._flags; - } - onFlags(start, end, global, ignoreCase, multiline, unicode, sticky, dotAll, hasIndices) { - this._flags = { - type: "Flags", - parent: null, - start, - end, - raw: this.source.slice(start, end), - global, - ignoreCase, - multiline, - unicode, - sticky, - dotAll, - hasIndices, - }; - } - onPatternEnter(start) { - this._node = { - type: "Pattern", - parent: null, - start, - end: start, - raw: "", - alternatives: [], - }; - this._backreferences.length = 0; - this._capturingGroups.length = 0; - } - onPatternLeave(start, end) { - this._node.end = end; - this._node.raw = this.source.slice(start, end); - for (const reference of this._backreferences) { - const ref = reference.ref; - const group = typeof ref === "number" - ? this._capturingGroups[ref - 1] - : this._capturingGroups.find(g => g.name === ref); - reference.resolved = group; - group.references.push(reference); - } - } - onAlternativeEnter(start) { - const parent = this._node; - if (parent.type !== "Assertion" && - parent.type !== "CapturingGroup" && - parent.type !== "Group" && - parent.type !== "Pattern") { - throw new Error("UnknownError"); - } - this._node = { - type: "Alternative", - parent, - start, - end: start, - raw: "", - elements: [], - }; - parent.alternatives.push(this._node); - } - onAlternativeLeave(start, end) { - const node = this._node; - if (node.type !== "Alternative") { - throw new Error("UnknownError"); - } - node.end = end; - node.raw = this.source.slice(start, end); - this._node = node.parent; - } - onGroupEnter(start) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - this._node = { - type: "Group", - parent, - start, - end: start, - raw: "", - alternatives: [], - }; - parent.elements.push(this._node); - } - onGroupLeave(start, end) { - const node = this._node; - if (node.type !== "Group" || node.parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - node.end = end; - node.raw = this.source.slice(start, end); - this._node = node.parent; - } - onCapturingGroupEnter(start, name) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - this._node = { - type: "CapturingGroup", - parent, - start, - end: start, - raw: "", - name, - alternatives: [], - references: [], - }; - parent.elements.push(this._node); - this._capturingGroups.push(this._node); - } - onCapturingGroupLeave(start, end) { - const node = this._node; - if (node.type !== "CapturingGroup" || - node.parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - node.end = end; - node.raw = this.source.slice(start, end); - this._node = node.parent; - } - onQuantifier(start, end, min, max, greedy) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - const element = parent.elements.pop(); - if (element == null || - element.type === "Quantifier" || - (element.type === "Assertion" && element.kind !== "lookahead")) { - throw new Error("UnknownError"); - } - const node = { - type: "Quantifier", - parent, - start: element.start, - end, - raw: this.source.slice(element.start, end), - min, - max, - greedy, - element, - }; - parent.elements.push(node); - element.parent = node; - } - onLookaroundAssertionEnter(start, kind, negate) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - const node = (this._node = { - type: "Assertion", - parent, - start, - end: start, - raw: "", - kind, - negate, - alternatives: [], - }); - parent.elements.push(node); - } - onLookaroundAssertionLeave(start, end) { - const node = this._node; - if (node.type !== "Assertion" || node.parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - node.end = end; - node.raw = this.source.slice(start, end); - this._node = node.parent; - } - onEdgeAssertion(start, end, kind) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "Assertion", - parent, - start, - end, - raw: this.source.slice(start, end), - kind, - }); - } - onWordBoundaryAssertion(start, end, kind, negate) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "Assertion", - parent, - start, - end, - raw: this.source.slice(start, end), - kind, - negate, - }); - } - onAnyCharacterSet(start, end, kind) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "CharacterSet", - parent, - start, - end, - raw: this.source.slice(start, end), - kind, - }); - } - onEscapeCharacterSet(start, end, kind, negate) { - const parent = this._node; - if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "CharacterSet", - parent, - start, - end, - raw: this.source.slice(start, end), - kind, - negate, - }); - } - onUnicodePropertyCharacterSet(start, end, kind, key, value, negate) { - const parent = this._node; - if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "CharacterSet", - parent, - start, - end, - raw: this.source.slice(start, end), - kind, - key, - value, - negate, - }); - } - onCharacter(start, end, value) { - const parent = this._node; - if (parent.type !== "Alternative" && parent.type !== "CharacterClass") { - throw new Error("UnknownError"); - } - parent.elements.push({ - type: "Character", - parent, - start, - end, - raw: this.source.slice(start, end), - value, - }); - } - onBackreference(start, end, ref) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - const node = { - type: "Backreference", - parent, - start, - end, - raw: this.source.slice(start, end), - ref, - resolved: DummyCapturingGroup, - }; - parent.elements.push(node); - this._backreferences.push(node); - } - onCharacterClassEnter(start, negate) { - const parent = this._node; - if (parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - this._node = { - type: "CharacterClass", - parent, - start, - end: start, - raw: "", - negate, - elements: [], - }; - parent.elements.push(this._node); - } - onCharacterClassLeave(start, end) { - const node = this._node; - if (node.type !== "CharacterClass" || - node.parent.type !== "Alternative") { - throw new Error("UnknownError"); - } - node.end = end; - node.raw = this.source.slice(start, end); - this._node = node.parent; - } - onCharacterClassRange(start, end) { - const parent = this._node; - if (parent.type !== "CharacterClass") { - throw new Error("UnknownError"); - } - const elements = parent.elements; - const max = elements.pop(); - const hyphen = elements.pop(); - const min = elements.pop(); - if (!min || - !max || - !hyphen || - min.type !== "Character" || - max.type !== "Character" || - hyphen.type !== "Character" || - hyphen.value !== HyphenMinus) { - throw new Error("UnknownError"); - } - const node = { - type: "CharacterClassRange", - parent, - start, - end, - raw: this.source.slice(start, end), - min, - max, - }; - min.parent = node; - max.parent = node; - elements.push(node); - } -} -class RegExpParser { - constructor(options) { - this._state = new RegExpParserState(options); - this._validator = new RegExpValidator(this._state); - } - parseLiteral(source, start = 0, end = source.length) { - this._state.source = source; - this._validator.validateLiteral(source, start, end); - const pattern = this._state.pattern; - const flags = this._state.flags; - const literal = { - type: "RegExpLiteral", - parent: null, - start, - end, - raw: source, - pattern, - flags, - }; - pattern.parent = literal; - flags.parent = literal; - return literal; - } - parseFlags(source, start = 0, end = source.length) { - this._state.source = source; - this._validator.validateFlags(source, start, end); - return this._state.flags; - } - parsePattern(source, start = 0, end = source.length, uFlag = false) { - this._state.source = source; - this._validator.validatePattern(source, start, end, uFlag); - return this._state.pattern; - } -} - -class RegExpVisitor { - constructor(handlers) { - this._handlers = handlers; - } - visit(node) { - switch (node.type) { - case "Alternative": - this.visitAlternative(node); - break; - case "Assertion": - this.visitAssertion(node); - break; - case "Backreference": - this.visitBackreference(node); - break; - case "CapturingGroup": - this.visitCapturingGroup(node); - break; - case "Character": - this.visitCharacter(node); - break; - case "CharacterClass": - this.visitCharacterClass(node); - break; - case "CharacterClassRange": - this.visitCharacterClassRange(node); - break; - case "CharacterSet": - this.visitCharacterSet(node); - break; - case "Flags": - this.visitFlags(node); - break; - case "Group": - this.visitGroup(node); - break; - case "Pattern": - this.visitPattern(node); - break; - case "Quantifier": - this.visitQuantifier(node); - break; - case "RegExpLiteral": - this.visitRegExpLiteral(node); - break; - default: - throw new Error(`Unknown type: ${node.type}`); - } - } - visitAlternative(node) { - if (this._handlers.onAlternativeEnter) { - this._handlers.onAlternativeEnter(node); - } - node.elements.forEach(this.visit, this); - if (this._handlers.onAlternativeLeave) { - this._handlers.onAlternativeLeave(node); - } - } - visitAssertion(node) { - if (this._handlers.onAssertionEnter) { - this._handlers.onAssertionEnter(node); - } - if (node.kind === "lookahead" || node.kind === "lookbehind") { - node.alternatives.forEach(this.visit, this); - } - if (this._handlers.onAssertionLeave) { - this._handlers.onAssertionLeave(node); - } - } - visitBackreference(node) { - if (this._handlers.onBackreferenceEnter) { - this._handlers.onBackreferenceEnter(node); - } - if (this._handlers.onBackreferenceLeave) { - this._handlers.onBackreferenceLeave(node); - } - } - visitCapturingGroup(node) { - if (this._handlers.onCapturingGroupEnter) { - this._handlers.onCapturingGroupEnter(node); - } - node.alternatives.forEach(this.visit, this); - if (this._handlers.onCapturingGroupLeave) { - this._handlers.onCapturingGroupLeave(node); - } - } - visitCharacter(node) { - if (this._handlers.onCharacterEnter) { - this._handlers.onCharacterEnter(node); - } - if (this._handlers.onCharacterLeave) { - this._handlers.onCharacterLeave(node); - } - } - visitCharacterClass(node) { - if (this._handlers.onCharacterClassEnter) { - this._handlers.onCharacterClassEnter(node); - } - node.elements.forEach(this.visit, this); - if (this._handlers.onCharacterClassLeave) { - this._handlers.onCharacterClassLeave(node); - } - } - visitCharacterClassRange(node) { - if (this._handlers.onCharacterClassRangeEnter) { - this._handlers.onCharacterClassRangeEnter(node); - } - this.visitCharacter(node.min); - this.visitCharacter(node.max); - if (this._handlers.onCharacterClassRangeLeave) { - this._handlers.onCharacterClassRangeLeave(node); - } - } - visitCharacterSet(node) { - if (this._handlers.onCharacterSetEnter) { - this._handlers.onCharacterSetEnter(node); - } - if (this._handlers.onCharacterSetLeave) { - this._handlers.onCharacterSetLeave(node); - } - } - visitFlags(node) { - if (this._handlers.onFlagsEnter) { - this._handlers.onFlagsEnter(node); - } - if (this._handlers.onFlagsLeave) { - this._handlers.onFlagsLeave(node); - } - } - visitGroup(node) { - if (this._handlers.onGroupEnter) { - this._handlers.onGroupEnter(node); - } - node.alternatives.forEach(this.visit, this); - if (this._handlers.onGroupLeave) { - this._handlers.onGroupLeave(node); - } - } - visitPattern(node) { - if (this._handlers.onPatternEnter) { - this._handlers.onPatternEnter(node); - } - node.alternatives.forEach(this.visit, this); - if (this._handlers.onPatternLeave) { - this._handlers.onPatternLeave(node); - } - } - visitQuantifier(node) { - if (this._handlers.onQuantifierEnter) { - this._handlers.onQuantifierEnter(node); - } - this.visit(node.element); - if (this._handlers.onQuantifierLeave) { - this._handlers.onQuantifierLeave(node); - } - } - visitRegExpLiteral(node) { - if (this._handlers.onRegExpLiteralEnter) { - this._handlers.onRegExpLiteralEnter(node); - } - this.visitPattern(node.pattern); - this.visitFlags(node.flags); - if (this._handlers.onRegExpLiteralLeave) { - this._handlers.onRegExpLiteralLeave(node); - } - } -} - -function parseRegExpLiteral(source, options) { - return new RegExpParser(options).parseLiteral(String(source)); -} -function validateRegExpLiteral(source, options) { - return new RegExpValidator(options).validateLiteral(source); -} -function visitRegExpAST(node, handlers) { - new RegExpVisitor(handlers).visit(node); -} - -export { ast as AST, RegExpParser, RegExpValidator, parseRegExpLiteral, validateRegExpLiteral, visitRegExpAST }; -//# sourceMappingURL=index.mjs.map diff --git a/packages/json-validator/src/index.ts b/packages/json-validator/src/index.ts index 294f41ac..170ce081 100644 --- a/packages/json-validator/src/index.ts +++ b/packages/json-validator/src/index.ts @@ -26,4 +26,4 @@ export * from "./validator"; export * from "./types"; export * from "./schema"; export { initState, setInitializer } from "./state"; -export { asMappedString } from "mapped-string"; \ No newline at end of file +export { asMappedString } from "@quarto/mapped-string"; diff --git a/packages/json-validator/src/regexp.js b/packages/json-validator/src/regexp.js index c7d7d69c..0ed579c9 100644 --- a/packages/json-validator/src/regexp.js +++ b/packages/json-validator/src/regexp.js @@ -7,7 +7,7 @@ * */ -import * as regexpp from "./external/regexpp.mjs"; +import * as regexpp from "regexpp"; function prefixesFromParse(parse) { if (parse.type === "Pattern" || parse.type === "CapturingGroup") { diff --git a/packages/json-validator/src/types.ts b/packages/json-validator/src/types.ts index 5353a852..aff6b5ba 100644 --- a/packages/json-validator/src/types.ts +++ b/packages/json-validator/src/types.ts @@ -4,8 +4,8 @@ * Copyright (C) 2022 Posit Software, PBC */ -import { TidyverseError } from "tidyverse-errors"; -import { ErrorLocation, MappedString } from "mapped-string"; +import { TidyverseError } from "@quarto/tidyverse-errors"; +import { ErrorLocation, MappedString } from "@quarto/mapped-string"; import { AnnotatedParse, @@ -51,7 +51,7 @@ export interface YAMLSchemaT { result: ValidatedParseResult, _src: MappedString, message: string, - + // eslint-disable-next-line @typescript-eslint/no-explicit-any error: (a: string) => any, log: (a: TidyverseError) => unknown, @@ -359,4 +359,3 @@ export function schemaDescription(schema: Schema): string { return schema.description || `be ${schemaType(schema)}`; } } - diff --git a/packages/json-validator/src/validator.ts b/packages/json-validator/src/validator.ts index 8af3a4e9..df7490f5 100644 --- a/packages/json-validator/src/validator.ts +++ b/packages/json-validator/src/validator.ts @@ -32,7 +32,7 @@ import { import { resolveSchema } from "./resolve"; -import { MappedString } from "mapped-string"; +import { MappedString } from "@quarto/mapped-string"; import { createLocalizedError } from "./errors"; //////////////////////////////////////////////////////////////////////////////// diff --git a/packages/json-validator/src/yaml-schema.ts b/packages/json-validator/src/yaml-schema.ts index 47a735ff..fa70d649 100644 --- a/packages/json-validator/src/yaml-schema.ts +++ b/packages/json-validator/src/yaml-schema.ts @@ -7,7 +7,7 @@ * Copyright (C) 2022 Posit Software, PBC */ -import { MappedString } from "mapped-string"; +import { MappedString } from "@quarto/mapped-string"; import { TidyverseError } from "tidyverse-errors"; import { ValidatorErrorHandlerFunction } from "./types"; diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index 43a1f806..80298f9a 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/mapped-string", - "version": "0.1.0", + "version": "0.1.1", "description": "A string data structure with integrated source maps.", "license": "MIT", "author": { @@ -23,7 +23,7 @@ "tsconfig": "*", "typescript": "^5.4.2", "ansi-colors": "^4.1.3", - "@quarto/tidyverse-errors": "^0.1.1" + "@quarto/tidyverse-errors": "^0.1.2" }, "devDependencies": { } } diff --git a/packages/mapped-string/src/error.ts b/packages/mapped-string/src/error.ts index e003c4ab..6a19863b 100644 --- a/packages/mapped-string/src/error.ts +++ b/packages/mapped-string/src/error.ts @@ -7,7 +7,7 @@ import { mappedIndexToLineCol } from "./mapped-text"; import { lines } from "./text"; import { MappedString, Range } from "./types"; -import { quotedStringColor } from "tidyverse-errors"; +import { quotedStringColor } from "@quarto/tidyverse-errors"; export class InternalError extends Error { constructor( @@ -177,7 +177,7 @@ export function createSourceContext( contextLines.push(content); contextLines.push( " ".repeat(prefixWidth + startColumn - 1) + - "~".repeat(endColumn - startColumn + 1), + "~".repeat(endColumn - startColumn + 1), ); } } diff --git a/packages/tidyverse-errors/package.json b/packages/tidyverse-errors/package.json index cd17f9cf..45df8747 100644 --- a/packages/tidyverse-errors/package.json +++ b/packages/tidyverse-errors/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/tidyverse-errors", - "version": "0.1.1", + "version": "0.1.2", "description": "Format errors using the tidyverse style.", "author": { "name": "Posit PBC" From b4e043bd8b1f9ad183e0afb01d087def37aabb57 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 12:22:13 -0400 Subject: [PATCH 14/18] more fixes --- packages/annotated-json/package.json | 3 --- packages/annotated-json/src/annotated-yaml.ts | 1 + packages/annotated-json/src/js-yaml-quarto-schema.ts | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index e96f992c..ae159659 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -22,8 +22,5 @@ "tsconfig": "*", "typescript": "^5.4.2" }, - "scripts": { - "build": "tsx build.ts" - }, "devDependencies": { } } diff --git a/packages/annotated-json/src/annotated-yaml.ts b/packages/annotated-json/src/annotated-yaml.ts index ee04f650..89f1f79c 100644 --- a/packages/annotated-json/src/annotated-yaml.ts +++ b/packages/annotated-json/src/annotated-yaml.ts @@ -16,6 +16,7 @@ import { EitherString } from "@quarto/mapped-string"; +// @ts-ignore import { load as jsYamlParse } from "./external/js-yaml.js"; import { QuartoJSONSchema } from "./js-yaml-quarto-schema"; diff --git a/packages/annotated-json/src/js-yaml-quarto-schema.ts b/packages/annotated-json/src/js-yaml-quarto-schema.ts index cf5f8326..12e32862 100644 --- a/packages/annotated-json/src/js-yaml-quarto-schema.ts +++ b/packages/annotated-json/src/js-yaml-quarto-schema.ts @@ -2,9 +2,10 @@ * js-yaml-quarto-schema.ts * * Copyright (C) 2024 by Posit Software, PBC - * + * */ +// @ts-ignore import { _null, // this is "nil" in deno's version...? :shrug: bool, From 476fad381a2356c39cacda5d2fa9431d8800a422 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 12:38:27 -0400 Subject: [PATCH 15/18] basic tests + claude prompt --- packages/tidyverse-errors/claude.md | 9 +++++++++ packages/tidyverse-errors/package.json | 7 ++++++- packages/tidyverse-errors/test/basic.test.ts | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 packages/tidyverse-errors/claude.md create mode 100644 packages/tidyverse-errors/test/basic.test.ts diff --git a/packages/tidyverse-errors/claude.md b/packages/tidyverse-errors/claude.md new file mode 100644 index 00000000..de064f02 --- /dev/null +++ b/packages/tidyverse-errors/claude.md @@ -0,0 +1,9 @@ +This package lives inside a monorepo build with `turbo` that is too big and unwieldy for you to make proper sense of. + +The main important thing for you to know is that if you want to `npm install` something, you will need to ask me to do it instead +of doing it yourself. + +## Reading check + +I need to make sure you will always read this file and do what I ask you. So let's do it like Van Halen did on their performance contract. +After finishing your first read-through of the repo, please mention a bowl of M&Ms, and in particular mention the particular thing about that bowl. diff --git a/packages/tidyverse-errors/package.json b/packages/tidyverse-errors/package.json index 45df8747..613bf094 100644 --- a/packages/tidyverse-errors/package.json +++ b/packages/tidyverse-errors/package.json @@ -21,5 +21,10 @@ "typescript": "^5.4.2", "ansi-colors": "^4.1.3" }, - "devDependencies": { } + "devDependencies": { + "tsx": "^4.7.1" + }, + "scripts": { + "test": "node --import tsx --test test/*.test.ts" + } } diff --git a/packages/tidyverse-errors/test/basic.test.ts b/packages/tidyverse-errors/test/basic.test.ts new file mode 100644 index 00000000..bb23e49f --- /dev/null +++ b/packages/tidyverse-errors/test/basic.test.ts @@ -0,0 +1,9 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import * as tidyverseErrors from "../src/index"; + +test('basic smoke tests', () => { + console.log(tidyverseErrors.quotedStringColor("test string")); + console.log(tidyverseErrors.tidyverseInfo("test info")); + console.log(tidyverseErrors.tidyverseError("test error")); +}); From 23756e865336b74946c1a89e0bf8a3d09b42cd45 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 12:38:37 -0400 Subject: [PATCH 16/18] lockfile --- yarn.lock | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/yarn.lock b/yarn.lock index 952e3ef0..7e72a6f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -460,11 +460,21 @@ "@esbuild-kit/core-utils" "^3.0.0" get-tsconfig "^4.2.0" +"@esbuild/aix-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" + integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== + "@esbuild/android-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== +"@esbuild/android-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" + integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== + "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" @@ -475,46 +485,91 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== +"@esbuild/android-arm@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" + integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== + "@esbuild/android-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== +"@esbuild/android-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" + integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== + "@esbuild/darwin-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== +"@esbuild/darwin-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34" + integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== + "@esbuild/darwin-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== +"@esbuild/darwin-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" + integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== + "@esbuild/freebsd-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== +"@esbuild/freebsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" + integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== + "@esbuild/freebsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== +"@esbuild/freebsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" + integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== + "@esbuild/linux-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== +"@esbuild/linux-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" + integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== + "@esbuild/linux-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== +"@esbuild/linux-arm@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" + integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== + "@esbuild/linux-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== +"@esbuild/linux-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" + integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== + "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" @@ -525,61 +580,131 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== +"@esbuild/linux-loong64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" + integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== + "@esbuild/linux-mips64el@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== +"@esbuild/linux-mips64el@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" + integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== + "@esbuild/linux-ppc64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== +"@esbuild/linux-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" + integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== + "@esbuild/linux-riscv64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== +"@esbuild/linux-riscv64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" + integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== + "@esbuild/linux-s390x@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== +"@esbuild/linux-s390x@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" + integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== + "@esbuild/linux-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== +"@esbuild/linux-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" + integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== + +"@esbuild/netbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" + integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== + "@esbuild/netbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== +"@esbuild/netbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" + integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== + +"@esbuild/openbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" + integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== + "@esbuild/openbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== +"@esbuild/openbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" + integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== + "@esbuild/sunos-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== +"@esbuild/sunos-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" + integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== + "@esbuild/win32-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== +"@esbuild/win32-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" + integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== + "@esbuild/win32-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== +"@esbuild/win32-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" + integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== + "@esbuild/win32-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== +"@esbuild/win32-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" + integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -4051,6 +4176,37 @@ esbuild@^0.16.7: "@esbuild/win32-ia32" "0.16.17" "@esbuild/win32-x64" "0.16.17" +esbuild@~0.25.0: + version "0.25.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430" + integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.5" + "@esbuild/android-arm" "0.25.5" + "@esbuild/android-arm64" "0.25.5" + "@esbuild/android-x64" "0.25.5" + "@esbuild/darwin-arm64" "0.25.5" + "@esbuild/darwin-x64" "0.25.5" + "@esbuild/freebsd-arm64" "0.25.5" + "@esbuild/freebsd-x64" "0.25.5" + "@esbuild/linux-arm" "0.25.5" + "@esbuild/linux-arm64" "0.25.5" + "@esbuild/linux-ia32" "0.25.5" + "@esbuild/linux-loong64" "0.25.5" + "@esbuild/linux-mips64el" "0.25.5" + "@esbuild/linux-ppc64" "0.25.5" + "@esbuild/linux-riscv64" "0.25.5" + "@esbuild/linux-s390x" "0.25.5" + "@esbuild/linux-x64" "0.25.5" + "@esbuild/netbsd-arm64" "0.25.5" + "@esbuild/netbsd-x64" "0.25.5" + "@esbuild/openbsd-arm64" "0.25.5" + "@esbuild/openbsd-x64" "0.25.5" + "@esbuild/sunos-x64" "0.25.5" + "@esbuild/win32-arm64" "0.25.5" + "@esbuild/win32-ia32" "0.25.5" + "@esbuild/win32-x64" "0.25.5" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4578,6 +4734,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + fstream@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" @@ -4665,6 +4826,13 @@ get-tsconfig@^4.2.0: resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.3.0.tgz#4c26fae115d1050e836aea65d6fe56b507ee249b" integrity sha512-YCcF28IqSay3fqpIu5y3Krg/utCBHBeoflkZyHj/QcqI2nrLPC3ZegS9CmIo+hJb8K7aiGsuUl7PwWVjNG2HQQ== +get-tsconfig@^4.7.5: + version "4.10.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== + dependencies: + resolve-pkg-maps "^1.0.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -6979,6 +7147,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve@^1.11.0, resolve@^1.11.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.22.1, resolve@~1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -7808,6 +7981,16 @@ tsx@^3.12.1: optionalDependencies: fsevents "~2.3.2" +tsx@^4.7.1: + version "4.20.3" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.20.3.tgz#f913e4911d59ad177c1bcee19d1035ef8dd6e2fb" + integrity sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ== + dependencies: + esbuild "~0.25.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" From 465e63ea24eda065c4634c5a081b2321a34efc89 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 13:05:55 -0400 Subject: [PATCH 17/18] add claude prompt, add tests, fix bugs --- packages/mapped-string/README.md | 23 ++ packages/mapped-string/claude.md | 10 + packages/mapped-string/package.json | 13 +- packages/mapped-string/src/mapped-text.ts | 10 +- packages/mapped-string/test/basic.test.ts | 306 ++++++++++++++++++++++ 5 files changed, 352 insertions(+), 10 deletions(-) create mode 100644 packages/mapped-string/README.md create mode 100644 packages/mapped-string/claude.md create mode 100644 packages/mapped-string/test/basic.test.ts diff --git a/packages/mapped-string/README.md b/packages/mapped-string/README.md new file mode 100644 index 00000000..f44c4450 --- /dev/null +++ b/packages/mapped-string/README.md @@ -0,0 +1,23 @@ +# @quarto/mapped-string + +A TypeScript library that provides a string data structure with integrated source maps, enabling precise tracking of character positions through string transformations. + +## Overview + +`@quarto/mapped-string` solves a common problem in text processing: maintaining accurate source location information when performing string manipulations. This is particularly valuable for error reporting. + +The library centers around the `MappedString` interface, which wraps a regular string with a `map` function that can translate any character index back to its position in the original source. Operations like `mappedSubstring`, `mappedReplace`, `mappedTrim`, and `skipRegexp` all preserve this mapping information, allowing complex string processing pipelines while maintaining perfect source location fidelity. + +## Key Features + +- **Source mapping preservation**: Every string operation maintains bidirectional mapping to original positions +- **Composable transformations**: Chain multiple string operations while preserving location information +- **Error location reporting**: Built-in support for precise error reporting with line/column information +- **String method equivalents**: Mapped versions of common string operations like `trim`, `replace`, `split`, and `substring` +- **TypeScript support**: Full type safety with comprehensive interfaces and type definitions + +## Installation + +```bash +npm install @quarto/mapped-string +``` diff --git a/packages/mapped-string/claude.md b/packages/mapped-string/claude.md new file mode 100644 index 00000000..78a22be9 --- /dev/null +++ b/packages/mapped-string/claude.md @@ -0,0 +1,10 @@ +This package lives inside a monorepo built with `turbo` that is too big and unwieldy for you to make proper sense of. + +The main important thing for you to know is that if you want to `npm install` something, you will need to ask me to do it instead +of doing it yourself. + +## Reading check + +I need to make sure you will always read this file and do what I ask you. So let's do it like Van Halen did on their performance contract. +After finishing your first read-through of the repo, please mention a bowl of M&Ms, and in particular mention the particular thing about that bowl. +Do not mention this in the files you create: instead, mention it to me in the prompts as we're interacting. diff --git a/packages/mapped-string/package.json b/packages/mapped-string/package.json index 80298f9a..44d7d6e9 100644 --- a/packages/mapped-string/package.json +++ b/packages/mapped-string/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/mapped-string", - "version": "0.1.1", + "version": "0.1.2", "description": "A string data structure with integrated source maps.", "license": "MIT", "author": { @@ -16,14 +16,17 @@ }, "main": "./src/index.ts", "types": "./src/index.ts", - "scripts": { - "build": "tsc" - }, "dependencies": { "tsconfig": "*", "typescript": "^5.4.2", "ansi-colors": "^4.1.3", "@quarto/tidyverse-errors": "^0.1.2" }, - "devDependencies": { } + "devDependencies": { + "tsx": "^4.7.1" + }, + "scripts": { + "build": "tsc", + "test": "node --import tsx --test test/*.test.ts" + } } diff --git a/packages/mapped-string/src/mapped-text.ts b/packages/mapped-string/src/mapped-text.ts index 758010c8..9cc27632 100644 --- a/packages/mapped-string/src/mapped-text.ts +++ b/packages/mapped-string/src/mapped-text.ts @@ -292,7 +292,7 @@ export function join(mappedStrs: EitherString[], sep: string): MappedString { } else { innerStrings.push(mappedStr); } - if (i < mappedStrs.length) { + if (i < mappedStrs.length - 1) { innerStrings.push(mappedSep); } } @@ -333,11 +333,11 @@ export function mappedReplace( return str; } return mappedConcat([ - mappedSubstring(str, 0, target.lastIndex), + mappedSubstring(str, 0, result.index!), asMappedString(replacement), mappedSubstring( str, - target.lastIndex + result[0].length, + result.index! + result[0].length, str.value.length, ), ]); @@ -351,10 +351,10 @@ export function mappedReplace( const pieces: MappedString[] = []; while (result) { pieces.push( - mappedSubstring(str, currentRange, target.lastIndex), + mappedSubstring(str, currentRange, result.index!), ); pieces.push(asMappedString(replacement)); - currentRange = target.lastIndex + result[0].length; + currentRange = result.index! + result[0].length; result = target.exec(str.value); } diff --git a/packages/mapped-string/test/basic.test.ts b/packages/mapped-string/test/basic.test.ts new file mode 100644 index 00000000..443982ce --- /dev/null +++ b/packages/mapped-string/test/basic.test.ts @@ -0,0 +1,306 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { + asMappedString, + mappedSubstring, + mappedConcat, + mappedString, + mappedTrim, + mappedTrimStart, + mappedTrimEnd, + join, + mappedLines, + mappedReplace, + breakOnDelimiter, + mappedNormalizeNewlines, + skipRegexp, + skipRegexpAll, + mappedIndexToLineCol, + type MappedString, + type EitherString +} from "../src/index"; +import { rangedSubstring, rangedLines } from "../src/index"; +import { lines, normalizeNewlines, indexToLineCol } from "../src/index"; + +test('asMappedString creates a mapped string from a regular string', () => { + const str = "hello world"; + const mapped = asMappedString(str); + + assert.strictEqual(mapped.value, str); + assert.strictEqual(mapped.fileName, undefined); + + // Test mapping functionality + const mapResult = mapped.map(6); + assert.notStrictEqual(mapResult, undefined); + assert.strictEqual(mapResult!.index, 6); + assert.strictEqual(mapResult!.originalString, mapped); +}); + +test('asMappedString with filename', () => { + const str = "hello world"; + const fileName = "test.txt"; + const mapped = asMappedString(str, fileName); + + assert.strictEqual(mapped.value, str); + assert.strictEqual(mapped.fileName, fileName); +}); + +test('asMappedString returns existing MappedString unchanged', () => { + const str = "hello world"; + const mapped = asMappedString(str, "test.txt"); + const remapped = asMappedString(mapped); + + assert.strictEqual(remapped, mapped); +}); + +test('mappedSubstring creates substring with correct mapping', () => { + const original = "hello world"; + const mapped = asMappedString(original); + const sub = mappedSubstring(mapped, 6, 11); + + assert.strictEqual(sub.value, "world"); + + // Test that mapping works correctly + const mapResult = sub.map(0); + assert.notStrictEqual(mapResult, undefined); + assert.strictEqual(mapResult!.index, 6); +}); + +test('mappedSubstring with string input', () => { + const original = "hello world"; + const sub = mappedSubstring(original, 6); + + assert.strictEqual(sub.value, "world"); + + const mapResult = sub.map(0); + assert.notStrictEqual(mapResult, undefined); + assert.strictEqual(mapResult!.index, 6); +}); + +test('mappedConcat joins multiple strings', () => { + const strings = ["hello", " ", "world"]; + const result = mappedConcat(strings); + + assert.strictEqual(result.value, "hello world"); + + // Test mapping of different parts + const helloMap = result.map(0); + const spaceMap = result.map(5); + const worldMap = result.map(6); + + assert.notStrictEqual(helloMap, undefined); + assert.notStrictEqual(spaceMap, undefined); + assert.notStrictEqual(worldMap, undefined); +}); + +test('mappedConcat with empty array', () => { + const result = mappedConcat([]); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.map(0), undefined); +}); + +test('mappedString creates string from pieces', () => { + const source = "hello beautiful world"; + const pieces = [ + { start: 0, end: 5 }, // "hello" + " ", // literal string + { start: 16, end: 21 } // "world" + ]; + + const result = mappedString(source, pieces); + assert.strictEqual(result.value, "hello world"); + + // Test that mapping works for each piece + const helloMap = result.map(0); + const worldMap = result.map(6); + + assert.notStrictEqual(helloMap, undefined); + assert.notStrictEqual(worldMap, undefined); + assert.strictEqual(helloMap!.index, 0); + assert.strictEqual(worldMap!.index, 16); +}); + +test('mappedTrim removes whitespace', () => { + const source = " hello world "; + const mapped = asMappedString(source); + const trimmed = mappedTrim(mapped); + + assert.strictEqual(trimmed.value, "hello world"); + + const mapResult = trimmed.map(0); + assert.notStrictEqual(mapResult, undefined); + assert.strictEqual(mapResult!.index, 2); // Should map to original position +}); + +test('mappedTrimStart removes leading whitespace', () => { + const source = " hello world "; + const mapped = asMappedString(source); + const trimmed = mappedTrimStart(mapped); + + assert.strictEqual(trimmed.value, "hello world "); +}); + +test('mappedTrimEnd removes trailing whitespace', () => { + const source = " hello world "; + const mapped = asMappedString(source); + const trimmed = mappedTrimEnd(mapped); + + assert.strictEqual(trimmed.value, " hello world"); +}); + +test('join works like Array.join', () => { + const strings = ["hello", "beautiful", "world"]; + const result = join(strings, " "); + + assert.strictEqual(result.value, "hello beautiful world"); +}); + +test('mappedLines splits into lines', () => { + const source = "line1\nline2\nline3"; + const mapped = asMappedString(source); + const lines = mappedLines(mapped); + + assert.strictEqual(lines.length, 3); + assert.strictEqual(lines[0].value, "line1"); + assert.strictEqual(lines[1].value, "line2"); + assert.strictEqual(lines[2].value, "line3"); +}); + +test('mappedReplace replaces string', () => { + const source = "hello world"; + const mapped = asMappedString(source); + const replaced = mappedReplace(mapped, "world", "universe"); + + assert.strictEqual(replaced.value, "hello universe"); +}); + +test('mappedReplace with regex', () => { + const source = "hello world world"; + const mapped = asMappedString(source); + const replaced = mappedReplace(mapped, /world/g, "universe"); + + assert.strictEqual(replaced.value, "hello universe universe"); +}); + +test('breakOnDelimiter splits string keeping delimiters', () => { + const source = "a,b,c"; + const mapped = asMappedString(source); + const parts = breakOnDelimiter(mapped, ","); + + assert.strictEqual(parts.length, 2); + assert.strictEqual(parts[0].value, "a,"); + assert.strictEqual(parts[1].value, "b,"); +}); + +test('mappedNormalizeNewlines handles different line endings', () => { + const source = "line1\r\nline2\nline3\r\nline4"; + const mapped = asMappedString(source); + const normalized = mappedNormalizeNewlines(mapped); + + assert.strictEqual(normalized.value, "line1\nline2\nline3\nline4"); +}); + +test('skipRegexp removes first match', () => { + const source = "hello world world"; + const mapped = asMappedString(source); + const result = skipRegexp(mapped, /world/); + + assert.strictEqual(result.value, "hello world"); +}); + +test('skipRegexpAll removes all matches', () => { + const source = "hello world world"; + const mapped = asMappedString(source); + const result = skipRegexpAll(mapped, /world/g); + + assert.strictEqual(result.value, "hello "); +}); + +test('mappedIndexToLineCol converts index to line/column', () => { + const source = "line1\nline2\nline3"; + const mapped = asMappedString(source); + const converter = mappedIndexToLineCol(mapped); + + const pos1 = converter(0); // Start of first line + const pos2 = converter(6); // Start of second line + const pos3 = converter(12); // Start of third line + + assert.strictEqual(pos1.line, 0); + assert.strictEqual(pos1.column, 0); + assert.strictEqual(pos2.line, 1); + assert.strictEqual(pos2.column, 0); + assert.strictEqual(pos3.line, 2); + assert.strictEqual(pos3.column, 0); +}); + +test('rangedSubstring creates substring with range info', () => { + const source = "hello world"; + const ranged = rangedSubstring(source, 6, 11); + + assert.strictEqual(ranged.substring, "world"); + assert.strictEqual(ranged.range.start, 6); + assert.strictEqual(ranged.range.end, 11); +}); + +test('rangedLines splits text into lines with ranges', () => { + const source = "line1\nline2\nline3"; + const lines = rangedLines(source); + + assert.strictEqual(lines.length, 3); + assert.strictEqual(lines[0].substring, "line1"); + assert.strictEqual(lines[0].range.start, 0); + assert.strictEqual(lines[0].range.end, 5); + assert.strictEqual(lines[1].substring, "line2"); + assert.strictEqual(lines[1].range.start, 6); + assert.strictEqual(lines[1].range.end, 11); +}); + +test('text utility functions work correctly', () => { + const source = "line1\nline2\nline3"; + + const textLines = lines(source); + assert.strictEqual(textLines.length, 3); + assert.strictEqual(textLines[0], "line1"); + + const normalized = normalizeNewlines("line1\r\nline2"); + assert.strictEqual(normalized, "line1\nline2"); + + const converter = indexToLineCol(source); + const pos = converter(6); + assert.strictEqual(pos.line, 1); + assert.strictEqual(pos.column, 0); +}); + +test('map function handles out-of-bounds access', () => { + const mapped = asMappedString("hello"); + + assert.strictEqual(mapped.map(-1), undefined); + assert.strictEqual(mapped.map(10), undefined); +}); + +test('map function with closest parameter', () => { + const mapped = asMappedString("hello"); + + const result1 = mapped.map(-1, true); + assert.notStrictEqual(result1, undefined); + assert.strictEqual(result1!.index, 0); + + const result2 = mapped.map(10, true); + assert.notStrictEqual(result2, undefined); + assert.strictEqual(result2!.index, 4); +}); + +test('complex mapping chain preserves original references', () => { + const original = "hello beautiful world"; + const mapped1 = asMappedString(original, "test.txt"); + const sub1 = mappedSubstring(mapped1, 6, 15); // "beautiful" + const sub2 = mappedSubstring(sub1, 0, 4); // "beau" + + assert.strictEqual(sub2.value, "beau"); + + const mapResult = sub2.map(0); + assert.notStrictEqual(mapResult, undefined); + assert.strictEqual(mapResult!.index, 6); + assert.strictEqual(mapResult!.originalString.value, original); + assert.strictEqual(mapResult!.originalString.fileName, "test.txt"); +}); From fbbc3a2976f05282be4c1ff956c44123e7e7a93b Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 17 Jun 2025 13:06:37 -0400 Subject: [PATCH 18/18] bump versions --- packages/annotated-json/package.json | 4 ++-- packages/json-validator/package.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json index ae159659..49b2e34b 100644 --- a/packages/annotated-json/package.json +++ b/packages/annotated-json/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/annotated-json", - "version": "0.1.1", + "version": "0.1.2", "description": "A data structure for storing and manipulation a JSON object together with source locations of its constituent parts.", "author": { "name": "Posit PBC" @@ -17,7 +17,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "dependencies": { - "@quarto/mapped-string": "^0.1.1", + "@quarto/mapped-string": "^0.1.2", "@quarto/tidyverse-errors": "^0.1.2", "tsconfig": "*", "typescript": "^5.4.2" diff --git a/packages/json-validator/package.json b/packages/json-validator/package.json index 5c0a03a6..17db19ca 100644 --- a/packages/json-validator/package.json +++ b/packages/json-validator/package.json @@ -1,6 +1,6 @@ { "name": "@quarto/json-validator", - "version": "0.1.1", + "version": "0.1.3", "description": "A validation library for JSON objects with an emphasis on good error messages.", "author": { "name": "Posit PBC" @@ -21,9 +21,9 @@ "build": "*", "typescript": "^5.4.2", "regexpp": "^3.2.0", - "@quarto/mapped-string": "^0.1.1", + "@quarto/mapped-string": "^0.1.2", "@quarto/tidyverse-errors": "^0.1.2", - "@quarto/annotated-json": "^0.1.1" + "@quarto/annotated-json": "^0.1.2" }, "devDependencies": { } }