UNPKG

codemirror-json-schema

Version:

Codemirror 6 extensions that provide full JSONSchema support for `@codemirror/lang-json` and `codemirror-json5`

904 lines (903 loc) 41.4 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JSONCompletion = void 0; exports.jsonCompletion = jsonCompletion; const autocomplete_1 = require("@codemirror/autocomplete"); const language_1 = require("@codemirror/language"); const debug_1 = require("../utils/debug"); const node_1 = require("../utils/node"); const state_1 = require("./state"); const json_schema_library_1 = require("json-schema-library"); const json_pointers_1 = require("../utils/json-pointers"); const constants_1 = require("../constants"); const dom_1 = require("../utils/dom"); const markdown_1 = require("../utils/markdown"); const parsers_1 = require("../parsers"); const recordUtil_1 = require("../utils/recordUtil"); class CompletionCollector { constructor() { this.completions = new Map(); this.reservedKeys = new Set(); } reserve(key) { this.reservedKeys.add(key); } add(completion) { if (this.reservedKeys.has(completion.label)) { return; } this.completions.set(completion.label, completion); } } function isRealSchema(subSchema) { return !(!subSchema || (0, json_schema_library_1.isJsonError)(subSchema) || subSchema.name === "UnknownPropertyError" || subSchema.type === "undefined"); } class JSONCompletion { // private lastKnownValidData: object | null = null; constructor(opts) { var _a, _b, _c; this.opts = opts; this.originalSchema = null; /** * Inlined (expanded) top-level $ref if present. */ this.schema = null; /** * Inlined (expanded) top-level $ref if present. * Does not contain any required properties and allows any additional properties everywhere. */ this.laxSchema = null; this.mode = constants_1.MODES.JSON; this.mode = (_a = opts.mode) !== null && _a !== void 0 ? _a : constants_1.MODES.JSON; this.parser = (_c = (_b = this.opts) === null || _b === void 0 ? void 0 : _b.jsonParser) !== null && _c !== void 0 ? _c : (0, parsers_1.getDefaultParser)(this.mode); } doComplete(ctx) { var _a; const schemaFromState = (0, state_1.getJSONSchema)(ctx.state); if (this.originalSchema !== schemaFromState) { // only process schema when it changed (could be huge) this.schema = (_a = expandSchemaProperty(schemaFromState, schemaFromState)) !== null && _a !== void 0 ? _a : schemaFromState; this.laxSchema = makeSchemaLax(this.schema); } if (!this.schema || !this.laxSchema) { // todo: should we even do anything without schema // without taking over the existing mode responsibilties? return []; } // first attempt to complete with the original schema debug_1.debug.log("xxx", "trying with original schema"); const completionResultForOriginalSchema = this.doCompleteForSchema(ctx, this.schema); if (completionResultForOriginalSchema.options.length !== 0) { return completionResultForOriginalSchema; } // if there are no completions, try with the lax schema (because json-schema-library would otherwise not provide schemas if invalid properties are present) debug_1.debug.log("xxx", "no completions with original schema, trying with lax schema"); return this.doCompleteForSchema(ctx, this.laxSchema); } doCompleteForSchema(ctx, rootSchema) { var _a, _b; const result = { from: ctx.pos, to: ctx.pos, options: [], filter: false, // will be handled manually }; const text = ctx.state.doc.sliceString(0); let node = (0, node_1.getNodeAtPosition)(ctx.state, ctx.pos); // position node word prefix (without quotes) for matching let prefix = ctx.state.sliceDoc(node.from, ctx.pos).replace(/^(["'])/, ""); debug_1.debug.log("xxx", "node", node, "prefix", prefix, "ctx", ctx); // Only show completions if we are filling out a word or right after the starting quote, or if explicitly requested if (!((0, node_1.isPrimitiveValueNode)(node, this.mode) || (0, node_1.isPropertyNameNode)(node, this.mode)) && !ctx.explicit) { debug_1.debug.log("xxx", "no completions for non-word/primitive", node); return result; } const currentWord = (0, node_1.getWord)(ctx.state.doc, node); const rawWord = (0, node_1.getWord)(ctx.state.doc, node, false); // Calculate overwrite range if (node && ((0, node_1.isPrimitiveValueNode)(node, this.mode) || (0, node_1.isPropertyNameNode)(node, this.mode))) { result.from = node.from; result.to = node.to; } else { const word = ctx.matchBefore(/[A-Za-z0-9._]*/); const overwriteStart = ctx.pos - currentWord.length; debug_1.debug.log("xxx", "overwriteStart after", overwriteStart, "ctx.pos", ctx.pos, "word", word, "currentWord", currentWord, "=>", text[overwriteStart - 1], "..", text[overwriteStart], "..", text); result.from = node.name === constants_1.TOKENS.INVALID ? ((_a = word === null || word === void 0 ? void 0 : word.from) !== null && _a !== void 0 ? _a : ctx.pos) : overwriteStart; result.to = ctx.pos; } const collector = new CompletionCollector(); let addValue = true; const closestPropertyNameNode = (0, node_1.getClosestNode)(node, constants_1.TOKENS.PROPERTY_NAME, this.mode); // if we are inside a property name node, we need to get the parent property name node // The only reason we would be inside a property name node is if the current node is invalid or a literal/primitive node if (closestPropertyNameNode) { debug_1.debug.log("xxx", "closestPropertyNameNode", closestPropertyNameNode, "node", node); node = closestPropertyNameNode; } if ((0, node_1.isPropertyNameNode)(node, this.mode)) { debug_1.debug.log("xxx", "isPropertyNameNode", node); const parent = node.parent; if (parent) { // get value node from parent const valueNode = (0, node_1.getChildValueNode)(parent, this.mode); addValue = !valueNode || (valueNode.name === constants_1.TOKENS.INVALID && valueNode.from - valueNode.to === 0) || // TODO: Verify this doesn't break anything else (valueNode.parent ? (0, node_1.getChildrenNodes)(valueNode.parent).length <= 1 : false); debug_1.debug.log("xxx", "addValue", addValue, (0, node_1.getChildValueNode)(parent, this.mode), node); // find object node node = (_b = (0, node_1.getClosestNode)(parent, constants_1.TOKENS.OBJECT, this.mode)) !== null && _b !== void 0 ? _b : null; } } debug_1.debug.log("xxx", node, currentWord, ctx, "node at pos", (0, node_1.getNodeAtPosition)(ctx.state, ctx.pos)); // proposals for properties if (node && [constants_1.TOKENS.OBJECT, constants_1.TOKENS.JSON_TEXT].includes((0, json_pointers_1.resolveTokenName)(node.name, this.mode)) && ((0, node_1.isPropertyNameNode)((0, node_1.getNodeAtPosition)(ctx.state, ctx.pos), this.mode) || closestPropertyNameNode)) { // don't suggest keys when the cursor is just before the opening curly brace if (node.from === ctx.pos) { debug_1.debug.log("xxx", "no completions for just before opening brace"); return result; } // property proposals with schema this.getPropertyCompletions(rootSchema, ctx, node, collector, addValue, rawWord); } else { // proposals for values const types = {}; // value proposals with schema const res = this.getValueCompletions(rootSchema, ctx, types, collector); debug_1.debug.log("xxx", "getValueCompletions res", res); if (res) { // TODO: While this works, we also need to handle the completion from and to positions to use it // // use the value node to calculate the prefix // prefix = res.valuePrefix; // debug.log("xxx", "using valueNode prefix", prefix); } } // handle filtering result.options = Array.from(collector.completions.values()).filter((v) => (0, node_1.stripSurroundingQuotes)(v.label).startsWith(prefix)); debug_1.debug.log("xxx", "result", result, "prefix", prefix, "collector.completions", collector.completions, "reservedKeys", collector.reservedKeys); return result; } applySnippetCompletion(completion) { return (0, autocomplete_1.snippetCompletion)(typeof completion.apply !== "string" ? completion.label : completion.apply, completion); } getPropertyCompletions(rootSchema, ctx, node, collector, addValue, rawWord) { // don't suggest properties that are already present const properties = (0, node_1.getMatchingChildrenNodes)(node, constants_1.TOKENS.PROPERTY, this.mode); debug_1.debug.log("xxx", "getPropertyCompletions", node, ctx, properties); properties.forEach((p) => { const key = (0, node_1.getWord)(ctx.state.doc, (0, node_1.getMatchingChildNode)(p, constants_1.TOKENS.PROPERTY_NAME, this.mode)); collector.reserve((0, node_1.stripSurroundingQuotes)(key)); }); // TODO: Handle separatorAfter // Get matching schemas const schemas = this.getSchemas(rootSchema, ctx); debug_1.debug.log("xxx", "propertyCompletion schemas", schemas); schemas.forEach((s) => { if (typeof s !== "object") { return; } const properties = s.properties; if (properties) { Object.entries(properties).forEach(([key, value]) => { var _a, _b; if (typeof value === "object") { const description = (_a = value.description) !== null && _a !== void 0 ? _a : ""; const type = (_b = value.type) !== null && _b !== void 0 ? _b : ""; const typeStr = Array.isArray(type) ? type.toString() : type; const completion = { // label is the unquoted key which will be displayed. label: key, apply: this.getInsertTextForProperty(key, addValue, rawWord, rootSchema, value), type: "property", detail: typeStr, info: () => (0, dom_1.el)("div", { inner: (0, markdown_1.renderMarkdown)(description), }), }; collector.add(this.applySnippetCompletion(completion)); } }); } const propertyNames = s.propertyNames; if (typeof propertyNames === "object") { if (propertyNames.enum) { propertyNames.enum.forEach((v) => { const label = v === null || v === void 0 ? void 0 : v.toString(); if (label) { const completion = { label, apply: this.getInsertTextForProperty(label, addValue, rawWord, rootSchema), type: "property", }; collector.add(this.applySnippetCompletion(completion)); } }); } if (propertyNames.const) { const label = propertyNames.const.toString(); const completion = { label, apply: this.getInsertTextForProperty(label, addValue, rawWord, rootSchema), type: "property", }; collector.add(this.applySnippetCompletion(completion)); } } }); } // apply is the quoted key which will be applied. // Normally the label needs to match the token // prefix i.e. if the token begins with `"to`, then the // label needs to have the quotes as well for it to match. // However we are manually filtering the results so we can // just use the unquoted key as the label, which is nicer // and gives us more control. // If no property value is present, then we add the colon as well. // Use snippetCompletion to handle insert value + position cursor e.g. "key": "#{}" // doc: https://codemirror.net/docs/ref/#autocomplete.snippetCompletion // idea: https://discuss.codemirror.net/t/autocomplete-cursor-position-in-apply-function/4088/3 getInsertTextForProperty(key, addValue, rawWord, rootSchema, propertySchema) { // expand schema property if it is a reference propertySchema = propertySchema ? expandSchemaProperty(propertySchema, rootSchema) : propertySchema; let resultText = this.getInsertTextForPropertyName(key, rawWord); if (!addValue) { return resultText; } resultText += ": "; let value; let nValueProposals = 0; if (typeof propertySchema === "object") { if (typeof propertySchema.default !== "undefined") { if (!value) { value = this.getInsertTextForGuessedValue(propertySchema.default, ""); } nValueProposals++; } else { if (propertySchema.enum) { if (!value && propertySchema.enum.length === 1) { value = this.getInsertTextForGuessedValue(propertySchema.enum[0], ""); } nValueProposals += propertySchema.enum.length; } if (typeof propertySchema.const !== "undefined") { if (!value) { value = this.getInsertTextForGuessedValue(propertySchema.const, ""); } nValueProposals++; } if (Array.isArray(propertySchema.examples) && propertySchema.examples.length) { if (!value) { value = this.getInsertTextForGuessedValue(propertySchema.examples[0], ""); } nValueProposals += propertySchema.examples.length; } if (value === undefined && nValueProposals === 0) { let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; if (!type) { if (propertySchema.properties) { type = "object"; } else if (propertySchema.items) { type = "array"; } } switch (type) { case "boolean": value = "#{}"; break; case "string": value = this.getInsertTextForString(""); break; case "object": switch (this.mode) { case constants_1.MODES.JSON5: value = "{#{}}"; break; case constants_1.MODES.YAML: value = "#{}"; break; default: value = "{#{}}"; break; } break; case "array": value = "[#{}]"; break; case "number": case "integer": value = "#{0}"; break; case "null": value = "#{null}"; break; default: // always advance the cursor after completing a property value = "#{}"; break; } } } } if (!value || nValueProposals > 1) { debug_1.debug.log("xxx", "value", value, "nValueProposals", nValueProposals, propertySchema); value = "#{}"; } return resultText + value; } getInsertTextForPropertyName(key, rawWord) { switch (this.mode) { case constants_1.MODES.JSON5: case constants_1.MODES.YAML: { if (rawWord.startsWith('"')) { return `"${key}"`; } if (rawWord.startsWith("'")) { return `'${key}'`; } return key; } default: return `"${key}"`; } } getInsertTextForString(value, prf = "#") { switch (this.mode) { case constants_1.MODES.JSON5: return `'${prf}{${value}}'`; case constants_1.MODES.YAML: return `${prf}{${value}}`; default: return `"${prf}{${value}}"`; } } // TODO: Is this actually working? getInsertTextForGuessedValue(value, separatorAfter = "") { switch (typeof value) { case "object": if (value === null) { return "${null}" + separatorAfter; } return this.getInsertTextForValue(value, separatorAfter); case "string": { let snippetValue = JSON.stringify(value); snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } return this.getInsertTextForString(snippetValue, "$") + separatorAfter; } case "number": case "boolean": return "${" + JSON.stringify(value) + "}" + separatorAfter; } return this.getInsertTextForValue(value, separatorAfter); } getInsertTextForPlainText(text) { return text.replace(/[\\$}]/g, "\\$&"); // escape $, \ and } } getInsertTextForValue(value, separatorAfter) { const text = JSON.stringify(value, null, "\t"); if (text === "{}") { return "{#{}}" + separatorAfter; } else if (text === "[]") { return "[#{}]" + separatorAfter; } return this.getInsertTextForPlainText(text + separatorAfter); } getValueCompletions(rootSchema, ctx, types, collector) { let node = (0, language_1.syntaxTree)(ctx.state).resolveInner(ctx.pos, -1); let valueNode = null; let parentKey = undefined; debug_1.debug.log("xxx", "getValueCompletions", node, ctx); if (node && (0, node_1.isPrimitiveValueNode)(node, this.mode)) { valueNode = node; node = node.parent; } if (!node) { this.addSchemaValueCompletions(rootSchema, types, collector); return; } if ((0, json_pointers_1.resolveTokenName)(node.name, this.mode) === constants_1.TOKENS.PROPERTY) { const keyNode = (0, node_1.getMatchingChildNode)(node, constants_1.TOKENS.PROPERTY_NAME, this.mode); if (keyNode) { parentKey = (0, node_1.getWord)(ctx.state.doc, keyNode); node = node.parent; } } debug_1.debug.log("xxx", "node", node, "parentKey", parentKey); if (node && (parentKey !== undefined || (0, json_pointers_1.resolveTokenName)(node.name, this.mode) === constants_1.TOKENS.ARRAY)) { // Get matching schemas const schemas = this.getSchemas(rootSchema, ctx); for (const s of schemas) { if (typeof s !== "object") { return; } if ((0, json_pointers_1.resolveTokenName)(node.name, this.mode) === constants_1.TOKENS.ARRAY && s.items) { let c = collector; if (s.uniqueItems) { c = Object.assign(Object.assign({}, c), { add(completion) { if (!c.completions.has(completion.label)) { collector.add(completion); } }, reserve(key) { collector.reserve(key); } }); } if (Array.isArray(s.items)) { let arrayIndex = 0; if (valueNode) { // get index of next node in array const foundIdx = (0, node_1.findNodeIndexInArrayNode)(node, valueNode, this.mode); if (foundIdx >= 0) { arrayIndex = foundIdx; } } const itemSchema = s.items[arrayIndex]; if (itemSchema) { this.addSchemaValueCompletions(itemSchema, types, c); } } else { this.addSchemaValueCompletions(s.items, types, c); } } if (s.type == null || s.type !== "object") { this.addSchemaValueCompletions(s, types, collector); } if (parentKey !== undefined) { let propertyMatched = false; if (s.properties) { const propertySchema = s.properties[parentKey]; if (propertySchema) { propertyMatched = true; this.addSchemaValueCompletions(propertySchema, types, collector); } } if (s.patternProperties && !propertyMatched) { for (const pattern of Object.keys(s.patternProperties)) { const regex = this.extendedRegExp(pattern); if (regex === null || regex === void 0 ? void 0 : regex.test(parentKey)) { propertyMatched = true; const propertySchema = s.patternProperties[pattern]; if (propertySchema) { this.addSchemaValueCompletions(propertySchema, types, collector); } } } } if (s.additionalProperties && !propertyMatched) { const propertySchema = s.additionalProperties; this.addSchemaValueCompletions(propertySchema, types, collector); } } if (types["boolean"]) { this.addBooleanValueCompletion(true, collector); this.addBooleanValueCompletion(false, collector); } if (types["null"]) { this.addNullValueCompletion(collector); } } } // TODO: We need to pass the from and to for the value node as well // TODO: What should be the from and to when the value node is null? // TODO: (NOTE: if we pass a prefix but no from and to, it will autocomplete the value but replace // TODO: the entire property nodewhich isn't what we want). Instead we need to change the from and to // TODO: based on the corresponding (relevant) value node const valuePrefix = valueNode ? (0, node_1.getWord)(ctx.state.doc, valueNode, true, false) : ""; return { valuePrefix, }; } addSchemaValueCompletions(schema, // TODO this is buggy because it does not resolve refs, should hand down rootSchema and expand each ref // rootSchema: JSONSchema7, types, collector) { if (typeof schema === "object") { this.addEnumValueCompletions(schema, collector); this.addDefaultValueCompletions(schema, collector); this.collectTypes(schema, types); if (Array.isArray(schema.allOf)) { schema.allOf.forEach((s) => this.addSchemaValueCompletions(s, types, collector)); } if (Array.isArray(schema.anyOf)) { schema.anyOf.forEach((s) => this.addSchemaValueCompletions(s, types, collector)); } if (Array.isArray(schema.oneOf)) { schema.oneOf.forEach((s) => this.addSchemaValueCompletions(s, types, collector)); } } } addDefaultValueCompletions(schema, collector, arrayDepth = 0) { let hasProposals = false; if (typeof schema.default !== "undefined") { let type = schema.type; let value = schema.default; for (let i = arrayDepth; i > 0; i--) { value = [value]; type = "array"; } const completionItem = Object.assign(Object.assign({ type: type === null || type === void 0 ? void 0 : type.toString() }, this.getAppliedValue(value)), { detail: "Default value" }); collector.add(completionItem); hasProposals = true; } if (Array.isArray(schema.examples)) { schema.examples.forEach((example) => { let type = schema.type; let value = example; for (let i = arrayDepth; i > 0; i--) { value = [value]; type = "array"; } collector.add(Object.assign({ type: type === null || type === void 0 ? void 0 : type.toString() }, this.getAppliedValue(value))); hasProposals = true; }); } if (!hasProposals && typeof schema.items === "object" && !Array.isArray(schema.items) && arrayDepth < 5 /* beware of recursion */) { this.addDefaultValueCompletions(schema.items, collector, arrayDepth + 1); } } addEnumValueCompletions(schema, collector) { var _a, _b; if (typeof schema.const !== "undefined") { collector.add(Object.assign(Object.assign({ type: (_a = schema.type) === null || _a === void 0 ? void 0 : _a.toString() }, this.getAppliedValue(schema.const)), { info: schema.description })); } if (Array.isArray(schema.enum)) { for (let i = 0, length = schema.enum.length; i < length; i++) { const enm = schema.enum[i]; collector.add(Object.assign(Object.assign({ type: (_b = schema.type) === null || _b === void 0 ? void 0 : _b.toString() }, this.getAppliedValue(enm)), { info: schema.description })); } } } addBooleanValueCompletion(value, collector) { collector.add({ type: "boolean", label: value ? "true" : "false", }); } addNullValueCompletion(collector) { collector.add({ type: "null", label: "null", }); } collectTypes(schema, types) { if (Array.isArray(schema.enum) || typeof schema.const !== "undefined") { return; } const type = schema.type; if (Array.isArray(type)) { type.forEach((t) => (types[t] = true)); } else if (type) { types[type] = true; } } getSchemas(rootSchema, ctx) { var _a, _b, _c; const { data: documentData } = this.parser(ctx.state); const draft = new json_schema_library_1.Draft07(rootSchema); let pointer = (0, json_pointers_1.jsonPointerForPosition)(ctx.state, ctx.pos, -1, this.mode); // TODO make jsonPointer consistent and compatible with json-schema-library by default (root path '/' or ' ' or undefined or '#', idk) if (pointer === "") pointer = undefined; if (pointer != null && pointer.endsWith("/")) { // opening new property under pointer // the property name is empty but json-schema-library would puke itself with a trailing slash, so we shouldn't even call it with that pointer = pointer.substring(0, pointer.length - 1); // when adding a new property, we just wanna return the possible properties if possible const effectiveSchemaOfPointer = getEffectiveObjectWithPropertiesSchema(rootSchema, documentData, pointer); if (effectiveSchemaOfPointer != null) { return [effectiveSchemaOfPointer]; } } let parentPointer = pointer != null ? pointer.replace(/\/[^/]*$/, "") : undefined; if (parentPointer === "") parentPointer = undefined; // Pass parsed data to getSchema to get the correct schema based on the data context (e.g. for anyOf or if-then) const effectiveSchemaOfParent = getEffectiveObjectWithPropertiesSchema(rootSchema, documentData, parentPointer); const deepestPropertyKey = pointer === null || pointer === void 0 ? void 0 : pointer.split("/").pop(); const pointerPointsToKnownProperty = deepestPropertyKey == null || deepestPropertyKey in ((_a = effectiveSchemaOfParent === null || effectiveSchemaOfParent === void 0 ? void 0 : effectiveSchemaOfParent.properties) !== null && _a !== void 0 ? _a : {}); // TODO upgrade json-schema-library, so this actually returns undefined if data and schema are incompatible (currently it sometimes pukes itself with invalid data and imagines schemas on-the-fly) let subSchema = draft.getSchema({ pointer, data: documentData !== null && documentData !== void 0 ? documentData : undefined, }); if (!pointerPointsToKnownProperty && (subSchema === null || subSchema === void 0 ? void 0 : subSchema.type) === "null" && this.mode === "yaml") { // TODO describe YAML special-case where null is given the value and json-schema-library simply makes up a new schema based on that null value for whatever reason subSchema = undefined; } debug_1.debug.log("xxxx", "draft.getSchema", subSchema, "data", documentData, "pointer", pointer, "pointerPointsToKnownProperty", pointerPointsToKnownProperty); if ((0, json_schema_library_1.isJsonError)(subSchema)) { subSchema = (_b = subSchema.data) === null || _b === void 0 ? void 0 : _b.schema; } // if we don't have a schema for the current pointer, try the parent pointer with data to get a list of possible properties if (!isRealSchema(subSchema)) { if (effectiveSchemaOfParent) { return [effectiveSchemaOfParent]; } } // then try the parent pointer without data if (!isRealSchema(subSchema)) { subSchema = draft.getSchema({ pointer: parentPointer }); // TODO should probably only change pointer if it actually found a schema there, but i left it as-is pointer = parentPointer; } debug_1.debug.log("xxx", "pointer..", JSON.stringify(pointer)); // For some reason, it returns undefined schema for the root pointer // We use the root schema in that case as the relevant (sub)schema if (!isRealSchema(subSchema) && (!pointer || pointer === "/")) { subSchema = (_c = expandSchemaProperty(rootSchema, rootSchema)) !== null && _c !== void 0 ? _c : rootSchema; } // const subSchema = new Draft07(this.dirtyCtx.rootSchema).getSchema(pointer); debug_1.debug.log("xxx", "subSchema..", subSchema); if (!subSchema) { return []; } if (Array.isArray(subSchema.allOf)) { return [ subSchema, ...subSchema.allOf.map((s) => expandSchemaProperty(s, rootSchema)), ]; } if (Array.isArray(subSchema.oneOf)) { return [ subSchema, ...subSchema.oneOf.map((s) => expandSchemaProperty(s, rootSchema)), ]; } if (Array.isArray(subSchema.anyOf)) { return [ subSchema, ...subSchema.anyOf.map((s) => expandSchemaProperty(s, rootSchema)), ]; } return [subSchema]; } getAppliedValue(value) { const stripped = (0, node_1.stripSurroundingQuotes)(JSON.stringify(value)); switch (this.mode) { case constants_1.MODES.JSON5: return { label: stripped, apply: (0, node_1.surroundingDoubleQuotesToSingle)(JSON.stringify(value)), }; case constants_1.MODES.YAML: return { label: stripped, apply: stripped, }; default: return { label: stripped, apply: JSON.stringify(value), }; } } getValueFromLabel(value) { return JSON.parse(value); } extendedRegExp(pattern) { let flags = ""; if (pattern.startsWith("(?i)")) { pattern = pattern.substring(4); flags = "i"; } try { return new RegExp(pattern, flags + "u"); } catch (e) { // could be an exception due to the 'u ' flag try { return new RegExp(pattern, flags); } catch (e) { // invalid pattern return undefined; } } } } exports.JSONCompletion = JSONCompletion; /** * provides a JSON schema enabled autocomplete extension for codemirror * @group Codemirror Extensions */ function jsonCompletion(opts = {}) { const completion = new JSONCompletion(opts); return function jsonDoCompletion(ctx) { return completion.doComplete(ctx); }; } /** * removes required properties and allows additional properties everywhere * @param schema */ function makeSchemaLax(schema) { return (0, recordUtil_1.replacePropertiesDeeply)(schema, (key, value) => { if (key === "additionalProperties" && value === false) { return []; } if (key === "required" && Array.isArray(value)) { return []; } if (key === "unevaluatedProperties" && value === false) { return []; } if (key === "unevaluatedItems" && value === false) { return []; } // TODO remove dependencies and other restrictions // if (key === 'dependencies' && typeof value === 'object') { // return Object.keys(value).reduce((acc: any, depKey) => { // const depValue = value[depKey]; // if (Array.isArray(depValue)) { // return acc; // } // return { ...acc, [depKey]: depValue }; // }, {}); // } return [key, value]; }); } /** * determines effective object schema for given data * TODO support patternProperties, etc. * @param schema * @param data * @param pointer */ function getEffectiveObjectWithPropertiesSchema(schema, data, pointer) { // TODO (unimportant): [performance] cache Draft07 in case it does some pre-processing? but does not seem to be significant const draft = new json_schema_library_1.Draft07(schema); const subSchema = draft.getSchema({ pointer, data: data !== null && data !== void 0 ? data : undefined, }); if (!isRealSchema(subSchema)) { return undefined; } const possibleDirectPropertyNames = getAllPossibleDirectStaticPropertyNames(draft, subSchema); const effectiveProperties = {}; for (let possibleDirectPropertyName of possibleDirectPropertyNames) { let propertyPointer = extendJsonPointer(pointer, possibleDirectPropertyName); const subSchemaForPropertyConsideringData = draft.getSchema({ // TODO [performance] use subSchema and only check it's sub-properties pointer: propertyPointer, data: data !== null && data !== void 0 ? data : undefined, // pointer: `/${possibleDirectPropertyName}`, // schema: subSchema }); if (isRealSchema(subSchemaForPropertyConsideringData)) { Object.assign(effectiveProperties, { [possibleDirectPropertyName]: subSchemaForPropertyConsideringData, }); } } if (possibleDirectPropertyNames.length === 0 || Object.keys(effectiveProperties).length === 0) { // in case json-schema-library behaves too weirdly and returns nothing, just return no schema too to let other cases handle this edge-case return undefined; } // TODO also resolve patternProperties of allOf, anyOf, oneOf const _a = subSchema, { allOf, anyOf, oneOf } = _a, subSchemaRest = __rest(_a, ["allOf", "anyOf", "oneOf"]); return Object.assign(Object.assign({}, subSchemaRest), { properties: effectiveProperties }); } /** * static means not from patternProperties * @param rootDraft * @param schema */ function getAllPossibleDirectStaticPropertyNames(rootDraft, schema) { schema = expandSchemaProperty(schema, rootDraft.rootSchema); if (typeof schema !== "object" || schema == null) { return []; } const possiblePropertyNames = []; function addFrom(subSchema) { const possiblePropertyNamesOfSubSchema = getAllPossibleDirectStaticPropertyNames(rootDraft, subSchema); possiblePropertyNames.push(...possiblePropertyNamesOfSubSchema); } if (typeof schema.properties === "object" && schema.properties != null) { possiblePropertyNames.push(...Object.keys(schema.properties)); } if (typeof schema.then === "object" && schema.then != null) { addFrom(schema.then); } if (Array.isArray(schema.allOf)) { for (const subSchema of schema.allOf) { addFrom(subSchema); } } if (Array.isArray(schema.anyOf)) { for (const subSchema of schema.anyOf) { addFrom(subSchema); } } if (Array.isArray(schema.oneOf)) { for (const subSchema of schema.oneOf) { addFrom(subSchema); } } return possiblePropertyNames; } function expandSchemaProperty(propertySchema, rootSchema) { if (typeof propertySchema === "object" && propertySchema.$ref) { const refSchema = getReferenceSchema(rootSchema, propertySchema.$ref); if (typeof refSchema === "object") { const dereferenced = Object.assign(Object.assign({}, propertySchema), refSchema); Reflect.deleteProperty(dereferenced, "$ref"); return dereferenced; } } return propertySchema; } function getReferenceSchema(schema, ref) { const refPath = ref.split("/"); let curReference = schema; refPath.forEach((cur) => { if (!cur) { return; } if (cur === "#") { curReference = schema; return; } if (typeof curReference === "object") { curReference = curReference[cur]; } }); return curReference; } function extendJsonPointer(pointer, key) { return pointer === undefined ? `/${key}` : `${pointer}/${key}`; }