diff --git a/packages/annotated-json/package.json b/packages/annotated-json/package.json new file mode 100644 index 00000000..49b2e34b --- /dev/null +++ b/packages/annotated-json/package.json @@ -0,0 +1,26 @@ +{ + "name": "@quarto/annotated-json", + "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" + }, + "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": { + "@quarto/mapped-string": "^0.1.2", + "@quarto/tidyverse-errors": "^0.1.2", + "tsconfig": "*", + "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..89f1f79c --- /dev/null +++ b/packages/annotated-json/src/annotated-yaml.ts @@ -0,0 +1,724 @@ +/* + * 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, + EitherString +} from "@quarto/mapped-string"; + +// @ts-ignore +import { load as jsYamlParse } from "./external/js-yaml.js"; + +import { QuartoJSONSchema } from "./js-yaml-quarto-schema"; +import { tidyverseInfo } from "@quarto/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 parse(yml: EitherString): AnnotatedParse { + 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; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stack: any[] = []; + const results: AnnotatedParse[] = []; + + // eslint-disable-next-line @typescript-eslint/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; + } + } + // eslint-disable-next-line @typescript-eslint/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, + // eslint-disable-next-line @typescript-eslint/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; + // eslint-disable-next-line @typescript-eslint/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) => { + // 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); + 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) => { + // 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); + 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) => { + // 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) { + 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/external/js-yaml.js b/packages/annotated-json/src/external/js-yaml.js new file mode 100644 index 00000000..7a11a83b --- /dev/null +++ b/packages/annotated-json/src/external/js-yaml.js @@ -0,0 +1,3313 @@ +/* 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 + * 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 = + /[\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/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/index.ts b/packages/annotated-json/src/index.ts new file mode 100644 index 00000000..dc630e50 --- /dev/null +++ b/packages/annotated-json/src/index.ts @@ -0,0 +1,26 @@ +/* + * 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"; +export * from "./annotated-yaml"; \ No newline at end of file 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..12e32862 --- /dev/null +++ b/packages/annotated-json/src/js-yaml-quarto-schema.ts @@ -0,0 +1,39 @@ +/* + * 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, + 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/src/types.ts b/packages/annotated-json/src/types.ts new file mode 100644 index 00000000..81127b80 --- /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 "@quarto/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 +} diff --git a/packages/annotated-json/tsconfig.json b/packages/annotated-json/tsconfig.json new file mode 100644 index 00000000..6682bf9a --- /dev/null +++ b/packages/annotated-json/tsconfig.json @@ -0,0 +1,9 @@ +{ + "exclude": ["node_modules", "dist"], + "extends": "tsconfig/base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "outDir": "./dist", + }, +} 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 new file mode 100644 index 00000000..17db19ca --- /dev/null +++ b/packages/json-validator/package.json @@ -0,0 +1,29 @@ +{ + "name": "@quarto/json-validator", + "version": "0.1.3", + "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", + "dependencies": { + "tsconfig": "*", + "build": "*", + "typescript": "^5.4.2", + "regexpp": "^3.2.0", + "@quarto/mapped-string": "^0.1.2", + "@quarto/tidyverse-errors": "^0.1.2", + "@quarto/annotated-json": "^0.1.2" + }, + "devDependencies": { } +} diff --git a/packages/json-validator/src/errors.ts b/packages/json-validator/src/errors.ts new file mode 100644 index 00000000..a18681c7 --- /dev/null +++ b/packages/json-validator/src/errors.ts @@ -0,0 +1,1037 @@ +/* + * 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 { + editDistance, // this truly needs to be in a separate package + mappedIndexToLineCol, + MappedString, + mappedString, + Range, + ErrorLocation, formatLineRange, lines +} from "@quarto/mapped-string"; + +import { possibleSchemaKeys, possibleSchemaValues } from "./schema-utils"; + +import { AnnotatedParse, JSONValue } from "@quarto/annotated-json"; + +import { + InstancePath, + LocalizedError, + ObjectSchema, + Schema, + schemaCall, + schemaDescription, + SchemaPath, + schemaType, +} from "./types"; + +//////////////////////////////////////////////////////////////////////////////// + +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/index.ts b/packages/json-validator/src/index.ts new file mode 100644 index 00000000..170ce081 --- /dev/null +++ b/packages/json-validator/src/index.ts @@ -0,0 +1,29 @@ +/* + * 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"; +export * from "./types"; +export * from "./schema"; +export { initState, setInitializer } from "./state"; +export { asMappedString } from "@quarto/mapped-string"; diff --git a/packages/json-validator/src/regexp.js b/packages/json-validator/src/regexp.js new file mode 100644 index 00000000..0ed579c9 --- /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 "regexpp"; + +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..aff6b5ba --- /dev/null +++ b/packages/json-validator/src/types.ts @@ -0,0 +1,361 @@ +/* + * types.ts + * + * Copyright (C) 2022 Posit Software, PBC + */ + +import { TidyverseError } from "@quarto/tidyverse-errors"; +import { ErrorLocation, MappedString } from "@quarto/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..df7490f5 --- /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 "@quarto/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..fa70d649 --- /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 "@quarto/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/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 new file mode 100644 index 00000000..44d7d6e9 --- /dev/null +++ b/packages/mapped-string/package.json @@ -0,0 +1,32 @@ +{ + "name": "@quarto/mapped-string", + "version": "0.1.2", + "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", + "dependencies": { + "tsconfig": "*", + "typescript": "^5.4.2", + "ansi-colors": "^4.1.3", + "@quarto/tidyverse-errors": "^0.1.2" + }, + "devDependencies": { + "tsx": "^4.7.1" + }, + "scripts": { + "build": "tsc", + "test": "node --import tsx --test test/*.test.ts" + } +} diff --git a/packages/mapped-string/src/error.ts b/packages/mapped-string/src/error.ts new file mode 100644 index 00000000..6a19863b --- /dev/null +++ b/packages/mapped-string/src/error.ts @@ -0,0 +1,186 @@ +/* + * error.ts + * + * Copyright (C) 2020-2024 Posit Software, PBC + */ + +import { mappedIndexToLineCol } from "./mapped-text"; +import { lines } from "./text"; +import { MappedString, Range } from "./types"; +import { quotedStringColor } from "@quarto/tidyverse-errors"; + +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); + } +} + +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/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..383d6a72 --- /dev/null +++ b/packages/mapped-string/src/index.ts @@ -0,0 +1,29 @@ +/* + * 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"; +export * from "./error"; +export * from "./text"; \ 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..9cc27632 --- /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 - 1) { + 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, result.index!), + asMappedString(replacement), + mappedSubstring( + str, + result.index! + 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, result.index!), + ); + pieces.push(asMappedString(replacement)); + currentRange = result.index! + 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/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"); +}); diff --git a/packages/mapped-string/tsconfig.json b/packages/mapped-string/tsconfig.json new file mode 100644 index 00000000..6682bf9a --- /dev/null +++ b/packages/mapped-string/tsconfig.json @@ -0,0 +1,9 @@ +{ + "exclude": ["node_modules", "dist"], + "extends": "tsconfig/base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "outDir": "./dist", + }, +} 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 new file mode 100644 index 00000000..613bf094 --- /dev/null +++ b/packages/tidyverse-errors/package.json @@ -0,0 +1,30 @@ +{ + "name": "@quarto/tidyverse-errors", + "version": "0.1.2", + "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", + "dependencies": { + "tsconfig": "*", + "typescript": "^5.4.2", + "ansi-colors": "^4.1.3" + }, + "devDependencies": { + "tsx": "^4.7.1" + }, + "scripts": { + "test": "node --import tsx --test test/*.test.ts" + } +} diff --git a/packages/tidyverse-errors/src/errors.ts b/packages/tidyverse-errors/src/errors.ts new file mode 100644 index 00000000..6cc91af6 --- /dev/null +++ b/packages/tidyverse-errors/src/errors.ts @@ -0,0 +1,81 @@ +// 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}`; + } +} + +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); +} + 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/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")); +}); 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 + } +} diff --git a/yarn.lock b/yarn.lock index 84e85629..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" @@ -2457,7 +2582,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== @@ -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" @@ -7910,6 +8093,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"