diff --git a/gulpfile.js b/gulpfile.js index ba6978a..1cc499d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -59,8 +59,24 @@ gulp.task('release', ['clean-release','compile'], function() { 'vs/language/json': __dirname + '/out' }, packages: [{ + name: 'vscode-yaml-languageservice', + location: __dirname + '/out/vscode-yaml-languageservice/', + main: 'yamlLanguageService' + }, { + name: 'yaml-language-server', + location: __dirname + '/out/yaml-language-server', + main: 'yamlLanguageService' + }, { + name: 'yaml-ast-parser', + location: __dirname + '/out/yaml-ast-parser', + main: 'index' + }, { + name: 'js-yaml', + location: __dirname + '/node_modules/js-yaml/dist', + main: 'js-yaml' + }, { name: 'vscode-json-languageservice', - location: __dirname + '/node_modules/vscode-json-languageservice/lib', + location: __dirname + '/node_modules/vscode-json-languageservice', main: 'jsonLanguageService' }, { name: 'vscode-languageserver-types', @@ -82,6 +98,10 @@ gulp.task('release', ['clean-release','compile'], function() { name: 'vscode-nls', location: __dirname + '/out/fillers', main: 'vscode-nls' + }, { + name: 'os', + location: __dirname + '/out/fillers', + main: 'os' }] }) } diff --git a/package.json b/package.json index 26f866e..174fd83 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,14 @@ "gulp-requirejs": "^0.1.3", "gulp-tsb": "^2.0.0", "gulp-uglify": "^1.5.3", + "jsonc-parser": "1.0.0", "merge-stream": "^1.0.0", "monaco-editor-core": "^0.10.1", "monaco-languages": "^0.9.0", "object-assign": "^4.1.0", "rimraf": "^2.5.2", "typescript": "^2.7.1", - "vscode-languageserver-types": "^3.5.0", - "vscode-json-languageservice": "^3.0.5" + "vscode-json-languageservice": "^3.0.5", + "vscode-languageserver-types": "^3.5.0" } } diff --git a/src/fillers/os.ts b/src/fillers/os.ts new file mode 100644 index 0000000..0f7b565 --- /dev/null +++ b/src/fillers/os.ts @@ -0,0 +1 @@ +export const EOL = '\n'; \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/documentPositionCalculator.ts b/src/vscode-yaml-languageservice/documentPositionCalculator.ts new file mode 100644 index 0000000..b408a38 --- /dev/null +++ b/src/vscode-yaml-languageservice/documentPositionCalculator.ts @@ -0,0 +1,62 @@ +"use strict" + +export function insertionPointReturnValue(pt: number) { + return ((-pt) - 1) +} + +export function binarySearch(array: number[], sought: number) { + + let lower = 0 + let upper = array.length - 1 + + while (lower <= upper) { + let idx = Math.floor((lower + upper) / 2) + const value = array[idx] + + if (value === sought) { + return idx; + } + + if (lower === upper) { + const insertionPoint = (value < sought) ? idx + 1 : idx + return insertionPointReturnValue(insertionPoint) + } + + if (sought > value) { + lower = idx + 1; + } else if (sought < value) { + upper = idx - 1; + } + } +} + +export function getLineStartPositions(text: string) { + const lineStartPositions = [0]; + for (var i = 0; i < text.length; i++) { + const c = text[i]; + + if (c === '\r') { + // Check for Windows encoding, otherwise we are old Mac + if (i + 1 < text.length && text[i + 1] == '\n') { + i++; + } + + lineStartPositions.push(i + 1); + } else if (c === '\n'){ + lineStartPositions.push(i + 1); + } + } + + return lineStartPositions; +} + +export function getPosition(pos: number, lineStartPositions: number[]){ + let line = binarySearch(lineStartPositions, pos) + + if (line < 0){ + const insertionPoint = -1 * line - 1; + line = insertionPoint - 1; + } + + return {line, column: pos - lineStartPositions[line]} +} \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/parser/yamlParser.ts b/src/vscode-yaml-languageservice/parser/yamlParser.ts new file mode 100644 index 0000000..2eb73d2 --- /dev/null +++ b/src/vscode-yaml-languageservice/parser/yamlParser.ts @@ -0,0 +1,326 @@ +'use strict'; + +import { JSONDocument, ASTNode, ErrorCode, BooleanASTNode, NullASTNode, ArrayASTNode, NumberASTNode, ObjectASTNode, PropertyASTNode, StringASTNode, IError, IApplicableSchema } from 'vscode-json-languageservice/lib/parser/jsonParser'; +import { JSONSchema } from 'vscode-json-languageservice/lib/jsonSchema'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import * as Yaml from '../../yaml-ast-parser/index' +import { Kind } from '../../yaml-ast-parser/index' + +import { getLineStartPositions, getPosition } from '../documentPositionCalculator' + +export class SingleYAMLDocument extends JSONDocument { + private lines; + public root; + + constructor(lines: number[]) { + super({ disallowComments: false, ignoreDanglingComma: true }); + this.lines = lines; + } + + // TODO: This is complicated, messy and probably buggy + // It should be re-written. + // To get the correct behavior, it probably needs to be aware of + // the type of the nodes it is processing since there are no delimiters + // like in JSON. (ie. so it correctly returns 'object' vs 'property') + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + if (!this.root) { + return; + } + if (offset < this.root.start || offset > this.root.end) { + // We somehow are completely outside the document + // This is unexpected + console.log("Attempting to resolve node outside of document") + return null; + } + + const children = this.root.getChildNodes() + + function* sliding2(nodes: ASTNode[]) { + var i = 0; + while (i < nodes.length) { + yield [nodes[i], (i === nodes.length) ? null : nodes[i + 1]] + i++; + } + } + + const onLaterLine = (offset: number, node: ASTNode) => { + const { line: actualLine } = getPosition(offset, this.lines) + const { line: nodeEndLine } = getPosition(node.end, this.lines) + + return actualLine > nodeEndLine; + } + + let findNode = (nodes: ASTNode[]): ASTNode => { + if (nodes.length === 0) { + return null; + } + + var gen = sliding2(nodes); + + let result: IteratorResult = { done: false, value: undefined } + + for (let [first, second] of gen) { + const end = (second) ? second.start : first.parent.end + if (offset >= first.start && offset < end) { + const children = first.getChildNodes(); + + const foundChild = findNode(children) + + if (foundChild) { + if (foundChild['isKey'] && foundChild.end < offset) { + return foundChild.parent; + } + + if (foundChild.type === "null") { + return null; + } + } + + if (!foundChild && onLaterLine(offset, first)) { + return this.getNodeByIndent(this.lines, offset, this.root) + } + + return foundChild || first; + } + } + + return null; + } + + return findNode(children) || this.root; + } + + public getNodeFromOffset(offset: number): ASTNode { + return this.getNodeFromOffsetEndInclusive(offset); + } + + private getNodeByIndent = (lines: number[], offset: number, node: ASTNode) => { + + const { line, column: indent } = getPosition(offset, this.lines) + + const children = node.getChildNodes() + + function findNode(children) { + for (var idx = 0; idx < children.length; idx++) { + var child = children[idx]; + + const { line: childLine, column: childCol } = getPosition(child.start, lines); + + if (childCol > indent) { + return null; + } + + const newChildren = child.getChildNodes() + const foundNode = findNode(newChildren) + + if (foundNode) { + return foundNode; + } + + // We have the right indentation, need to return based on line + if (childLine == line) { + return child; + } + if (childLine > line) { + // Get previous + (idx - 1) >= 0 ? children[idx - 1] : child; + } + // Else continue loop to try next element + } + + // Special case, we found the correct + return children[children.length - 1] + } + + return findNode(children) || node + } +} + + +function recursivelyBuildAst(parent: ASTNode, node: Yaml.YAMLNode): ASTNode { + + if (!node) { + return; + } + + switch (node.kind) { + case Yaml.Kind.MAP: { + const instance = node; + + const result = new ObjectASTNode(parent, null, node.startPosition, node.endPosition) + result.addProperty + + for (const mapping of instance.mappings) { + result.addProperty(recursivelyBuildAst(result, mapping)) + } + + return result; + } + case Yaml.Kind.MAPPING: { + const instance = node; + const key = instance.key; + + // Technically, this is an arbitrary node in YAML + // I doubt we would get a better string representation by parsing it + const keyNode = new StringASTNode(null, null, true, key.startPosition, key.endPosition); + keyNode.value = key.value; + + const result = new PropertyASTNode(parent, keyNode) + result.end = instance.endPosition + + const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNode(parent, key.value, instance.endPosition, instance.endPosition) + valueNode.location = key.value + + result.setValue(valueNode) + + return result; + } + case Yaml.Kind.SEQ: { + const instance = node; + + const result = new ArrayASTNode(parent, null, instance.startPosition, instance.endPosition); + + let count = 0; + for (const item of instance.items) { + if (item === null && count === instance.items.length - 1) { + break; + } + + // Be aware of https://github.com/nodeca/js-yaml/issues/321 + // Cannot simply work around it here because we need to know if we are in Flow or Block + var itemNode = (item === null) ? new NullASTNode(parent, null, instance.endPosition, instance.endPosition) : recursivelyBuildAst(result, item); + + itemNode.location = count++; + result.addItem(itemNode); + } + + return result; + } + case Yaml.Kind.SCALAR: { + const instance = node; + + const type = Yaml.determineScalarType(instance) + + // The name is set either by the sequence or the mapping case. + const name = null; + const value = instance.value; + + switch (type) { + case Yaml.ScalarType.null: { + return new NullASTNode(parent, name, instance.startPosition, instance.endPosition); + } + case Yaml.ScalarType.bool: { + return new BooleanASTNode(parent, name, Yaml.parseYamlBoolean(value), node.startPosition, node.endPosition) + } + case Yaml.ScalarType.int: { + const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); + result.value = Yaml.parseYamlInteger(value); + result.isInteger = true; + return result; + } + case Yaml.ScalarType.float: { + const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); + result.value = Yaml.parseYamlFloat(value); + result.isInteger = false; + return result; + } + case Yaml.ScalarType.string: { + const result = new StringASTNode(parent, name, false, node.startPosition, node.endPosition); + result.value = node.value; + return result; + } + } + + break; + } + case Yaml.Kind.ANCHOR_REF: { + const instance = (node).value + + return recursivelyBuildAst(parent, instance) || + new NullASTNode(parent, null, node.startPosition, node.endPosition); + } + case Yaml.Kind.INCLUDE_REF: { + // Issue Warning + console.log("Unsupported feature, node kind: " + node.kind); + break; + } + } +} + +function convertError(e: Yaml.YAMLException) { + // Subtract 2 because \n\0 is added by the parser (see loader.ts/loadDocuments) + const bufferLength = e.mark.buffer.length - 2; + + // TODO determine correct positioning. + return { message: `${e.message}`, location: { start: Math.min(e.mark.position, bufferLength - 1), end: bufferLength, code: ErrorCode.Undefined } } +} + +function createJSONDocument(yamlDoc: Yaml.YAMLNode, startPositions: number[]) { + let _doc = new SingleYAMLDocument(startPositions); + _doc.root = recursivelyBuildAst(null, yamlDoc) + + if (!_doc.root) { + // TODO: When this is true, consider not pushing the other errors. + _doc.errors.push({ message: localize('Invalid symbol', 'Expected a YAML object, array or literal'), code: ErrorCode.Undefined, location: { start: yamlDoc.startPosition, end: yamlDoc.endPosition } }); + } + + const duplicateKeyReason = 'duplicate key' + + const errors = yamlDoc.errors.filter(e => e.reason !== duplicateKeyReason && !e.isWarning).map(e => convertError(e)) + const warnings = yamlDoc.errors.filter(e => e.reason === duplicateKeyReason || e.isWarning).map(e => convertError(e)) + + errors.forEach(e => _doc.errors.push(e)); + warnings.forEach(e => _doc.warnings.push(e)); + + return _doc; +} + +export class YAMLDocument { + public documents: JSONDocument[] + + constructor(documents: JSONDocument[]) { + this.documents = documents; + } + + get errors(): IError[] { + return ([]).concat(...this.documents.map(d => d.errors)) + } + + get warnings(): IError[] { + return ([]).concat(...this.documents.map(d => d.warnings)) + } + + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + return this.getNodeFromOffset(offset); + } + public getNodeFromOffset(offset: number): ASTNode { + // Depends on the documents being sorted + for (let element of this.documents) { + if (offset <= element.root.end) { + return element.getNodeFromOffset(offset) + } + } + + return undefined; + } + + public validate(schema: JSONSchema, matchingSchemas: IApplicableSchema[] = null, offset: number = -1): void { + this.documents.forEach(doc => { + doc.validate(schema, matchingSchemas, offset) + }); + } +} + +export function parse(text: string): YAMLDocument { + + const startPositions = getLineStartPositions(text) + // This is documented to return a YAMLNode even though the + // typing only returns a YAMLDocument + const yamlDocs = [] + Yaml.loadAll(text, doc => yamlDocs.push(doc), {}) + + return new YAMLDocument(yamlDocs.map(doc => createJSONDocument(doc, startPositions))); +} \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/services/yamlCompletion.ts b/src/vscode-yaml-languageservice/services/yamlCompletion.ts new file mode 100644 index 0000000..e2de544 --- /dev/null +++ b/src/vscode-yaml-languageservice/services/yamlCompletion.ts @@ -0,0 +1,8 @@ +"use strict" + +import { TextDocument } from "vscode-languageserver-types"; + +export function isInComment(document: TextDocument, start: number, offset: number) { + const text = document.getText().substr(start, offset); + return /(?:^|\s+)#/.test(text); +} \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/services/yamlFormatter.ts b/src/vscode-yaml-languageservice/services/yamlFormatter.ts new file mode 100644 index 0000000..c867d9f --- /dev/null +++ b/src/vscode-yaml-languageservice/services/yamlFormatter.ts @@ -0,0 +1,26 @@ +'use strict'; + +import jsyaml = require('js-yaml') +import { EOL } from 'os'; +import { TextDocument, Range, Position, FormattingOptions, TextEdit } from 'vscode-languageserver-types'; + +export function format(document: TextDocument, options: FormattingOptions): TextEdit[] { + const text = document.getText() + + const documents = [] + jsyaml.loadAll(text, doc => documents.push(doc)) + + const dumpOptions = { indent: options.tabSize, noCompatMode: true }; + + let newText; + if (documents.length == 1) { + const yaml = documents[0] + newText = jsyaml.safeDump(yaml, dumpOptions) + } + else { + const formatted = documents.map(d => jsyaml.safeDump(d, dumpOptions)) + newText = '%YAML 1.2' + EOL + '---' + EOL + formatted.join('...' + EOL + '---' + EOL) + '...' + EOL + } + + return [TextEdit.replace(Range.create(Position.create(0, 0), document.positionAt(text.length)), newText)] +} \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/tsconfig.json b/src/vscode-yaml-languageservice/tsconfig.json new file mode 100644 index 0000000..7401570 --- /dev/null +++ b/src/vscode-yaml-languageservice/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "umd", + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "stripInternal": true, + "outDir": "../lib", + "lib": [ + "es6" + ], + "baseUrl": "../types", + "typeRoots": [ + "../node_modules/@types", + "../types" + ] + } +} \ No newline at end of file diff --git a/src/vscode-yaml-languageservice/yamlLanguageService.ts b/src/vscode-yaml-languageservice/yamlLanguageService.ts new file mode 100644 index 0000000..8aec1d5 --- /dev/null +++ b/src/vscode-yaml-languageservice/yamlLanguageService.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Adam Voss. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, + TextEdit, FormattingOptions, MarkedString} from 'vscode-languageserver-types'; + +import {JSONCompletion} from 'vscode-json-languageservice/lib/services/jsonCompletion'; +import {JSONHover} from 'vscode-json-languageservice/lib/services/jsonHover'; +import {JSONValidation} from 'vscode-json-languageservice/lib/services/jsonValidation'; +import {JSONSchema} from 'vscode-json-languageservice/lib/jsonSchema'; +import {JSONDocumentSymbols} from 'vscode-json-languageservice/lib/services/jsonDocumentSymbols'; +import {parse as JSONDocumentConfig} from 'vscode-json-languageservice/lib/parser/jsonParser'; + +import {parse as parseYAML} from './parser/yamlParser'; +import {isInComment} from './services/yamlCompletion' +import {format as formatYAML} from './services/yamlFormatter'; + +import {schemaContributions} from 'vscode-json-languageservice/lib/services/configuration'; +import {JSONSchemaService} from 'vscode-json-languageservice/lib/services/jsonSchemaService'; +import {JSONWorkerContribution, JSONPath, Segment, CompletionsCollector} from 'vscode-json-languageservice/lib/jsonContributions'; + +export type JSONDocument = {} +export type YAMLDocument = { documents: JSONDocument[]} +export {JSONSchema, JSONWorkerContribution, JSONPath, Segment, CompletionsCollector}; +export {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, + TextEdit, FormattingOptions, MarkedString}; + +export interface LanguageService { + configure(settings: LanguageSettings): void; + doValidation(document: TextDocument, yamlDocument: YAMLDocument): Thenable; + parseYAMLDocument(document: TextDocument): YAMLDocument; + resetSchema(uri: string): boolean; + doResolve(item: CompletionItem): Thenable; + doComplete(document: TextDocument, position: Position, doc: YAMLDocument): Thenable; + findDocumentSymbols(document: TextDocument, doc: YAMLDocument): SymbolInformation[]; + doHover(document: TextDocument, position: Position, doc: YAMLDocument): Thenable; + format(document: TextDocument, options: FormattingOptions): TextEdit[]; +} + +export interface LanguageSettings { + /** + * If set, the validator will return syntax errors. + */ + validate?: boolean; + + /** + * A list of known schemas and/or associations of schemas to file names. + */ + schemas?: SchemaConfiguration[]; +} + +export interface SchemaConfiguration { + /** + * The URI of the schema, which is also the identifier of the schema. + */ + uri: string; + /** + * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' + */ + fileMatch?: string[]; + /** + * The schema for the given URI. + * If no schema is provided, the schema will be fetched with the schema request service (if available). + */ + schema?: JSONSchema; +} + +export interface WorkspaceContextService { + resolveRelativePath(relativePath: string, resource: string): string; +} +/** + * The schema request service is used to fetch schemas. The result should the schema file comment, or, + * in case of an error, a displayable error string + */ +export interface SchemaRequestService { + (uri: string): Thenable; +} + +export interface PromiseConstructor { + /** + * Creates a new Promise. + * @param executor A callback used to initialize the promise. This callback is passed two arguments: + * a resolve callback used resolve the promise with a value or the result of another promise, + * and a reject callback used to reject the promise with a provided reason or error. + */ + new (executor: (resolve: (value?: T | Thenable) => void, reject: (reason?: any) => void) => void): Thenable; + + /** + * Creates a Promise that is resolved with an array of results when all of the provided Promises + * resolve, or rejected when any Promise is rejected. + * @param values An array of Promises. + * @returns A new Promise. + */ + all(values: Array>): Thenable; + /** + * Creates a new rejected promise for the provided reason. + * @param reason The reason the promise was rejected. + * @returns A new rejected Promise. + */ + reject(reason: any): Thenable; + + /** + * Creates a new resolved promise for the provided value. + * @param value A promise. + * @returns A promise whose internal state matches the provided promise. + */ + resolve(value: T | Thenable): Thenable; + +} + +export interface Thenable { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; +} + +export interface LanguageServiceParams { + /** + * The schema request service is used to fetch schemas. The result should the schema file comment, or, + * in case of an error, a displayable error string + */ + schemaRequestService?: SchemaRequestService; + /** + * The workspace context is used to resolve relative paths for relative schema references. + */ + workspaceContext?: WorkspaceContextService; + /** + * An optional set of completion and hover participants. + */ + contributions?: JSONWorkerContribution[]; + /** + * A promise constructor. If not set, the ES5 Promise will be used. + */ + promiseConstructor?: PromiseConstructor; +} + +export function getLanguageService(params: LanguageServiceParams): LanguageService { + let promise = params.promiseConstructor || Promise; + + let jsonSchemaService = new JSONSchemaService(params.schemaRequestService, params.workspaceContext, promise); + jsonSchemaService.setSchemaContributions(schemaContributions); + + let jsonCompletion = new JSONCompletion(jsonSchemaService, params.contributions, promise); + jsonCompletion['isInComment'] = isInComment.bind(jsonCompletion); + + let jsonHover = new JSONHover(jsonSchemaService, params.contributions, promise); + let jsonDocumentSymbols = new JSONDocumentSymbols(jsonSchemaService); + let jsonValidation = new JSONValidation(jsonSchemaService, promise); + + + function doValidation(textDocument: TextDocument, yamlDocument: YAMLDocument) { + var validate: (JSONDocument) => Thenable = + jsonValidation.doValidation.bind(jsonValidation, textDocument) + const validationResults = yamlDocument.documents.map(d => validate(d)) + const resultsPromise = promise.all(validationResults); + return resultsPromise.then(res => ([]).concat(...res)) + } + + return { + configure: (settings: LanguageSettings) => { + jsonSchemaService.clearExternalSchemas(); + if (settings.schemas) { + settings.schemas.forEach(settings => { + jsonSchemaService.registerExternalSchema(settings.uri, settings.fileMatch, settings.schema); + }); + }; + jsonValidation.configure(settings); + }, + resetSchema: (uri: string) => jsonSchemaService.onResourceChange(uri), + doValidation: doValidation, + parseYAMLDocument : (document: TextDocument) => parseYAML(document.getText()), + doResolve: jsonCompletion.doResolve.bind(jsonCompletion), + doComplete: jsonCompletion.doComplete.bind(jsonCompletion), + findDocumentSymbols: jsonDocumentSymbols.findDocumentSymbols.bind(jsonDocumentSymbols), + doHover: jsonHover.doHover.bind(jsonHover), + format: formatYAML + }; +} diff --git a/src/yaml-ast-parser/common.ts b/src/yaml-ast-parser/common.ts new file mode 100644 index 0000000..1536b56 --- /dev/null +++ b/src/yaml-ast-parser/common.ts @@ -0,0 +1,52 @@ + + +export function isNothing(subject) { + return (typeof subject === 'undefined') || (null === subject); +} + + +export function isObject(subject) { + return (typeof subject === 'object') && (null !== subject); +} + + +export function toArray(sequence) { + if (Array.isArray(sequence)) { + return sequence; + } else if (isNothing(sequence)) { + return []; + } + return [sequence]; +} + + +export 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; +} + + +export function repeat(string, count) { + var result = '', cycle; + + for (cycle = 0; cycle < count; cycle += 1) { + result += string; + } + + return result; +} + + +export function isNegativeZero(number) { + return (0 === number) && (Number.NEGATIVE_INFINITY === 1 / number); +} diff --git a/src/yaml-ast-parser/dumper.ts b/src/yaml-ast-parser/dumper.ts new file mode 100644 index 0000000..b6c59a2 --- /dev/null +++ b/src/yaml-ast-parser/dumper.ts @@ -0,0 +1,831 @@ +/*eslint-disable no-use-before-define*/ + +import * as common from './common'; +import YAMLException from './exception'; +import DEFAULT_FULL_SCHEMA from './schema/default_full'; +import DEFAULT_SAFE_SCHEMA from './schema/default_safe'; + +var _toString = Object.prototype.toString; +var _hasOwnProperty = Object.prototype.hasOwnProperty; + +var CHAR_TAB = 0x09; /* Tab */ +var CHAR_LINE_FEED = 0x0A; /* LF */ +var CHAR_CARRIAGE_RETURN = 0x0D; /* CR */ +var CHAR_SPACE = 0x20; /* Space */ +var CHAR_EXCLAMATION = 0x21; /* ! */ +var CHAR_DOUBLE_QUOTE = 0x22; /* " */ +var CHAR_SHARP = 0x23; /* # */ +var CHAR_PERCENT = 0x25; /* % */ +var CHAR_AMPERSAND = 0x26; /* & */ +var CHAR_SINGLE_QUOTE = 0x27; /* ' */ +var CHAR_ASTERISK = 0x2A; /* * */ +var CHAR_COMMA = 0x2C; /* , */ +var CHAR_MINUS = 0x2D; /* - */ +var CHAR_COLON = 0x3A; /* : */ +var CHAR_GREATER_THAN = 0x3E; /* > */ +var CHAR_QUESTION = 0x3F; /* ? */ +var CHAR_COMMERCIAL_AT = 0x40; /* @ */ +var CHAR_LEFT_SQUARE_BRACKET = 0x5B; /* [ */ +var CHAR_RIGHT_SQUARE_BRACKET = 0x5D; /* ] */ +var CHAR_GRAVE_ACCENT = 0x60; /* ` */ +var CHAR_LEFT_CURLY_BRACKET = 0x7B; /* { */ +var CHAR_VERTICAL_LINE = 0x7C; /* | */ +var CHAR_RIGHT_CURLY_BRACKET = 0x7D; /* } */ + +var ESCAPE_SEQUENCES = {}; + +ESCAPE_SEQUENCES[0x00] = '\\0'; +ESCAPE_SEQUENCES[0x07] = '\\a'; +ESCAPE_SEQUENCES[0x08] = '\\b'; +ESCAPE_SEQUENCES[0x09] = '\\t'; +ESCAPE_SEQUENCES[0x0A] = '\\n'; +ESCAPE_SEQUENCES[0x0B] = '\\v'; +ESCAPE_SEQUENCES[0x0C] = '\\f'; +ESCAPE_SEQUENCES[0x0D] = '\\r'; +ESCAPE_SEQUENCES[0x1B] = '\\e'; +ESCAPE_SEQUENCES[0x22] = '\\"'; +ESCAPE_SEQUENCES[0x5C] = '\\\\'; +ESCAPE_SEQUENCES[0x85] = '\\N'; +ESCAPE_SEQUENCES[0xA0] = '\\_'; +ESCAPE_SEQUENCES[0x2028] = '\\L'; +ESCAPE_SEQUENCES[0x2029] = '\\P'; + +var DEPRECATED_BOOLEANS_SYNTAX = [ + 'y', 'Y', 'yes', 'Yes', 'YES', 'on', 'On', 'ON', + 'n', 'N', 'no', 'No', 'NO', 'off', 'Off', 'OFF' +]; + +function compileStyleMap(schema, map) { + var result, keys, index, length, tag, style, type; + + if (null === map) { + return {}; + } + + result = {}; + keys = Object.keys(map); + + for (index = 0, length = keys.length; index < length; index += 1) { + tag = keys[index]; + style = String(map[tag]); + + if ('!!' === tag.slice(0, 2)) { + tag = 'tag:yaml.org,2002:' + tag.slice(2); + } + + type = schema.compiledTypeMap[tag]; + + if (type && _hasOwnProperty.call(type.styleAliases, style)) { + style = type.styleAliases[style]; + } + + result[tag] = style; + } + + return result; +} + +function encodeHex(character) { + var string, handle, length; + + string = character.toString(16).toUpperCase(); + + if (character <= 0xFF) { + handle = 'x'; + length = 2; + } else if (character <= 0xFFFF) { + handle = 'u'; + length = 4; + } else if (character <= 0xFFFFFFFF) { + handle = 'U'; + length = 8; + } else { + throw new YAMLException('code point within a string may not be greater than 0xFFFFFFFF'); + } + + return '\\' + handle + common.repeat('0', length - string.length) + string; +} + +function State(options) { + this.schema = options['schema'] || DEFAULT_FULL_SCHEMA; + this.indent = Math.max(1, (options['indent'] || 2)); + this.skipInvalid = options['skipInvalid'] || false; + this.flowLevel = (common.isNothing(options['flowLevel']) ? -1 : options['flowLevel']); + this.styleMap = compileStyleMap(this.schema, options['styles'] || null); + + this.implicitTypes = this.schema.compiledImplicit; + this.explicitTypes = this.schema.compiledExplicit; + + this.tag = null; + this.result = ''; + + this.duplicates = []; + this.usedDuplicates = null; +} + +function indentString(string: 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, str) { + var index, length, type; + + for (index = 0, length = state.implicitTypes.length; index < length; index += 1) { + type = state.implicitTypes[index]; + + if (type.resolve(str)) { + return true; + } + } + + return false; +} + +function StringBuilder(source) { + this.source = source; + this.result = ''; + this.checkpoint = 0; +} + +StringBuilder.prototype.takeUpTo = function (position) { + var er; + + if (position < this.checkpoint) { + er = new Error('position should be > checkpoint'); + er.position = position; + er.checkpoint = this.checkpoint; + throw er; + } + + this.result += this.source.slice(this.checkpoint, position); + this.checkpoint = position; + return this; +}; + +StringBuilder.prototype.escapeChar = function () { + var character, esc; + + character = this.source.charCodeAt(this.checkpoint); + esc = ESCAPE_SEQUENCES[character] || encodeHex(character); + this.result += esc; + this.checkpoint += 1; + + return this; +}; + +StringBuilder.prototype.finish = function () { + if (this.source.length > this.checkpoint) { + this.takeUpTo(this.source.length); + } +}; + +function writeScalar(state, object, level) { + var simple, first, spaceWrap, folded, literal, single, double, + sawLineFeed, linePosition, longestLine, indent, max, character, + position, escapeSeq, hexEsc, previous, lineLength, modifier, + trailingLineBreaks, result; + + if (0 === object.length) { + state.dump = "''"; + return; + } + if (object.indexOf("!include") == 0) { + state.dump = "" + object;//FIXME + return; + } + if (object.indexOf("!$$$novalue") == 0) { + state.dump = "";//FIXME + return; + } + if (-1 !== DEPRECATED_BOOLEANS_SYNTAX.indexOf(object)) { + state.dump = "'" + object + "'"; + return; + } + + simple = true; + first = object.length ? object.charCodeAt(0) : 0; + spaceWrap = (CHAR_SPACE === first || + CHAR_SPACE === object.charCodeAt(object.length - 1)); + + // Simplified check for restricted first characters + // http://www.yaml.org/spec/1.2/spec.html#ns-plain-first%28c%29 + if (CHAR_MINUS === first || + CHAR_QUESTION === first || + CHAR_COMMERCIAL_AT === first || + CHAR_GRAVE_ACCENT === first) { + simple = false; + } + + // can only use > and | if not wrapped in spaces. + if (spaceWrap) { + simple = false; + folded = false; + literal = false; + } else { + folded = true; + literal = true; + } + + single = true; + double = new StringBuilder(object); + + sawLineFeed = false; + linePosition = 0; + longestLine = 0; + + indent = state.indent * level; + max = 80; + if (indent < 40) { + max -= indent; + } else { + max = 40; + } + + for (position = 0; position < object.length; position++) { + character = object.charCodeAt(position); + if (simple) { + // Characters that can never appear in the simple scalar + if (!simpleChar(character)) { + simple = false; + } else { + // Still simple. If we make it all the way through like + // this, then we can just dump the string as-is. + continue; + } + } + + if (single && character === CHAR_SINGLE_QUOTE) { + single = false; + } + + escapeSeq = ESCAPE_SEQUENCES[character]; + hexEsc = needsHexEscape(character); + + if (!escapeSeq && !hexEsc) { + continue; + } + + if (character !== CHAR_LINE_FEED && + character !== CHAR_DOUBLE_QUOTE && + character !== CHAR_SINGLE_QUOTE) { + folded = false; + literal = false; + } else if (character === CHAR_LINE_FEED) { + sawLineFeed = true; + single = false; + if (position > 0) { + previous = object.charCodeAt(position - 1); + if (previous === CHAR_SPACE) { + literal = false; + folded = false; + } + } + if (folded) { + lineLength = position - linePosition; + linePosition = position; + if (lineLength > longestLine) { + longestLine = lineLength; + } + } + } + + if (character !== CHAR_DOUBLE_QUOTE) { + single = false; + } + + double.takeUpTo(position); + double.escapeChar(); + } + + if (simple && testImplicitResolving(state, object)) { + simple = false; + } + + modifier = ''; + if (folded || literal) { + trailingLineBreaks = 0; + if (object.charCodeAt(object.length - 1) === CHAR_LINE_FEED) { + trailingLineBreaks += 1; + if (object.charCodeAt(object.length - 2) === CHAR_LINE_FEED) { + trailingLineBreaks += 1; + } + } + + if (trailingLineBreaks === 0) { + modifier = '-'; + } else if (trailingLineBreaks === 2) { + modifier = '+'; + } + } + + if (literal && longestLine < max) { + folded = false; + } + + // If it's literally one line, then don't bother with the literal. + // We may still want to do a fold, though, if it's a super long line. + if (!sawLineFeed) { + literal = false; + } + + if (simple) { + state.dump = object; + } else if (single) { + state.dump = '\'' + object + '\''; + } else if (folded) { + result = fold(object, max); + state.dump = '>' + modifier + '\n' + indentString(result, indent); + } else if (literal) { + if (!modifier) { + object = object.replace(/\n$/, ''); + } + state.dump = '|' + modifier + '\n' + indentString(object, indent); + } else if (double) { + double.finish(); + state.dump = '"' + double.result + '"'; + } else { + throw new Error('Failed to dump scalar value'); + } + + return; +} + +// The `trailing` var is a regexp match of any trailing `\n` characters. +// +// There are three cases we care about: +// +// 1. One trailing `\n` on the string. Just use `|` or `>`. +// This is the assumed default. (trailing = null) +// 2. No trailing `\n` on the string. Use `|-` or `>-` to "chomp" the end. +// 3. More than one trailing `\n` on the string. Use `|+` or `>+`. +// +// In the case of `>+`, these line breaks are *not* doubled (like the line +// breaks within the string), so it's important to only end with the exact +// same number as we started. +function fold(object, max) { + var result = '', + position = 0, + length = object.length, + trailing = /\n+$/.exec(object), + newLine; + + if (trailing) { + length = trailing.index + 1; + } + + while (position < length) { + newLine = object.indexOf('\n', position); + if (newLine > length || newLine === -1) { + if (result) { + result += '\n\n'; + } + result += foldLine(object.slice(position, length), max); + position = length; + } else { + if (result) { + result += '\n\n'; + } + result += foldLine(object.slice(position, newLine), max); + position = newLine + 1; + } + } + if (trailing && trailing[0] !== '\n') { + result += trailing[0]; + } + + return result; +} + +function foldLine(line, max) { + if (line === '') { + return line; + } + + var foldRe = /[^\s] [^\s]/g, + result = '', + prevMatch = 0, + foldStart = 0, + match = foldRe.exec(line), + index, + foldEnd, + folded; + + while (match) { + index = match.index; + + // when we cross the max len, if the previous match would've + // been ok, use that one, and carry on. If there was no previous + // match on this fold section, then just have a long line. + if (index - foldStart > max) { + if (prevMatch !== foldStart) { + foldEnd = prevMatch; + } else { + foldEnd = index; + } + + if (result) { + result += '\n'; + } + folded = line.slice(foldStart, foldEnd); + result += folded; + foldStart = foldEnd + 1; + } + prevMatch = index + 1; + match = foldRe.exec(line); + } + + if (result) { + result += '\n'; + } + + // if we end up with one last word at the end, then the last bit might + // be slightly bigger than we wanted, because we exited out of the loop. + if (foldStart !== prevMatch && line.length - foldStart > max) { + result += line.slice(foldStart, prevMatch) + '\n' + + line.slice(prevMatch + 1); + } else { + result += line.slice(foldStart); + } + + return result; +} + +// Returns true if character can be found in a simple scalar +function simpleChar(character) { + return CHAR_TAB !== character && + CHAR_LINE_FEED !== character && + CHAR_CARRIAGE_RETURN !== character && + CHAR_COMMA !== character && + CHAR_LEFT_SQUARE_BRACKET !== character && + CHAR_RIGHT_SQUARE_BRACKET !== character && + CHAR_LEFT_CURLY_BRACKET !== character && + CHAR_RIGHT_CURLY_BRACKET !== character && + CHAR_SHARP !== character && + CHAR_AMPERSAND !== character && + CHAR_ASTERISK !== character && + CHAR_EXCLAMATION !== character && + CHAR_VERTICAL_LINE !== character && + CHAR_GREATER_THAN !== character && + CHAR_SINGLE_QUOTE !== character && + CHAR_DOUBLE_QUOTE !== character && + CHAR_PERCENT !== character && + CHAR_COLON !== character && + !ESCAPE_SEQUENCES[character] && + !needsHexEscape(character); +} + +// Returns true if the character code needs to be escaped. +function needsHexEscape(character) { + return !((0x00020 <= character && character <= 0x00007E) || + (0x00085 === character) || + (0x000A0 <= character && character <= 0x00D7FF) || + (0x0E000 <= character && character <= 0x00FFFD) || + (0x10000 <= character && character <= 0x10FFFF)); +} + +function writeFlowSequence(state, level, object) { + var _result = '', + _tag = state.tag, + index, + length; + + for (index = 0, length = object.length; index < length; index += 1) { + // Write only valid elements. + if (writeNode(state, level, object[index], false, false)) { + if (0 !== index) { + _result += ', '; + } + _result += state.dump; + } + } + + state.tag = _tag; + state.dump = '[' + _result + ']'; +} + +function writeBlockSequence(state, level, object, compact) { + var _result = '', + _tag = state.tag, + index, + length; + + for (index = 0, length = object.length; index < length; index += 1) { + // Write only valid elements. + if (writeNode(state, level + 1, object[index], true, true)) { + if (!compact || 0 !== index) { + _result += generateNextLine(state, level); + } + _result += '- ' + state.dump; + } + } + + state.tag = _tag; + state.dump = _result || '[]'; // Empty sequence if no valid values. +} + +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 (0 !== index) { + pairBuffer += ', '; + } + + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + + if (!writeNode(state, level, objectKey, false, false)) { + continue; // Skip this pair because of invalid key; + } + + if (state.dump.length > 1024) { + pairBuffer += '? '; + } + + pairBuffer += state.dump + ': '; + + if (!writeNode(state, level, objectValue, false, false)) { + continue; // Skip this pair because of invalid value. + } + + pairBuffer += state.dump; + + // Both key and value are valid. + _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; + + for (index = 0, length = objectKeyList.length; index < length; index += 1) { + pairBuffer = ''; + + if (!compact || 0 !== index) { + pairBuffer += generateNextLine(state, level); + } + + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + + if (!writeNode(state, level + 1, objectKey, true, true)) { + continue; // Skip this pair because of invalid key. + } + + explicitPair = (null !== state.tag && '?' !== 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; // Skip this pair because of invalid value. + } + + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += ':'; + } else { + pairBuffer += ': '; + } + + pairBuffer += state.dump; + + // Both key and value are valid. + _result += pairBuffer; + } + + state.tag = _tag; + state.dump = _result || '{}'; // Empty mapping if no valid pairs. +} + +function detectType(state, object, explicit) { + var _result, typeList, index, length, type, style; + + typeList = explicit ? state.explicitTypes : state.implicitTypes; + + for (index = 0, length = typeList.length; index < length; index += 1) { + type = typeList[index]; + + if ((type.instanceOf || type.predicate) && + (!type.instanceOf || (('object' === typeof object) && (object instanceof type.instanceOf))) && + (!type.predicate || type.predicate(object))) { + + state.tag = explicit ? type.tag : '?'; + + if (type.represent) { + style = state.styleMap[type.tag] || type.defaultStyle; + + if ('[object Function]' === _toString.call(type.represent)) { + _result = type.represent(object, style); + } else if (_hasOwnProperty.call(type.represent, style)) { + _result = type.represent[style](object, style); + } else { + throw new YAMLException('!<' + type.tag + '> tag resolver accepts not "' + style + '" style'); + } + + state.dump = _result; + } + + return true; + } + } + + return false; +} + +// Serializes `object` and writes it to global `result`. +// Returns true on success, or false on invalid object. +// +function writeNode(state, level, object, block, compact) { + state.tag = null; + state.dump = object; + + if (!detectType(state, object, false)) { + detectType(state, object, true); + } + + var type = _toString.call(state.dump); + + if (block) { + block = (0 > state.flowLevel || state.flowLevel > level); + } + + if ((null !== state.tag && '?' !== state.tag) || (2 !== state.indent && level > 0)) { + compact = false; + } + + var objectOrArray = '[object Object]' === type || '[object Array]' === type, + duplicateIndex, + duplicate; + + if (objectOrArray) { + duplicateIndex = state.duplicates.indexOf(object); + duplicate = duplicateIndex !== -1; + } + + if (duplicate && state.usedDuplicates[duplicateIndex]) { + state.dump = '*ref_' + duplicateIndex; + } else { + if (objectOrArray && duplicate && !state.usedDuplicates[duplicateIndex]) { + state.usedDuplicates[duplicateIndex] = true; + } + if ('[object Object]' === type) { + if (block && (0 !== Object.keys(state.dump).length)) { + writeBlockMapping(state, level, state.dump, compact); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + (0 === level ? '\n' : '') + state.dump; + } + } else { + writeFlowMapping(state, level, state.dump); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + ' ' + state.dump; + } + } + } else if ('[object Array]' === type) { + if (block && (0 !== state.dump.length)) { + writeBlockSequence(state, level, state.dump, compact); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + (0 === level ? '\n' : '') + state.dump; + } + } else { + writeFlowSequence(state, level, state.dump); + if (duplicate) { + state.dump = '&ref_' + duplicateIndex + ' ' + state.dump; + } + } + } else if ('[object String]' === type) { + if ('?' !== state.tag) { + writeScalar(state, state.dump, level); + } + } else { + if (state.skipInvalid) { + return false; + } + throw new YAMLException('unacceptable kind of an object to dump ' + type); + } + + if (null !== state.tag && '?' !== state.tag) { + state.dump = '!<' + state.tag + '> ' + 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 type = _toString.call(object), + objectKeyList, + index, + length; + + if (null !== object && 'object' === typeof object) { + index = objects.indexOf(object); + if (-1 !== index) { + if (-1 === duplicatesIndexes.indexOf(index)) { + 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); + } + } + } + } +} + +export function dump(input, options) { + options = options || {}; + + var state = new State(options); + + getDuplicateReferences(input, state); + + if (writeNode(state, 0, input, true, true)) { + return state.dump + '\n'; + } + return ''; +} + +export function safeDump(input, options) { + return dump(input, common.extend({ schema: DEFAULT_SAFE_SCHEMA }, options)); +} diff --git a/src/yaml-ast-parser/exception.ts b/src/yaml-ast-parser/exception.ts new file mode 100644 index 0000000..d52cb2e --- /dev/null +++ b/src/yaml-ast-parser/exception.ts @@ -0,0 +1,53 @@ + +'use strict'; + +import Mark from "./mark" + +export default class YAMLException { + message: string + reason: string + name: string + mark: Mark + isWarning: boolean + + private static CLASS_IDENTIFIER = "yaml-ast-parser.YAMLException"; + + public static isInstance(instance: any): instance is YAMLException { + if (instance != null && instance.getClassIdentifier + && typeof (instance.getClassIdentifier) == "function") { + + for (let currentIdentifier of instance.getClassIdentifier()) { + if (currentIdentifier == YAMLException.CLASS_IDENTIFIER) return true; + } + } + + return false; + } + + public getClassIdentifier(): string[] { + var superIdentifiers = []; + + return superIdentifiers.concat(YAMLException.CLASS_IDENTIFIER); + } + + constructor(reason: string, mark: Mark = null, isWarning = false) { + this.name = 'YAMLException'; + this.reason = reason; + this.mark = mark; + this.message = this.toString(false); + this.isWarning = isWarning; + } + + toString(compact: boolean = false) { + var result; + + result = 'JS-YAML: ' + (this.reason || '(unknown reason)'); + + if (!compact && this.mark) { + result += ' ' + this.mark.toString(); + } + + return result; + + } +} diff --git a/src/yaml-ast-parser/index.ts b/src/yaml-ast-parser/index.ts new file mode 100644 index 0000000..426ed2b --- /dev/null +++ b/src/yaml-ast-parser/index.ts @@ -0,0 +1,22 @@ + +/** + * Created by kor on 06/05/15. + */ + +export { load, loadAll, safeLoad, safeLoadAll, LoadOptions } from './loader'; +export { dump, safeDump } from './dumper'; + +import Mark from "./mark" +import YAMLException from './exception'; + +export * from './yamlAST' + +export type Error = YAMLException + +function deprecated(name) { + return function () { + throw new Error('Function ' + name + ' is deprecated and cannot be used.'); + }; +} + +export * from './scalarInference' diff --git a/src/yaml-ast-parser/loader.ts b/src/yaml-ast-parser/loader.ts new file mode 100644 index 0000000..be8206a --- /dev/null +++ b/src/yaml-ast-parser/loader.ts @@ -0,0 +1,1844 @@ +/*eslint-disable max-len,no-use-before-define*/ + +import * as ast from "./yamlAST" + +import * as common from './common'; +import YAMLException from './exception'; +import Mark from './mark'; +import { Schema } from './schema' +import DEFAULT_SAFE_SCHEMA from './schema/default_safe'; +import DEFAULT_FULL_SCHEMA from './schema/default_full'; + + +var _hasOwnProperty = 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\uD800-\uDFFF\uFFFE\uFFFF]/; +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 is_EOL(c) { + return (c === 0x0A/* LF */) || (c === 0x0D/* CR */); +} + +function is_WHITE_SPACE(c) { + return (c === 0x09/* Tab */) || (c === 0x20/* Space */); +} + +function is_WS_OR_EOL(c) { + return (c === 0x09/* Tab */) || + (c === 0x20/* Space */) || + (c === 0x0A/* LF */) || + (c === 0x0D/* CR */); +} + +function is_FLOW_INDICATOR(c) { + return 0x2C/* , */ === c || + 0x5B/* [ */ === c || + 0x5D/* ] */ === c || + 0x7B/* { */ === c || + 0x7D/* } */ === c; +} + +function fromHexCode(c) { + var lc; + + if ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) { + return c - 0x30; + } + + /*eslint-disable no-bitwise*/ + lc = c | 0x20; + + if ((0x61/* a */ <= lc) && (lc <= 0x66/* f */)) { + return lc - 0x61 + 10; + } + + return -1; +} + +function escapedHexLen(c) { + if (c === 0x78/* x */) { return 2; } + if (c === 0x75/* u */) { return 4; } + if (c === 0x55/* U */) { return 8; } + return 0; +} + +function fromDecimalCode(c) { + if ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) { + return c - 0x30; + } + + return -1; +} + +function simpleEscapeSequence(c) { + return (c === 0x30/* 0 */) ? '\x00' : + (c === 0x61/* a */) ? '\x07' : + (c === 0x62/* b */) ? '\x08' : + (c === 0x74/* t */) ? '\x09' : + (c === 0x09/* Tab */) ? '\x09' : + (c === 0x6E/* n */) ? '\x0A' : + (c === 0x76/* v */) ? '\x0B' : + (c === 0x66/* f */) ? '\x0C' : + (c === 0x72/* r */) ? '\x0D' : + (c === 0x65/* e */) ? '\x1B' : + (c === 0x20/* Space */) ? ' ' : + (c === 0x22/* " */) ? '\x22' : + (c === 0x2F/* / */) ? '/' : + (c === 0x5C/* \ */) ? '\x5C' : + (c === 0x4E/* N */) ? '\x85' : + (c === 0x5F/* _ */) ? '\xA0' : + (c === 0x4C/* L */) ? '\u2028' : + (c === 0x50/* P */) ? '\u2029' : ''; +} + +function charFromCodepoint(c) { + if (c <= 0xFFFF) { + return String.fromCharCode(c); + } + // Encode UTF-16 surrogate pair + // https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B010000_to_U.2B10FFFF + return String.fromCharCode(((c - 0x010000) >> 10) + 0xD800, + ((c - 0x010000) & 0x03FF) + 0xDC00); +} + +var simpleEscapeCheck = new Array(256); // integer, for fast access +var simpleEscapeMap = new Array(256); +var customEscapeCheck = new Array(256); // integer, for fast access +var customEscapeMap = new Array(256); +for (var i = 0; i < 256; i++) { + customEscapeMap[i] = simpleEscapeMap[i] = simpleEscapeSequence(i); + simpleEscapeCheck[i] = simpleEscapeMap[i] ? 1 : 0; + customEscapeCheck[i] = 1; + + if (!simpleEscapeCheck[i]) { + customEscapeMap[i] = '\\' + String.fromCharCode(i); + } +} + + + +class State { + + input: string + filename: string; + schema: Schema + errorMap: any = {} + errors: YAMLException[] = [] + onWarning: () => any + legacy: boolean; + implicitTypes: any + typeMap: any + length: number + position: number + line: number + lineStart: number + lineIndent: number + documents: ast.YAMLNode[]; + kind: string + result: ast.YAMLNode + tag: string + anchor: string + anchorMap: { [name: string]: ast.YAMLNode } + tagMap: any + version: string + checkLineBreaks: boolean + allowAnyEscape: boolean + ignoreDuplicateKeys: boolean; + + lines: Line[] = []; + + constructor(input: string, options: any) { + this.input = input; + + this.filename = options['filename'] || null; + this.schema = options['schema'] || DEFAULT_FULL_SCHEMA; + this.onWarning = options['onWarning'] || null; + this.legacy = options['legacy'] || false; + this.allowAnyEscape = options['allowAnyEscape'] || false; + this.ignoreDuplicateKeys = options['ignoreDuplicateKeys'] || false; + + 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.documents = []; + + } +} + + + +function generateError(state, message, isWarning = false) { + return new YAMLException( + message, + new Mark(state.filename, state.input, state.position, state.line, (state.position - state.lineStart)), + isWarning); +} + +function throwErrorFromPosition(state, position: number, message, isWarning = false, toLineEnd = false) { + var line = positionToLine(state, position); + + if (!line) { + return; + } + + var hash = message + position; + + if (state.errorMap[hash]) { + return; + } + + var mark = new Mark(state.filename, state.input, position, line.line, (position - line.start)); + if (toLineEnd) { + mark.toLineEnd = true; + } + + var error = new YAMLException(message, mark, isWarning); + state.errors.push(error); +} + +function throwError(state: State, message) { + //FIXME + var error = generateError(state, message); + var hash = error.message + error.mark.position; + if (!state.errorMap[hash]) { + state.errors.push(error); + state.errorMap[hash] = 1; + } + var or = state.position; + while (true) { + if (state.position >= state.input.length - 1) { + return; + } + var c = state.input.charAt(state.position); + if (c == '\n') { + + state.position--; + if (state.position == or) { + state.position += 1; + } + return; + } + if (c == '\r') { + state.position--; + if (state.position == or) { + state.position += 1; + } + return; + } + state.position++; + } + //throw generateError(state, message); +} + +function throwWarning(state, message) { + var error = generateError(state, message); + + if (state.onWarning) { + state.onWarning.call(null, error); + } else { + //throw error; + } +} + + +var directiveHandlers = { + + YAML: function handleYamlDirective(state, name, args) { + + var match, major, minor; + + if (null !== state.version) { + throwError(state, 'duplication of %YAML directive'); + } + + if (1 !== args.length) { + throwError(state, 'YAML directive accepts exactly one argument'); + } + + match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); + + if (null === match) { + throwError(state, 'ill-formed argument of the YAML directive'); + } + + major = parseInt(match[1], 10); + minor = parseInt(match[2], 10); + + if (1 !== major) { + throwError(state, 'found incompatible YAML document (version 1.2 is required)'); + } + + state.version = args[0]; + state.checkLineBreaks = (minor < 2); + + if (2 !== minor) { + throwError(state, 'found incompatible YAML document (version 1.2 is required)'); + } + }, + + TAG: function handleTagDirective(state, name, args) { + + var handle, prefix; + + if (2 !== args.length) { + 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.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'); + } + + state.tagMap[handle] = prefix; + } +}; + + +function captureSegment(state: State, start: number, end: number, checkJson: boolean): void { + var _position, _length, _character, _result; + var scalar: ast.YAMLScalar = state.result; + if (scalar.startPosition == -1) { + scalar.startPosition = start; + } + 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 (!(0x09 === _character || + 0x20 <= _character && _character <= 0x10FFFF)) { + throwError(state, 'expected valid JSON character'); + } + } + } + scalar.value += _result; + scalar.endPosition = end; + } +} + +function mergeMappings(state: State, destination, source) { + 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.call(destination, key)) { + destination[key] = source[key]; + } + } +} + +function storeMappingPair(state: State, _result: ast.YamlMap, keyTag, keyNode: ast.YAMLNode, + valueNode: ast.YAMLNode): ast.YamlMap { + var index, quantity; + if (keyNode == null) { + return; + } + //keyNode = String(keyNode); + + if (null === _result) { + _result = { + startPosition: keyNode.startPosition, + endPosition: valueNode.endPosition, + parent: null, + errors: [], + mappings: [], kind: ast.Kind.MAP + }; + } + + // if ('tag:yaml.org,2002:merge' === keyTag) { + // if (Array.isArray(valueNode)) { + // for (index = 0, quantity = (valueNode).length; index < quantity; index += 1) { + // mergeMappings(state, _result, valueNode[index]); + // } + // } else { + // mergeMappings(state, _result, valueNode); + // } + // } else { + + var mapping = ast.newMapping(keyNode, valueNode); + mapping.parent = _result; + keyNode.parent = mapping; + if (valueNode != null) { + valueNode.parent = mapping; + } + + !state.ignoreDuplicateKeys && _result.mappings.forEach(sibling => { + if (sibling.key && sibling.key.value === (mapping.key && mapping.key.value)) { + throwErrorFromPosition(state, mapping.key.startPosition, 'duplicate key'); + throwErrorFromPosition(state, sibling.key.startPosition, 'duplicate key'); + } + }); + + _result.mappings.push(mapping) + _result.endPosition = valueNode ? valueNode.endPosition : keyNode.endPosition + 1; //FIXME.workaround should be position of ':' indeed + // } + + return _result; +} + +function readLineBreak(state: State) { + var ch; + + ch = state.input.charCodeAt(state.position); + + if (0x0A/* LF */ === ch) { + state.position++; + } else if (0x0D/* CR */ === ch) { + state.position++; + if (0x0A/* LF */ === state.input.charCodeAt(state.position)) { + state.position++; + } + } else { + throwError(state, 'a line break is expected'); + } + + state.line += 1; + state.lineStart = state.position; + + state.lines.push({ + start: state.lineStart, + line: state.line + }); +} + +class Line { + start: number; + line: number; +} + +function positionToLine(state: State, position: number): Line { + var line: Line; + + for (var i = 0; i < state.lines.length; i++) { + if (state.lines[i].start > position) { + break; + } + + line = state.lines[i]; + } + + if (!line) { + return { + start: 0, + line: 0 + } + } + + return line; +} + +function skipSeparationSpace(state: State, allowComments, checkIndent) { + var lineBreaks = 0, + ch = state.input.charCodeAt(state.position); + + while (0 !== ch) { + while (is_WHITE_SPACE(ch)) { + if (ch === 0x09/*Tab*/) { + state.errors.push(generateError(state, "Using tabs can lead to unpredictable results", true)); + } + ch = state.input.charCodeAt(++state.position); + } + + if (allowComments && 0x23/* # */ === ch) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0x0A/* LF */ && ch !== 0x0D/* CR */ && 0 !== ch); + } + + if (is_EOL(ch)) { + readLineBreak(state); + + ch = state.input.charCodeAt(state.position); + lineBreaks++; + state.lineIndent = 0; + + while (0x20/* Space */ === ch) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + } else { + break; + } + } + + if (-1 !== checkIndent && 0 !== lineBreaks && state.lineIndent < checkIndent) { + throwWarning(state, 'deficient indentation'); + } + + return lineBreaks; +} + +function testDocumentSeparator(state: State) { + var _position = state.position, + ch; + + ch = state.input.charCodeAt(_position); + + // Condition state.position === state.lineStart is tested + // in parent on each call, for efficiency. No needs to test here again. + if ((0x2D/* - */ === ch || 0x2E/* . */ === ch) && + state.input.charCodeAt(_position + 1) === ch && + state.input.charCodeAt(_position + 2) === ch) { + + _position += 3; + + ch = state.input.charCodeAt(_position); + + if (ch === 0 || is_WS_OR_EOL(ch)) { + return true; + } + } + + return false; +} + +function writeFoldedLines(state: State, scalar: ast.YAMLScalar, count: number) { + if (1 === count) { + scalar.value += ' '; + } else if (count > 1) { + scalar.value += common.repeat('\n', count - 1); + } +} + + +function readPlainScalar(state: State, nodeIndent, withinFlowCollection) { + var preceding, + following, + captureStart, + captureEnd, + hasPendingContent, + _line, + _lineStart, + _lineIndent, + _kind = state.kind, + _result = state.result, + ch; + var state_result = ast.newScalar(); + state_result.plainScalar = true; + state.result = state_result; + ch = state.input.charCodeAt(state.position); + + if (is_WS_OR_EOL(ch) || + is_FLOW_INDICATOR(ch) || + 0x23/* # */ === ch || + 0x26/* & */ === ch || + 0x2A/* * */ === ch || + 0x21/* ! */ === ch || + 0x7C/* | */ === ch || + 0x3E/* > */ === ch || + 0x27/* ' */ === ch || + 0x22/* " */ === ch || + 0x25/* % */ === ch || + 0x40/* @ */ === ch || + 0x60/* ` */ === ch) { + return false; + } + + if (0x3F/* ? */ === ch || 0x2D/* - */ === ch) { + 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 (0 !== ch) { + if (0x3A/* : */ === ch) { + following = state.input.charCodeAt(state.position + 1); + + if (is_WS_OR_EOL(following) || + withinFlowCollection && is_FLOW_INDICATOR(following)) { + break; + } + + } else if (0x23/* # */ === ch) { + 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_result, state.line - _line); + captureStart = captureEnd = state.position; + hasPendingContent = false; + } + + if (!is_WHITE_SPACE(ch)) { + captureEnd = state.position + 1; + } + + ch = state.input.charCodeAt(++state.position); + if (state.position >= state.input.length) { + return false; + + } + } + + captureSegment(state, captureStart, captureEnd, false); + + if (state.result.startPosition != -1) { + state_result.rawValue = state.input.substring(state_result.startPosition, state_result.endPosition); + return true; + } + + state.kind = _kind; + state.result = _result; + return false; +} + +function readSingleQuotedScalar(state: State, nodeIndent) { + var ch, + captureStart, captureEnd; + + ch = state.input.charCodeAt(state.position); + + if (0x27/* ' */ !== ch) { + return false; + } + var scalar = ast.newScalar(); + scalar.singleQuoted = true; + state.kind = 'scalar'; + state.result = scalar; + scalar.startPosition = state.position; + + state.position++; + captureStart = captureEnd = state.position; + + while (0 !== (ch = state.input.charCodeAt(state.position))) { + //console.log('ch: <' + String.fromCharCode(ch) + '>'); + if (0x27/* ' */ === ch) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + + //console.log('next: <' + String.fromCharCode(ch) + '>'); + scalar.endPosition = state.position; + if (0x27/* ' */ === ch) { + captureStart = captureEnd = state.position; + state.position++; + } else { + return true; + } + + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, scalar, 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; + scalar.endPosition = state.position; + } + } + + throwError(state, 'unexpected end of the stream within a single quoted scalar'); +} + +function readDoubleQuotedScalar(state: State, nodeIndent: number) { + var captureStart, + captureEnd, + hexLength, + hexResult, + tmp, tmpEsc, + ch; + + ch = state.input.charCodeAt(state.position); + + if (0x22/* " */ !== ch) { + return false; + } + + state.kind = 'scalar'; + var scalar = ast.newScalar(); + scalar.doubleQuoted = true; + state.result = scalar; + scalar.startPosition = state.position; + state.position++; + captureStart = captureEnd = state.position; + while (0 !== (ch = state.input.charCodeAt(state.position))) { + if (0x22/* " */ === ch) { + captureSegment(state, captureStart, state.position, true); + state.position++; + scalar.endPosition = state.position; + scalar.rawValue = state.input.substring(scalar.startPosition, scalar.endPosition); + return true; + + } else if (0x5C/* \ */ === ch) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + + if (is_EOL(ch)) { + skipSeparationSpace(state, false, nodeIndent); + + // TODO: rework to inline fn with no type cast? + } else if (ch < 256 && (state.allowAnyEscape ? customEscapeCheck[ch] : simpleEscapeCheck[ch])) { + scalar.value += (state.allowAnyEscape ? customEscapeMap[ch] : 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'); + } + } + + scalar.value += 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, scalar, 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: State, nodeIndent) { + var readNext = true, + _line, + _tag = state.tag, + _result: ast.YAMLNode, + _anchor = state.anchor, + following, + terminator, + isPair, + isExplicitPair, + isMapping, + keyNode, + keyTag, + valueNode, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x5B/* [ */) { + terminator = 0x5D;/* ] */ + isMapping = false; + _result = ast.newItems(); + _result.startPosition = state.position + } else if (ch === 0x7B/* { */) { + terminator = 0x7D;/* } */ + isMapping = true; + _result = ast.newMap(); + _result.startPosition = state.position + } else { + return false; + } + + if (null !== state.anchor) { + _result.anchorId = state.anchor; + state.anchorMap[state.anchor] = _result; + } + + ch = state.input.charCodeAt(++state.position); + + while (0 !== ch) { + 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; + _result.endPosition = state.position + return true; + } else if (!readNext) { + var p = state.position + throwError(state, 'missed comma between flow collection entries'); + state.position = p + 1; + } + + keyTag = keyNode = valueNode = null; + isPair = isExplicitPair = false; + + if (0x3F/* ? */ === ch) { + 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; + 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) && 0x3A/* : */ === ch) { + 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), keyTag, keyNode, valueNode); + } else if (isPair) { + var mp = storeMappingPair(state, null, keyTag, keyNode, valueNode); + mp.parent = _result; + (_result).items.push(mp); + } else { + keyNode.parent = _result; + (_result).items.push(keyNode); + } + _result.endPosition = state.position + 1/*need to add one more char*/; + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if (0x2C/* , */ === ch) { + 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: State, nodeIndent) { + var captureStart, + folding, + chomping = CHOMPING_CLIP, + detectedIndent = false, + textIndent = nodeIndent, + emptyLines = 0, + atMoreIndented = false, + tmp, + ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x7C/* | */) { + folding = false; + } else if (ch === 0x3E/* > */) { + folding = true; + } else { + return false; + } + var sc = ast.newScalar(); + state.kind = 'scalar'; + state.result = sc; + sc.startPosition = state.position + while (0 !== ch) { + ch = state.input.charCodeAt(++state.position); + + if (0x2B/* + */ === ch || 0x2D/* - */ === ch) { + if (CHOMPING_CLIP === chomping) { + chomping = (0x2B/* + */ === ch) ? 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 (0x23/* # */ === ch) { + do { ch = state.input.charCodeAt(++state.position); } + while (!is_EOL(ch) && (0 !== ch)); + } + } + + while (0 !== ch) { + readLineBreak(state); + state.lineIndent = 0; + + ch = state.input.charCodeAt(state.position); + + while ((!detectedIndent || state.lineIndent < textIndent) && + (0x20/* Space */ === ch)) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + + if (!detectedIndent && state.lineIndent > textIndent) { + textIndent = state.lineIndent; + } + + if (is_EOL(ch)) { + emptyLines++; + continue; + } + + // End of the scalar. + if (state.lineIndent < textIndent) { + + // Perform the chomping. + if (chomping === CHOMPING_KEEP) { + sc.value += common.repeat('\n', emptyLines); + } else if (chomping === CHOMPING_CLIP) { + if (detectedIndent) { // i.e. only if the scalar is not empty. + sc.value += '\n'; + } + } + + // Break this `while` cycle and go to the funciton's epilogue. + break; + } + + // Folded style: use fancy rules to handle line breaks. + if (folding) { + + // Lines starting with white space characters (more-indented lines) are not folded. + if (is_WHITE_SPACE(ch)) { + atMoreIndented = true; + sc.value += common.repeat('\n', emptyLines + 1); + + // End of more-indented block. + } else if (atMoreIndented) { + atMoreIndented = false; + sc.value += common.repeat('\n', emptyLines + 1); + + // Just one line break - perceive as the same line. + } else if (0 === emptyLines) { + if (detectedIndent) { // i.e. only if we have already read some scalar content. + sc.value += ' '; + } + + // Several line breaks - perceive as different lines. + } else { + sc.value += common.repeat('\n', emptyLines); + } + + // Literal style: just add exact number of line breaks between content lines. + } else if (detectedIndent) { + // If current line isn't the first one - count line break from the last content line. + sc.value += common.repeat('\n', emptyLines + 1); + } else { + // In case of the first content line - count only empty lines. + } + + detectedIndent = true; + emptyLines = 0; + captureStart = state.position; + + while (!is_EOL(ch) && (0 !== ch)) { + ch = state.input.charCodeAt(++state.position); + } + + captureSegment(state, captureStart, state.position, false); + } + sc.endPosition = state.position; + var i = state.position - 1; + var needMinus = false; + while (true) { + var c = state.input[i]; + if (c == '\r' || c == '\n') { + if (needMinus) { + i--; + } + break; + } + if (c != ' ' && c != '\t') { + break; + } + i--; + //needMinus=true; + + } + sc.endPosition = i; + sc.rawValue = state.input.substring(sc.startPosition, sc.endPosition); + return true; +} + +function readBlockSequence(state: State, nodeIndent) { + var _line, + _tag = state.tag, + _anchor = state.anchor, + _result = ast.newItems(), + following, + detected = false, + ch; + + if (null !== state.anchor) { + _result.anchorId = state.anchor; + state.anchorMap[state.anchor] = _result; + } + _result.startPosition = state.position; + ch = state.input.charCodeAt(state.position); + + while (0 !== ch) { + + if (0x2D/* - */ !== ch) { + 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.items.push(null); + ch = state.input.charCodeAt(state.position); + continue; + } + } + + _line = state.line; + composeNode(state, nodeIndent, CONTEXT_BLOCK_IN, false, true); + state.result.parent = _result; + _result.items.push(state.result); + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if ((state.line === _line || state.lineIndent > nodeIndent) && (0 !== ch)) { + throwError(state, 'bad indentation of a sequence entry'); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + _result.endPosition = state.position + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = 'sequence'; + state.result = _result; + _result.endPosition = state.position; + return true; + } + return false; +} + +function readBlockMapping(state: State, nodeIndent, flowIndent) { + var following, + allowCompact, + _line, + _tag = state.tag, + _anchor = state.anchor, + _result = ast.newMap(), + keyTag = null, + keyNode = null, + valueNode = null, + atExplicitKey = false, + detected = false, + ch; + _result.startPosition = state.position + if (null !== state.anchor) { + _result.anchorId = state.anchor; + state.anchorMap[state.anchor] = _result; + } + + ch = state.input.charCodeAt(state.position); + + while (0 !== ch) { + following = state.input.charCodeAt(state.position + 1); + _line = state.line; // Save the current line. + + // + // Explicit notation case. There are two separate blocks: + // first for the key (denoted by "?") and second for the value (denoted by ":") + // + if ((0x3F/* ? */ === ch || 0x3A/* : */ === ch) && is_WS_OR_EOL(following)) { + + if (0x3F/* ? */ === ch) { + if (atExplicitKey) { + storeMappingPair(state, _result, keyTag, keyNode, null); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = true; + allowCompact = true; + + } else if (atExplicitKey) { + // i.e. 0x3A/* : */ === character after the explicit key. + atExplicitKey = false; + allowCompact = true; + + } else { + throwError(state, 'incomplete explicit mapping pair; a key node is missed'); + } + + state.position += 1; + ch = following; + + // + // Implicit notation case. Flow-style node as the key first, then ":", and the value. + // + } else if (composeNode(state, flowIndent, CONTEXT_FLOW_OUT, false, true)) { + + if (state.line === _line) { + ch = state.input.charCodeAt(state.position); + + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (0x3A/* : */ === ch) { + 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, keyTag, keyNode, null); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = false; + allowCompact = false; + keyTag = state.tag; + keyNode = state.result; + + } else if (state.position == state.lineStart && testDocumentSeparator(state)) { + break; // Reading is done. Go to the epilogue. + } 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; // Keep the result of `composeNode`. + } + + } else if (detected) { + throwError(state, 'can not read a block mapping entry; a multiline key may not be an implicit key'); + while (state.position > 0) { + ch = state.input.charCodeAt(--state.position); + if (is_EOL(ch)) { + state.position++; + break; + } + } + } else { + state.tag = _tag; + state.anchor = _anchor; + return true; // Keep the result of `composeNode`. + } + + } else { + break; // Reading is done. Go to the epilogue. + } + + // + // Common reading code for both explicit and implicit notations. + // + if (state.line === _line || state.lineIndent > nodeIndent) { + if (composeNode(state, nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact)) { + if (atExplicitKey) { + keyNode = state.result; + } else { + valueNode = state.result; + } + } + + if (!atExplicitKey) { + storeMappingPair(state, _result, keyTag, keyNode, valueNode); + keyTag = keyNode = valueNode = null; + } + + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + } + + if (state.lineIndent > nodeIndent && (0 !== ch)) { + throwError(state, 'bad indentation of a mapping entry'); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + + // + // Epilogue. + // + + // Special case: last mapping's node contains only the key in explicit notation. + if (atExplicitKey) { + storeMappingPair(state, _result, keyTag, keyNode, null); + } + + // Expose the resulting mapping. + if (detected) { + state.tag = _tag; + state.anchor = _anchor; + state.kind = 'mapping'; + state.result = _result; + } + + return detected; +} + +function readTagProperty(state: State) { + var _position, + isVerbatim = false, + isNamed = false, + tagHandle, + tagName, + ch; + + ch = state.input.charCodeAt(state.position); + + if (0x21/* ! */ !== ch) { + return false; + } + + if (null !== state.tag) { + throwError(state, 'duplication of a tag property'); + } + + ch = state.input.charCodeAt(++state.position); + + if (0x3C/* < */ === ch) { + isVerbatim = true; + ch = state.input.charCodeAt(++state.position); + + } else if (0x21/* ! */ === ch) { + isNamed = true; + tagHandle = '!!'; + ch = state.input.charCodeAt(++state.position); + + } else { + tagHandle = '!'; + } + + _position = state.position; + + if (isVerbatim) { + do { ch = state.input.charCodeAt(++state.position); } + while (0 !== ch && 0x3E/* > */ !== ch); + + 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 (0 !== ch && !is_WS_OR_EOL(ch)) { + + if (0x21/* ! */ === ch) { + 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); + } + + if (isVerbatim) { + state.tag = tagName; + + } else if (_hasOwnProperty.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: State) { + var _position, + ch; + + ch = state.input.charCodeAt(state.position); + + if (0x26/* & */ !== ch) { + return false; + } + + if (null !== state.anchor) { + throwError(state, 'duplication of an anchor property'); + } + + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (0 !== ch && !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: State) { + var _position, alias, + len = state.length, + input = state.input, + ch; + + ch = state.input.charCodeAt(state.position); + + if (0x2A/* * */ !== ch) { + return false; + } + + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (0 !== ch && !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'); + state.position = _position + 1; + } + alias = state.input.slice(_position, state.position); + + if (!state.anchorMap.hasOwnProperty(alias)) { + throwError(state, 'unidentified alias "' + alias + '"'); + if (state.position <= _position) { + state.position = _position + 1; + } + } + + state.result = ast.newAnchorRef(alias, _position, state.position, state.anchorMap[alias]); + skipSeparationSpace(state, true, -1); + return true; +} + +function composeNode(state: State, parentIndent, nodeContext, allowToSeek, allowCompact) { + var allowBlockStyles, + allowBlockScalars, + allowBlockCollections, + indentStatus = 1, // 1: this>parent, 0: this=parent, -1: this parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } + } + + let tagStart = state.position; + let tagColumn = state.position - state.lineStart; + if (1 === indentStatus) { + 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 (1 === indentStatus || 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 (1 === indentStatus) { + 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 (null !== state.tag || null !== state.anchor) { + throwError(state, 'alias node should not have any properties'); + } + + } else if (readPlainScalar(state, flowIndent, CONTEXT_FLOW_IN === nodeContext)) { + hasContent = true; + + if (null === state.tag) { + state.tag = '?'; + } + } + + if (null !== state.anchor) { + state.anchorMap[state.anchor] = state.result; + state.result.anchorId = state.anchor + } + } + } else if (0 === indentStatus) { + // Special case: block sequences are allowed to have same indentation level as the parent. + // http://www.yaml.org/spec/1.2/spec.html#id2799784 + hasContent = allowBlockCollections && readBlockSequence(state, blockIndent); + } + } + + if (null !== state.tag && '!' !== state.tag) { + if (state.tag == "!include") { + if (!state.result) { + state.result = ast.newScalar(); + state.result.startPosition = state.position; + state.result.endPosition = state.position; + throwError(state, "!include without value"); + } + state.result.kind = ast.Kind.INCLUDE_REF + } + else if ('?' === state.tag) { + for (typeIndex = 0, typeQuantity = state.implicitTypes.length; + typeIndex < typeQuantity; + typeIndex += 1) { + type = state.implicitTypes[typeIndex]; + + // Implicit resolving is not allowed for non-scalar types, and '?' + // non-specific tag is only assigned to plain scalars. So, it isn't + // needed to check for 'kind' conformity. + var vl = state.result['value']; + if (type.resolve(vl)) { // `state.result` updated in resolver if matched + state.result.valueObject = type.construct(state.result['value']); + state.tag = type.tag; + if (null !== state.anchor) { + state.result.anchorId = state.anchor + state.anchorMap[state.anchor] = state.result; + } + break; + } + } + } else if (_hasOwnProperty.call(state.typeMap, state.tag)) { + type = state.typeMap[state.tag]; + + if (null !== state.result && type.kind !== state.kind) { + throwError(state, 'unacceptable node kind for !<' + state.tag + '> tag; it should be "' + type.kind + '", not "' + state.kind + '"'); + } + + if (!type.resolve(state.result)) { // `state.result` updated in resolver if matched + throwError(state, 'cannot resolve a node with !<' + state.tag + '> explicit tag'); + } else { + state.result = type.construct(state.result); + if (null !== state.anchor) { + state.result.anchorId = state.anchor + state.anchorMap[state.anchor] = state.result; + } + } + } else { + throwErrorFromPosition(state, tagStart, 'unknown tag <' + state.tag + '>', false, true); + } + } + + return null !== state.tag || null !== state.anchor || hasContent; +} + +function readDocument(state: State) { + var documentStart = state.position, + _position, + directiveName, + directiveArgs, + hasDirectives = false, + ch; + + state.version = null; + state.checkLineBreaks = state.legacy; + state.tagMap = {}; + state.anchorMap = {}; + + while (0 !== (ch = state.input.charCodeAt(state.position))) { + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if (state.lineIndent > 0 || 0x25/* % */ !== ch) { + break; + } + + hasDirectives = true; + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (0 !== ch && !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 (0 !== ch) { + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (0x23/* # */ === ch) { + do { ch = state.input.charCodeAt(++state.position); } + while (0 !== ch && !is_EOL(ch)); + break; + } + + if (is_EOL(ch)) { + break; + } + + _position = state.position; + + while (0 !== ch && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + directiveArgs.push(state.input.slice(_position, state.position)); + } + + if (0 !== ch) { + readLineBreak(state); + } + + if (_hasOwnProperty.call(directiveHandlers, directiveName)) { + directiveHandlers[directiveName](state, directiveName, directiveArgs); + } else { + throwWarning(state, 'unknown document directive "' + directiveName + '"'); + state.position++; + } + } + + skipSeparationSpace(state, true, -1); + + if (0 === state.lineIndent && + 0x2D/* - */ === state.input.charCodeAt(state.position) && + 0x2D/* - */ === state.input.charCodeAt(state.position + 1) && + 0x2D/* - */ === state.input.charCodeAt(state.position + 2)) { + 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 (0x2E/* . */ === state.input.charCodeAt(state.position)) { + 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: string, options) { + input = String(input); + options = options || {}; + + let inputLength = input.length; + if (inputLength !== 0) { + + // Add tailing `\n` if not exists + if (0x0A/* LF */ !== input.charCodeAt(inputLength - 1) && + 0x0D/* CR */ !== input.charCodeAt(inputLength - 1)) { + input += '\n'; + } + + // Strip BOM + if (input.charCodeAt(0) === 0xFEFF) { + input = input.slice(1); + } + } + + var state = new State(input, options); + + if (PATTERN_NON_PRINTABLE.test(state.input)) { + throwError(state, 'the stream contains non-printable characters'); + } + + // Use 0 as string terminator. That significantly simplifies bounds check. + state.input += '\0'; + + while (0x20/* Space */ === state.input.charCodeAt(state.position)) { + state.lineIndent += 1; + state.position += 1; + } + + while (state.position < (state.length - 1)) { + var q = state.position + readDocument(state); + if (state.position <= q) { + for (; state.position < state.length - 1; state.position++) { + var c = state.input.charAt(state.position) + if (c == '\n') { + break; + } + } + //skip to the new lne + } + } + + let documents = state.documents; + let docsCount = documents.length; + if (docsCount > 0) { + //last document takes the file till the end + documents[docsCount - 1].endPosition = inputLength; + } + + for (let x of documents) { + x.errors = state.errors; + if (x.startPosition > x.endPosition) { + x.startPosition = x.endPosition; + } + } + return documents; +} + + +export function loadAll(input: string, iterator: (document: ast.YAMLNode) => void, options: LoadOptions = {}) { + var documents = loadDocuments(input, options), index, length; + + for (index = 0, length = documents.length; index < length; index += 1) { + iterator(documents[index]); + } +} + + +export function load(input: string, options: LoadOptions = {}): ast.YAMLNode { + var documents = loadDocuments(input, options), index, length; + + if (0 === documents.length) { + /*eslint-disable no-undefined*/ + return undefined; + } else if (1 === documents.length) { + return documents[0]; + } + var e = new YAMLException('expected a single document in the stream, but found more'); + e.mark = new Mark("", "", 0, 0, 0); + e.mark.position = documents[0].endPosition; + documents[0].errors.push(e); + //it is an artifact which is caused by the fact that we are checking next char before stopping parse + + + return documents[0]; +} + + +export function safeLoadAll(input: string, output: (document: ast.YAMLNode) => void, options: LoadOptions = {}) { + loadAll(input, output, common.extend({ schema: DEFAULT_SAFE_SCHEMA }, options)); +} + + +export function safeLoad(input: string, options: LoadOptions = {}): ast.YAMLNode { + return load(input, common.extend({ schema: DEFAULT_SAFE_SCHEMA }, options)); +} + +export interface LoadOptions { + filename?: string, + schema?: any, + onWarning?: () => any, + legacy?: boolean, + allowAnyEscape?: boolean, + ignoreDuplicateKeys?: boolean +} diff --git a/src/yaml-ast-parser/mark.ts b/src/yaml-ast-parser/mark.ts new file mode 100644 index 0000000..e42cb01 --- /dev/null +++ b/src/yaml-ast-parser/mark.ts @@ -0,0 +1,72 @@ +import * as common from './common'; + +export default class Mark { + + constructor(public name: string, public buffer: string, public position: number, public line: number, public column: number) { + } + + filePath: string; + + toLineEnd: boolean; + + getSnippet(indent: number = 0, maxLength: number = 75) { + var head, start, tail, end, snippet; + + if (!this.buffer) { + return null; + } + + indent = indent || 4; + maxLength = maxLength || 75; + + head = ''; + start = this.position; + + while (start > 0 && -1 === '\x00\r\n\x85\u2028\u2029'.indexOf(this.buffer.charAt(start - 1))) { + start -= 1; + if (this.position - start > (maxLength / 2 - 1)) { + head = ' ... '; + start += 5; + break; + } + } + + tail = ''; + end = this.position; + + while (end < this.buffer.length && -1 === '\x00\r\n\x85\u2028\u2029'.indexOf(this.buffer.charAt(end))) { + end += 1; + if (end - this.position > (maxLength / 2 - 1)) { + tail = ' ... '; + end -= 5; + break; + } + } + + snippet = this.buffer.slice(start, end); + + return common.repeat(' ', indent) + head + snippet + tail + '\n' + + common.repeat(' ', indent + this.position - start + head.length) + '^'; + } + + toString(compact: boolean = true) { + var snippet, where = ''; + + if (this.name) { + where += 'in "' + this.name + '" '; + } + + where += 'at line ' + (this.line + 1) + ', column ' + (this.column + 1); + + if (!compact) { + snippet = this.getSnippet(); + + if (snippet) { + where += ':\n' + snippet; + } + } + + return where; + } + +} \ No newline at end of file diff --git a/src/yaml-ast-parser/scalarInference.ts b/src/yaml-ast-parser/scalarInference.ts new file mode 100644 index 0000000..6b4543d --- /dev/null +++ b/src/yaml-ast-parser/scalarInference.ts @@ -0,0 +1,98 @@ +import { YAMLScalar } from './yamlAST' + +export function parseYamlBoolean(input: string): boolean { + if (["true", "True", "TRUE"].lastIndexOf(input) >= 0) { + return true; + } + else if (["false", "False", "FALSE"].lastIndexOf(input) >= 0) { + return false; + } + throw `Invalid boolean "${input}"` +} + +function safeParseYamlInteger(input: string): number { + // Use startsWith when es6 methods becomes available + if (input.lastIndexOf('0o', 0) === 0) { + return parseInt(input.substring(2), 8) + } + + return parseInt(input); +} + +export function parseYamlInteger(input: string): number { + const result = safeParseYamlInteger(input) + + if (isNaN(result)) { + throw `Invalid integer "${input}"` + } + + return result; +} + +export function parseYamlFloat(input: string): number { + + if ([".nan", ".NaN", ".NAN"].lastIndexOf(input) >= 0) { + return NaN; + } + + const infinity = /^([-+])?(?:\.inf|\.Inf|\.INF)$/ + const match = infinity.exec(input) + if (match) { + return (match[1] === '-') ? -Infinity : Infinity; + } + + const result = parseFloat(input) + + if (!isNaN(result)) { + return result; + } + + throw `Invalid float "${input}"` +} + +export enum ScalarType { + null, bool, int, float, string +} + +/** Determines the type of a scalar according to + * the YAML 1.2 Core Schema (http://www.yaml.org/spec/1.2/spec.html#id2804923) + */ +export function determineScalarType(node: YAMLScalar): ScalarType { + if (node === undefined) { + return ScalarType.null; + } + + if (node.doubleQuoted || !node.plainScalar || node['singleQuoted']) { + return ScalarType.string + } + + const value = node.value; + + if (["null", "Null", "NULL", "~", ''].indexOf(value) >= 0) { + return ScalarType.null; + } + + if (value === null || value === undefined) { + return ScalarType.null; + } + + if (["true", "True", "TRUE", "false", "False", "FALSE"].indexOf(value) >= 0) { + return ScalarType.bool; + } + + const base10 = /^[-+]?[0-9]+$/ + const base8 = /^0o[0-7]+$/ + const base16 = /^0x[0-9a-fA-F]+$/ + + if (base10.test(value) || base8.test(value) || base16.test(value)) { + return ScalarType.int; + } + + const float = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/ + const infinity = /^[-+]?(\.inf|\.Inf|\.INF)$/ + if (float.test(value) || infinity.test(value) || [".nan", ".NaN", ".NAN"].indexOf(value) >= 0) { + return ScalarType.float; + } + + return ScalarType.string; +} \ No newline at end of file diff --git a/src/yaml-ast-parser/schema.ts b/src/yaml-ast-parser/schema.ts new file mode 100644 index 0000000..a4a5262 --- /dev/null +++ b/src/yaml-ast-parser/schema.ts @@ -0,0 +1,112 @@ + +/*eslint-disable max-len*/ + +import * as common from './common'; +import YAMLException from './exception'; +import { Type } from './type'; + + +function compileList(schema: Schema, name, result) { + var exclude = []; + + schema.include.forEach(function (includedSchema) { + result = compileList(includedSchema, name, result); + }); + + schema[name].forEach(function (currentType) { + result.forEach(function (previousType, previousIndex) { + if (previousType.tag === currentType.tag) { + exclude.push(previousIndex); + } + }); + + result.push(currentType); + }); + + return result.filter(function (type, index) { + return -1 === exclude.indexOf(index); + }); +} + + +function compileMap(/* lists... */) { + var result = {}, index, length; + + function collectType(type) { + result[type.tag] = type; + } + + for (index = 0, length = arguments.length; index < length; index += 1) { + arguments[index].forEach(collectType); + } + + return result; +} + +export interface SchemaDefinition { + include?: Schema[] + implicit?: Type[] + explicit?: Type[] +} + +export class Schema { + + include: Schema[] + implicit: Type[] + explicit: Type[] + + compiledImplicit: any[] + compiledExplicit: any[] + compiledTypeMap: any[] + constructor(definition: SchemaDefinition) { + this.include = definition.include || []; + this.implicit = definition.implicit || []; + this.explicit = definition.explicit || []; + + this.implicit.forEach(function (type) { + if (type.loadKind && 'scalar' !== type.loadKind) { + throw new YAMLException('There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.'); + } + }); + + this.compiledImplicit = compileList(this, 'implicit', []); + this.compiledExplicit = compileList(this, 'explicit', []); + this.compiledTypeMap = (compileMap)(this.compiledImplicit, this.compiledExplicit); + } + + static DEFAULT = null; + static create = function createSchema() { + var schemas, types; + + switch (arguments.length) { + case 1: + schemas = Schema.DEFAULT; + types = arguments[0]; + break; + + case 2: + schemas = arguments[0]; + types = arguments[1]; + break; + + default: + throw new YAMLException('Wrong number of arguments for Schema.create function'); + } + + schemas = common.toArray(schemas); + types = common.toArray(types); + + if (!schemas.every(function (schema) { return schema instanceof Schema; })) { + throw new YAMLException('Specified list of super schemas (or a single Schema object) contains a non-Schema object.'); + } + + if (!types.every(function (type) { return type instanceof Type; })) { + throw new YAMLException('Specified list of YAML types (or a single Type object) contains a non-Type object.'); + } + + return new Schema({ + include: schemas, + explicit: types + }); + } +} \ No newline at end of file diff --git a/src/yaml-ast-parser/schema/core.ts b/src/yaml-ast-parser/schema/core.ts new file mode 100644 index 0000000..33a1073 --- /dev/null +++ b/src/yaml-ast-parser/schema/core.ts @@ -0,0 +1,15 @@ + +// Standard YAML's Core schema. +// http://www.yaml.org/spec/1.2/spec.html#id2804923 +// +// NOTE: JS-YAML does not support schema-specific tag resolution restrictions. +// So, Core schema has no distinctions from JSON schema is JS-YAML. + +import { Schema } from '../schema'; +import json from './json'; + +export default new Schema({ + include: [ + json + ] +}); diff --git a/src/yaml-ast-parser/schema/default_full.ts b/src/yaml-ast-parser/schema/default_full.ts new file mode 100644 index 0000000..bccabfa --- /dev/null +++ b/src/yaml-ast-parser/schema/default_full.ts @@ -0,0 +1,28 @@ + +// JS-YAML's default schema for `load` function. +// It is not described in the YAML specification. +// +// This schema is based on JS-YAML's default safe schema and includes +// JavaScript-specific types: !!js/undefined, !!js/regexp and !!js/function. +// +// Also this schema is used as default base schema at `Schema.create` function. + +import { Schema } from '../schema'; + +import DefaultSafe from './default_safe'; + +import UndefinedType from '../type/js/undefined'; +import RegexpType from '../type/js/regexp'; + +var schema = new Schema({ + include: [ + DefaultSafe + ], + explicit: [ + UndefinedType, + RegexpType + + ] +}) +Schema.DEFAULT = schema; +export default schema; diff --git a/src/yaml-ast-parser/schema/default_safe.ts b/src/yaml-ast-parser/schema/default_safe.ts new file mode 100644 index 0000000..515ecd9 --- /dev/null +++ b/src/yaml-ast-parser/schema/default_safe.ts @@ -0,0 +1,34 @@ + + +// JS-YAML's default schema for `safeLoad` function. +// It is not described in the YAML specification. +// +// This schema is based on standard YAML's Core schema and includes most of +// extra types described at YAML tag repository. (http://yaml.org/type/) + + +import { Schema } from '../schema'; + +import Core from './core'; +import TimestampType from '../type/timestamp'; +import MergeType from '../type/merge'; +import BinaryType from '../type/binary'; +import OmapType from '../type/omap'; +import PairsType from '../type/pairs'; +import SetType from '../type/set'; + +export default new Schema({ + include: [ + Core + ], + implicit: [ + TimestampType, + MergeType + ], + explicit: [ + BinaryType, + OmapType, + PairsType, + SetType + ] +}) diff --git a/src/yaml-ast-parser/schema/failsafe.ts b/src/yaml-ast-parser/schema/failsafe.ts new file mode 100644 index 0000000..bbdf58e --- /dev/null +++ b/src/yaml-ast-parser/schema/failsafe.ts @@ -0,0 +1,18 @@ + + +// Standard YAML's Failsafe schema. +// http://www.yaml.org/spec/1.2/spec.html#id2802346 + +import { Schema } from '../schema'; + +import StrType from '../type/str'; +import SeqType from '../type/seq'; +import MapType from '../type/map'; + +export default new Schema({ + explicit: [ + StrType, + SeqType, + MapType + ] +}); diff --git a/src/yaml-ast-parser/schema/json.ts b/src/yaml-ast-parser/schema/json.ts new file mode 100644 index 0000000..30f264c --- /dev/null +++ b/src/yaml-ast-parser/schema/json.ts @@ -0,0 +1,28 @@ + +// Standard YAML's JSON schema. +// http://www.yaml.org/spec/1.2/spec.html#id2803231 +// +// NOTE: JS-YAML does not support schema-specific tag resolution restrictions. +// So, this schema is not such strict as defined in the YAML specification. +// It allows numbers in binary notaion, use `Null` and `NULL` as `null`, etc. + +import { Schema } from '../schema'; + +import failsafe from './failsafe'; + +import NullType from '../type/null'; +import BoolType from '../type/bool'; +import IntType from '../type/int'; +import FloatType from '../type/float'; + +export default new Schema({ + include: [ + failsafe + ], + implicit: [ + NullType, + BoolType, + IntType, + FloatType + ] +}); diff --git a/src/yaml-ast-parser/type.ts b/src/yaml-ast-parser/type.ts new file mode 100644 index 0000000..13a2f84 --- /dev/null +++ b/src/yaml-ast-parser/type.ts @@ -0,0 +1,73 @@ +'use strict'; + +import YAMLException = require('./exception'); + +var TYPE_CONSTRUCTOR_OPTIONS = [ + 'kind', + 'resolve', + 'construct', + 'instanceOf', + 'predicate', + 'represent', + 'defaultStyle', + 'styleAliases' +]; + +var YAML_NODE_KINDS = [ + 'scalar', + 'sequence', + 'mapping' +]; + +function compileStyleAliases(map) { + var result = {}; + + if (null !== map) { + Object.keys(map).forEach(function (style) { + map[style].forEach(function (alias) { + result[String(alias)] = style; + }); + }); + } + + return result; +} + +export class Type { + + tag; + kind; + resolve; + construct; + instanceOf; + predicate; + represent; + defaultStyle; + styleAliases; + loadKind; + + constructor(tag, options) { + options = options || {}; + + Object.keys(options).forEach(function (name) { + if (-1 === TYPE_CONSTRUCTOR_OPTIONS.indexOf(name)) { + throw new YAMLException('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.'); + } + }); + + // TODO: Add tag format check. + 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.defaultStyle = options['defaultStyle'] || null; + this.styleAliases = compileStyleAliases(options['styleAliases'] || null); + + if (-1 === YAML_NODE_KINDS.indexOf(this.kind)) { + throw new YAMLException('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.'); + } + } +} \ No newline at end of file diff --git a/src/yaml-ast-parser/type/binary.ts b/src/yaml-ast-parser/type/binary.ts new file mode 100644 index 0000000..7d5da58 --- /dev/null +++ b/src/yaml-ast-parser/type/binary.ts @@ -0,0 +1,133 @@ + +/*eslint-disable no-bitwise*/ + +// A trick for browserified version. +// Since we make browserifier to ignore `buffer` module, NodeBuffer will be undefined +declare var Buffer: any; + +import { Type } from '../type'; + +// [ 64, 65, 66 ] -> [ padding, CR, LF ] +var BASE64_MAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r'; + + +function resolveYamlBinary(data) { + if (null === data) { + return false; + } + + var code, idx, bitlen = 0, len = 0, max = data.length, map = BASE64_MAP; + + // Convert one by one. + for (idx = 0; idx < max; idx++) { + code = map.indexOf(data.charAt(idx)); + + // Skip CR/LF + if (code > 64) { continue; } + + // Fail on illegal characters + if (code < 0) { return false; } + + bitlen += 6; + } + + // If there are any bits left, source was corrupted + return (bitlen % 8) === 0; +} + +function constructYamlBinary(data) { + var code, idx, tailbits, + input = data.replace(/[\r\n=]/g, ''), // remove CR/LF & padding to simplify scan + max = input.length, + map = BASE64_MAP, + bits = 0, + result = []; + + // Collect by 6*4 bits (3 bytes) + + for (idx = 0; idx < max; idx++) { + if ((idx % 4 === 0) && idx) { + result.push((bits >> 16) & 0xFF); + result.push((bits >> 8) & 0xFF); + result.push(bits & 0xFF); + } + + bits = (bits << 6) | map.indexOf(input.charAt(idx)); + } + + // Dump tail + + tailbits = (max % 4) * 6; + + if (tailbits === 0) { + result.push((bits >> 16) & 0xFF); + result.push((bits >> 8) & 0xFF); + result.push(bits & 0xFF); + } else if (tailbits === 18) { + result.push((bits >> 10) & 0xFF); + result.push((bits >> 2) & 0xFF); + } else if (tailbits === 12) { + result.push((bits >> 4) & 0xFF); + } + + // Wrap into Buffer for NodeJS and leave Array for browser + if (Buffer) { + return new Buffer(result); + } + + return result; +} + +function representYamlBinary(object /*, style*/) { + var result = '', bits = 0, idx, tail, + max = object.length, + map = BASE64_MAP; + + // Convert every three bytes to 4 ASCII characters. + + for (idx = 0; idx < max; idx++) { + if ((idx % 3 === 0) && idx) { + result += map[(bits >> 18) & 0x3F]; + result += map[(bits >> 12) & 0x3F]; + result += map[(bits >> 6) & 0x3F]; + result += map[bits & 0x3F]; + } + + bits = (bits << 8) + object[idx]; + } + + // Dump tail + + tail = max % 3; + + if (tail === 0) { + result += map[(bits >> 18) & 0x3F]; + result += map[(bits >> 12) & 0x3F]; + result += map[(bits >> 6) & 0x3F]; + result += map[bits & 0x3F]; + } else if (tail === 2) { + result += map[(bits >> 10) & 0x3F]; + result += map[(bits >> 4) & 0x3F]; + result += map[(bits << 2) & 0x3F]; + result += map[64]; + } else if (tail === 1) { + result += map[(bits >> 2) & 0x3F]; + result += map[(bits << 4) & 0x3F]; + result += map[64]; + result += map[64]; + } + + return result; +} + +function isBinary(object) { + return Buffer && Buffer.isBuffer(object); +} + +export default new Type('tag:yaml.org,2002:binary', { + kind: 'scalar', + resolve: resolveYamlBinary, + construct: constructYamlBinary, + predicate: isBinary, + represent: representYamlBinary +}); diff --git a/src/yaml-ast-parser/type/bool.ts b/src/yaml-ast-parser/type/bool.ts new file mode 100644 index 0000000..ac0611c --- /dev/null +++ b/src/yaml-ast-parser/type/bool.ts @@ -0,0 +1,36 @@ + +import { Type } from '../type'; + +function resolveYamlBoolean(data) { + if (null === data) { + 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 Boolean]' === Object.prototype.toString.call(object); +} + +export default 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' +}); diff --git a/src/yaml-ast-parser/type/float.ts b/src/yaml-ast-parser/type/float.ts new file mode 100644 index 0000000..2abe455 --- /dev/null +++ b/src/yaml-ast-parser/type/float.ts @@ -0,0 +1,107 @@ + +import * as common from '../common'; +import { Type } from '../type'; + +var YAML_FLOAT_PATTERN = new RegExp( + '^(?:[-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+][0-9]+)?' + + '|\\.[0-9_]+(?:[eE][-+][0-9]+)?' + + '|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*' + + '|[-+]?\\.(?:inf|Inf|INF)' + + '|\\.(?:nan|NaN|NAN))$'); + +function resolveYamlFloat(data) { + if (null === data) { + return false; + } + + var value, sign, base, digits; + + if (!YAML_FLOAT_PATTERN.test(data)) { + return false; + } + return true; +} + +function constructYamlFloat(data) { + var value, sign, base, digits; + + value = data.replace(/_/g, '').toLowerCase(); + sign = '-' === value[0] ? -1 : 1; + digits = []; + + if (0 <= '+-'.indexOf(value[0])) { + value = value.slice(1); + } + + if ('.inf' === value) { + return (1 === sign) ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + + } else if ('.nan' === value) { + return NaN; + + } else if (0 <= value.indexOf(':')) { + value.split(':').forEach(function (v) { + digits.unshift((parseFloat)(v, 10)); + }); + + value = 0.0; + base = 1; + + digits.forEach(function (d) { + value += d * base; + base *= 60; + }); + + return sign * value; + + } + return sign * (parseFloat)(value, 10); +} + +function representYamlFloat(object, style) { + 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'; + } + return object.toString(10); +} + +function isFloat(object) { + return ('[object Number]' === Object.prototype.toString.call(object)) && + (0 !== object % 1 || common.isNegativeZero(object)); +} + +export default new Type('tag:yaml.org,2002:float', { + kind: 'scalar', + resolve: resolveYamlFloat, + construct: constructYamlFloat, + predicate: isFloat, + represent: representYamlFloat, + defaultStyle: 'lowercase' +}); diff --git a/src/yaml-ast-parser/type/int.ts b/src/yaml-ast-parser/type/int.ts new file mode 100644 index 0000000..12edbd6 --- /dev/null +++ b/src/yaml-ast-parser/type/int.ts @@ -0,0 +1,183 @@ + + +import * as common from '../common'; +import { Type } from '../type'; + +function isHexCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)) || + ((0x41/* A */ <= c) && (c <= 0x46/* F */)) || + ((0x61/* a */ <= c) && (c <= 0x66/* f */)); +} + +function isOctCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x37/* 7 */)); +} + +function isDecCode(c) { + return ((0x30/* 0 */ <= c) && (c <= 0x39/* 9 */)); +} + +function resolveYamlInteger(data) { + if (null === data) { + return false; + } + + var max = data.length, + index = 0, + hasDigits = false, + ch; + + if (!max) { return false; } + + ch = data[index]; + + // sign + if (ch === '-' || ch === '+') { + ch = data[++index]; + } + + if (ch === '0') { + // 0 + if (index + 1 === max) { return true; } + ch = data[++index]; + + // base 2, base 8, base 16 + + if (ch === 'b') { + // base 2 + index++; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') { continue; } + if (ch !== '0' && ch !== '1') { + return false; + } + hasDigits = true; + } + return hasDigits; + } + + + if (ch === 'x') { + // base 16 + index++; + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') { continue; } + if (!isHexCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + return hasDigits; + } + + // base 8 + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') { continue; } + if (!isOctCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + return hasDigits; + } + + // base 10 (except 0) or base 60 + + for (; index < max; index++) { + ch = data[index]; + if (ch === '_') { continue; } + if (ch === ':') { break; } + if (!isDecCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + + if (!hasDigits) { return false; } + + // if !base60 - done; + if (ch !== ':') { return true; } + + // base60 almost not used, no needs to optimize + return /^(:[0-5]?[0-9])+$/.test(data.slice(index)); +} + +function constructYamlInteger(data) { + var value = data, sign = 1, ch, base, digits = []; + + 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 ('0' === value) { + return 0; + } + + if (ch === '0') { + if (value[1] === 'b') { + return sign * parseInt(value.slice(2), 2); + } + if (value[1] === 'x') { + return sign * parseInt(value, 16); + } + return sign * parseInt(value, 8); + + } + + if (value.indexOf(':') !== -1) { + value.split(':').forEach(function (v) { + digits.unshift(parseInt(v, 10)); + }); + + value = 0; + base = 1; + + digits.forEach(function (d) { + value += (d * base); + base *= 60; + }); + + return sign * value; + + } + + return sign * parseInt(value, 10); +} + +function isInteger(object) { + return ('[object Number]' === Object.prototype.toString.call(object)) && + (0 === object % 1 && !common.isNegativeZero(object)); +} + +export default new Type('tag:yaml.org,2002:int', { + kind: 'scalar', + resolve: resolveYamlInteger, + construct: constructYamlInteger, + predicate: isInteger, + represent: { + binary: function (object) { return '0b' + object.toString(2); }, + octal: function (object) { return '0' + object.toString(8); }, + decimal: function (object) { return object.toString(10); }, + hexadecimal: function (object) { return '0x' + object.toString(16).toUpperCase(); } + }, + defaultStyle: 'decimal', + styleAliases: { + binary: [2, 'bin'], + octal: [8, 'oct'], + decimal: [10, 'dec'], + hexadecimal: [16, 'hex'] + } +}); diff --git a/src/yaml-ast-parser/type/js/function.ts b/src/yaml-ast-parser/type/js/function.ts new file mode 100644 index 0000000..6d437e0 --- /dev/null +++ b/src/yaml-ast-parser/type/js/function.ts @@ -0,0 +1,79 @@ + +declare var esprima: any; + +// Browserified version does not have esprima +// +// 1. For node.js just require module as deps +// 2. For browser try to require mudule via external AMD system. +// If not found - try to fallback to window.esprima. If not +// found too - then fail to parse. +// + +import { Type } from '../../type'; + +function resolveJavascriptFunction(data) { + if (null === data) { + return false; + } + + try { + var source = '(' + data + ')', + ast = esprima.parse(source, { range: true }), + params = [], + body; + + if ('Program' !== ast.type || + 1 !== ast.body.length || + 'ExpressionStatement' !== ast.body[0].type || + 'FunctionExpression' !== ast.body[0]['expression'].type) { + return false; + } + + return true; + } catch (err) { + return false; + } +} + +function constructJavascriptFunction(data) { + /*jslint evil:true*/ + + var source = '(' + data + ')', + ast = esprima.parse(source, { range: true }), + params: string[] = [], + body; + + if ('Program' !== ast.type || + 1 !== ast.body.length || + 'ExpressionStatement' !== ast.body[0].type || + 'FunctionExpression' !== ast.body[0]['expression'].type) { + throw new Error('Failed to resolve function'); + } + + ast.body[0]['expression'].params.forEach(function (param) { + params.push(param.name); + }); + + body = ast.body[0]['expression'].body.range; + + // Esprima's ranges include the first '{' and the last '}' characters on + // function expressions. So cut them out. + /*eslint-disable no-new-func*/ + return new (Function)(params, source.slice(body[0] + 1, body[1] - 1)); +} + +function representJavascriptFunction(object /*, style*/) { + return object.toString(); +} + +function isFunction(object) { + return '[object Function]' === Object.prototype.toString.call(object); +} + +export default new Type('tag:yaml.org,2002:js/function', { + kind: 'scalar', + resolve: resolveJavascriptFunction, + construct: constructJavascriptFunction, + predicate: isFunction, + represent: representJavascriptFunction +}); diff --git a/src/yaml-ast-parser/type/js/regexp.ts b/src/yaml-ast-parser/type/js/regexp.ts new file mode 100644 index 0000000..21ae083 --- /dev/null +++ b/src/yaml-ast-parser/type/js/regexp.ts @@ -0,0 +1,84 @@ + + +import { Type } from '../../type'; + +function resolveJavascriptRegExp(data) { + if (null === data) { + return false; + } + + if (0 === data.length) { + return false; + } + + var regexp = data, + tail = /\/([gim]*)$/.exec(data), + modifiers = ''; + + // if regexp starts with '/' it can have modifiers and must be properly closed + // `/foo/gim` - modifiers tail can be maximum 3 chars + if ('/' === regexp[0]) { + if (tail) { + modifiers = tail[1]; + } + + if (modifiers.length > 3) { return false; } + // if expression starts with /, is should be properly terminated + if (regexp[regexp.length - modifiers.length - 1] !== '/') { return false; } + + regexp = regexp.slice(1, regexp.length - modifiers.length - 1); + } + + try { + var dummy = new RegExp(regexp, modifiers); + return true; + } catch (error) { + return false; + } +} + +function constructJavascriptRegExp(data) { + var regexp = data, + tail = /\/([gim]*)$/.exec(data), + modifiers = ''; + + // `/foo/gim` - tail can be maximum 4 chars + if ('/' === regexp[0]) { + if (tail) { + modifiers = tail[1]; + } + regexp = regexp.slice(1, regexp.length - modifiers.length - 1); + } + + return new RegExp(regexp, modifiers); +} + +function representJavascriptRegExp(object /*, style*/) { + var result = '/' + object.source + '/'; + + if (object.global) { + result += 'g'; + } + + if (object.multiline) { + result += 'm'; + } + + if (object.ignoreCase) { + result += 'i'; + } + + return result; +} + +function isRegExp(object) { + return '[object RegExp]' === Object.prototype.toString.call(object); +} + +export default new Type('tag:yaml.org,2002:js/regexp', { + kind: 'scalar', + resolve: resolveJavascriptRegExp, + construct: constructJavascriptRegExp, + predicate: isRegExp, + represent: representJavascriptRegExp +}); diff --git a/src/yaml-ast-parser/type/js/undefined.ts b/src/yaml-ast-parser/type/js/undefined.ts new file mode 100644 index 0000000..d54a12c --- /dev/null +++ b/src/yaml-ast-parser/type/js/undefined.ts @@ -0,0 +1,28 @@ + + +import { Type } from '../../type'; + +function resolveJavascriptUndefined() { + return true; +} + +function constructJavascriptUndefined() { + /*eslint-disable no-undefined*/ + return undefined; +} + +function representJavascriptUndefined() { + return ''; +} + +function isUndefined(object) { + return 'undefined' === typeof object; +} + +export default new Type('tag:yaml.org,2002:js/undefined', { + kind: 'scalar', + resolve: resolveJavascriptUndefined, + construct: constructJavascriptUndefined, + predicate: isUndefined, + represent: representJavascriptUndefined +}); diff --git a/src/yaml-ast-parser/type/map.ts b/src/yaml-ast-parser/type/map.ts new file mode 100644 index 0000000..df6e007 --- /dev/null +++ b/src/yaml-ast-parser/type/map.ts @@ -0,0 +1,7 @@ + +import { Type } from '../type'; + +export default new Type('tag:yaml.org,2002:map', { + kind: 'mapping', + construct: function (data) { return null !== data ? data : {}; } +}); diff --git a/src/yaml-ast-parser/type/merge.ts b/src/yaml-ast-parser/type/merge.ts new file mode 100644 index 0000000..8df8844 --- /dev/null +++ b/src/yaml-ast-parser/type/merge.ts @@ -0,0 +1,12 @@ + + +import { Type } from '../type'; + +function resolveYamlMerge(data) { + return '<<' === data || null === data; +} + +export default new Type('tag:yaml.org,2002:merge', { + kind: 'scalar', + resolve: resolveYamlMerge +}); diff --git a/src/yaml-ast-parser/type/null.ts b/src/yaml-ast-parser/type/null.ts new file mode 100644 index 0000000..5ec6d39 --- /dev/null +++ b/src/yaml-ast-parser/type/null.ts @@ -0,0 +1,35 @@ + +import { Type } from '../type'; + +function resolveYamlNull(data) { + if (null === data) { + 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 null === object; +} + +export default 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'; } + }, + defaultStyle: 'lowercase' +}); diff --git a/src/yaml-ast-parser/type/omap.ts b/src/yaml-ast-parser/type/omap.ts new file mode 100644 index 0000000..e600835 --- /dev/null +++ b/src/yaml-ast-parser/type/omap.ts @@ -0,0 +1,55 @@ + +import { Type } from '../type'; + +var _hasOwnProperty = Object.prototype.hasOwnProperty; +var _toString = Object.prototype.toString; + +function resolveYamlOmap(data) { + if (null === data) { + 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 ('[object Object]' !== _toString.call(pair)) { + return false; + } + + for (pairKey in pair) { + if (_hasOwnProperty.call(pair, pairKey)) { + if (!pairHasKey) { + pairHasKey = true; + } else { + return false; + } + } + } + + if (!pairHasKey) { + return false; + } + + if (-1 === objectKeys.indexOf(pairKey)) { + objectKeys.push(pairKey); + } else { + return false; + } + } + + return true; +} + +function constructYamlOmap(data) { + return null !== data ? data : []; +} + +export default new Type('tag:yaml.org,2002:omap', { + kind: 'sequence', + resolve: resolveYamlOmap, + construct: constructYamlOmap +}); diff --git a/src/yaml-ast-parser/type/pairs.ts b/src/yaml-ast-parser/type/pairs.ts new file mode 100644 index 0000000..00de9e1 --- /dev/null +++ b/src/yaml-ast-parser/type/pairs.ts @@ -0,0 +1,74 @@ + + +import { Type } from '../type'; +import * as ast from "../yamlAST"; + +var _toString = Object.prototype.toString; + +function resolveYamlPairs(data) { + if (null === data) { + return true; + } + if (data.kind != ast.Kind.SEQ) { + return false; + } + + var index, length, pair, keys, result, + object = data.items; + + for (index = 0, length = object.length; index < length; index += 1) { + pair = object[index]; + + if ('[object Object]' !== _toString.call(pair)) { + return false; + } + + if (!Array.isArray(pair.mappings)) { + return false; + } + + if (1 !== pair.mappings.length) { + return false; + } + } + + return true; +} + +function constructYamlPairs(data) { + if (null === data || !Array.isArray(data.items)) { + return []; + } + + let index, length, keys, result, + object = data.items; + + result = ast.newItems(); + result.parent = data.parent; + result.startPosition = data.startPosition; + result.endPosition = data.endPosition; + + for (index = 0, length = object.length; index < length; index += 1) { + let pair = object[index]; + + let mapping = pair.mappings[0]; + + let pairSeq = ast.newItems(); + pairSeq.parent = result; + pairSeq.startPosition = mapping.key.startPosition + pairSeq.endPosition = mapping.value.startPosition + mapping.key.parent = pairSeq; + mapping.value.parent = pairSeq; + pairSeq.items = [mapping.key, mapping.value]; + + result.items.push(pairSeq); + } + + return result; +} + +export default new Type('tag:yaml.org,2002:pairs', { + kind: 'sequence', + resolve: resolveYamlPairs, + construct: constructYamlPairs +}); diff --git a/src/yaml-ast-parser/type/seq.ts b/src/yaml-ast-parser/type/seq.ts new file mode 100644 index 0000000..e7ce47c --- /dev/null +++ b/src/yaml-ast-parser/type/seq.ts @@ -0,0 +1,8 @@ + + +import { Type } from '../type'; + +export default new Type('tag:yaml.org,2002:seq', { + kind: 'sequence', + construct: function (data) { return null !== data ? data : []; } +}); diff --git a/src/yaml-ast-parser/type/set.ts b/src/yaml-ast-parser/type/set.ts new file mode 100644 index 0000000..4882e28 --- /dev/null +++ b/src/yaml-ast-parser/type/set.ts @@ -0,0 +1,28 @@ + + +import { Type } from '../type'; +import * as ast from "../yamlAST"; + +var _hasOwnProperty = Object.prototype.hasOwnProperty; + +function resolveYamlSet(data) { + if (null === data) { + return true; + } + + if (data.kind != ast.Kind.MAP) { + return false; + } + + return true; +} + +function constructYamlSet(data) { + return null !== data ? data : {}; +} + +export default new Type('tag:yaml.org,2002:set', { + kind: 'mapping', + resolve: resolveYamlSet, + construct: constructYamlSet +}); diff --git a/src/yaml-ast-parser/type/str.ts b/src/yaml-ast-parser/type/str.ts new file mode 100644 index 0000000..844771f --- /dev/null +++ b/src/yaml-ast-parser/type/str.ts @@ -0,0 +1,7 @@ + +import { Type } from '../type'; + +export default new Type('tag:yaml.org,2002:str', { + kind: 'scalar', + construct: function (data) { return null !== data ? data : ''; } +}); diff --git a/src/yaml-ast-parser/type/timestamp.ts b/src/yaml-ast-parser/type/timestamp.ts new file mode 100644 index 0000000..b6fe0f3 --- /dev/null +++ b/src/yaml-ast-parser/type/timestamp.ts @@ -0,0 +1,97 @@ + +import { Type } from '../type'; + +var YAML_TIMESTAMP_REGEXP = new RegExp( + '^([0-9][0-9][0-9][0-9])' + // [1] year + '-([0-9][0-9]?)' + // [2] month + '-([0-9][0-9]?)' + // [3] day + '(?:(?:[Tt]|[ \\t]+)' + // ... + '([0-9][0-9]?)' + // [4] hour + ':([0-9][0-9])' + // [5] minute + ':([0-9][0-9])' + // [6] second + '(?:\\.([0-9]*))?' + // [7] fraction + '(?:[ \\t]*(Z|([-+])([0-9][0-9]?)' + // [8] tz [9] tz_sign [10] tz_hour + '(?::([0-9][0-9]))?))?)?$'); // [11] tz_minute + +function resolveYamlTimestamp(data) { + if (null === data) { + return false; + } + + var match, year, month, day, hour, minute, second, fraction = 0, + delta = null, tz_hour, tz_minute, date; + + match = YAML_TIMESTAMP_REGEXP.exec(data); + + if (null === match) { + return false; + } + + return true; +} + +function constructYamlTimestamp(data) { + var match, year, month, day, hour, minute, second, fraction: number | string = 0, + delta = null, tz_hour, tz_minute, date; + + match = YAML_TIMESTAMP_REGEXP.exec(data); + + if (null === match) { + throw new Error('Date resolve error'); + } + + // match: [1] year [2] month [3] day + + year = +(match[1]); + month = +(match[2]) - 1; // JS month starts with 0 + day = +(match[3]); + + if (!match[4]) { // no hour + return new Date(Date.UTC(year, month, day)); + } + + // match: [4] hour [5] minute [6] second [7] fraction + + hour = +(match[4]); + minute = +(match[5]); + second = +(match[6]); + + if (match[7]) { + fraction = match[7].slice(0, 3); + while ((fraction).length < 3) { // milli-seconds + fraction = fraction + '0'; + } + fraction = +fraction; + } + + // match: [8] tz [9] tz_sign [10] tz_hour [11] tz_minute + + if (match[9]) { + tz_hour = +(match[10]); + tz_minute = +(match[11] || 0); + delta = (tz_hour * 60 + tz_minute) * 60000; // delta in mili-seconds + 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 /*, style*/) { + return object.toISOString(); +} + +export default new Type('tag:yaml.org,2002:timestamp', { + kind: 'scalar', + resolve: resolveYamlTimestamp, + construct: constructYamlTimestamp, + instanceOf: Date, + represent: representYamlTimestamp +}); diff --git a/src/yaml-ast-parser/yamlAST.ts b/src/yaml-ast-parser/yamlAST.ts new file mode 100644 index 0000000..5351880 --- /dev/null +++ b/src/yaml-ast-parser/yamlAST.ts @@ -0,0 +1,125 @@ + +/** + * Created by kor on 06/05/15. + */ +import YAMLException from './exception'; +export enum Kind { + SCALAR, + MAPPING, + MAP, + SEQ, + ANCHOR_REF, + INCLUDE_REF +} + +export interface YAMLDocument { + startPosition: number + endPosition: number + errors: YAMLException[] +} +export interface YAMLNode extends YAMLDocument { + startPosition: number + endPosition: number + kind: Kind + anchorId?: string + valueObject?: any + parent: YAMLNode + errors: YAMLException[] + /** + * @deprecated Inspect kind and cast to the appropriate subtype instead. + */ + value?: any + + /** + * @deprecated Inspect kind and cast to the appropriate subtype instead. + */ + key?: any + + /** + * @deprecated Inspect kind and cast to the appropriate subtype instead. + */ + mappings?: any +} + +export interface YAMLAnchorReference extends YAMLNode { + referencesAnchor: string + value: YAMLNode +} +export interface YAMLScalar extends YAMLNode { + value: string + doubleQuoted?: boolean + singleQuoted?: boolean + plainScalar?: boolean + rawValue: string +} + +export interface YAMLMapping extends YAMLNode { + key: YAMLScalar + value: YAMLNode +} +export interface YAMLSequence extends YAMLNode { + items: YAMLNode[] +} +export interface YamlMap extends YAMLNode { + mappings: YAMLMapping[] +} +export function newMapping(key: YAMLScalar, value: YAMLNode): YAMLMapping { + var end = (value ? value.endPosition : key.endPosition + 1); //FIXME.workaround, end should be defied by position of ':' + //console.log('key: ' + key.value + ' ' + key.startPosition + '..' + key.endPosition + ' ' + value + ' end: ' + end); + var node = { + key: key, + value: value, + startPosition: key.startPosition, + endPosition: end, + kind: Kind.MAPPING, + parent: null, + errors: [] + }; + return node +} +export function newAnchorRef(key: string, start: number, end: number, value: YAMLNode): YAMLAnchorReference { + return { + errors: [], + referencesAnchor: key, + value: value, + startPosition: start, + endPosition: end, + kind: Kind.ANCHOR_REF, + parent: null + } +} +export function newScalar(v: string = ""): YAMLScalar { + return { + errors: [], + startPosition: -1, + endPosition: -1, + value: v, + kind: Kind.SCALAR, + parent: null, + doubleQuoted: false, + rawValue: v + } +} +export function newItems(): YAMLSequence { + return { + errors: [], + startPosition: -1, + endPosition: -1, + items: [], + kind: Kind.SEQ, + parent: null + } +} +export function newSeq(): YAMLSequence { + return newItems(); +} +export function newMap(mappings?: YAMLMapping[]): YamlMap { + return { + errors: [], + startPosition: -1, + endPosition: -1, + mappings: mappings ? mappings : [], + kind: Kind.MAP, + parent: null + } +} diff --git a/src/yaml-languageservice/jsonContributions.ts b/src/yaml-languageservice/jsonContributions.ts new file mode 100644 index 0000000..081d242 --- /dev/null +++ b/src/yaml-languageservice/jsonContributions.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import {Thenable, MarkedString, CompletionItem} from 'vscode-json-languageservice'; + +export interface JSONWorkerContribution { + getInfoContribution(uri: string, location: JSONPath): Thenable; + collectPropertyCompletions(uri: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Thenable; + collectValueCompletions(uri: string, location: JSONPath, propertyKey: string, result: CompletionsCollector): Thenable; + collectDefaultCompletions(uri: string, result: CompletionsCollector): Thenable; + resolveCompletion?(item: CompletionItem): Thenable; +} +export type Segment = string | number; +export type JSONPath = Segment[]; + +export interface CompletionsCollector { + add(suggestion: CompletionItem): void; + error(message: string): void; + log(message: string): void; + setAsIncomplete(): void; + getNumberOfProposals(): number; +} \ No newline at end of file diff --git a/src/yaml-languageservice/jsonSchema.ts b/src/yaml-languageservice/jsonSchema.ts new file mode 100644 index 0000000..9f9f19f --- /dev/null +++ b/src/yaml-languageservice/jsonSchema.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export interface JSONSchema { + id?: string; + $schema?: string; + type?: string | string[]; + title?: string; + default?: any; + definitions?: JSONSchemaMap; + description?: string; + properties?: JSONSchemaMap; + patternProperties?: JSONSchemaMap; + additionalProperties?: any; + minProperties?: number; + maxProperties?: number; + dependencies?: JSONSchemaMap | string[]; + items?: any; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalItems?: boolean; + pattern?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; + multipleOf?: number; + required?: string[]; + $ref?: string; + anyOf?: JSONSchema[]; + allOf?: JSONSchema[]; + oneOf?: JSONSchema[]; + not?: JSONSchema; + enum?: any[]; + format?: string; + errorMessage?: string; // VSCode extension + patternErrorMessage?: string; // VSCode extension + deprecationMessage?: string; // VSCode extension + enumDescriptions?: string[]; // VSCode extension + "x-kubernetes-group-version-kind"?; //Kubernetes extension +} + +export interface JSONSchemaMap { + [name: string]:JSONSchema; +} diff --git a/src/yaml-languageservice/parser/jsonParser.ts b/src/yaml-languageservice/parser/jsonParser.ts new file mode 100644 index 0000000..a18140b --- /dev/null +++ b/src/yaml-languageservice/parser/jsonParser.ts @@ -0,0 +1,1050 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import Json = require('jsonc-parser'); +import { JSONSchema } from '../jsonSchema'; +import * as objects from '../utils/objects'; + +import * as nls from 'vscode-nls'; +import { LanguageSettings } from '../yamlLanguageService'; +const localize = nls.loadMessageBundle(); + +export interface IRange { + start: number; + end: number; +} + +export enum ErrorCode { + Undefined = 0, + EnumValueMismatch = 1, + CommentsNotAllowed = 2 +} + +export enum ProblemSeverity { + Error, Warning +} + +export interface IProblem { + location: IRange; + severity: ProblemSeverity; + code?: ErrorCode; + message: string; +} + +export class ASTNode { + public start: number; + public end: number; + public type: string; + public parent: ASTNode; + public parserSettings: LanguageSettings; + public location: Json.Segment; + + constructor(parent: ASTNode, type: string, location: Json.Segment, start: number, end?: number) { + this.type = type; + this.location = location; + this.start = start; + this.end = end; + this.parent = parent; + this.parserSettings = { + isKubernetes: false + }; + } + + public setParserSettings(parserSettings: LanguageSettings){ + this.parserSettings = parserSettings; + } + + public getPath(): Json.JSONPath { + let path = this.parent ? this.parent.getPath() : []; + if (this.location !== null) { + path.push(this.location); + } + return path; + } + + + public getChildNodes(): ASTNode[] { + return []; + } + + public getLastChild(): ASTNode { + return null; + } + + public getValue(): any { + // override in children + return; + } + + public contains(offset: number, includeRightBound: boolean = false): boolean { + return offset >= this.start && offset < this.end || includeRightBound && offset === this.end; + } + + public toString(): string { + return 'type: ' + this.type + ' (' + this.start + '/' + this.end + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : ''); + } + + public visit(visitor: (node: ASTNode) => boolean): boolean { + return visitor(this); + } + + public getNodeFromOffset(offset: number): ASTNode { + let findNode = (node: ASTNode): ASTNode => { + if (offset >= node.start && offset < node.end) { + let children = node.getChildNodes(); + for (let i = 0; i < children.length && children[i].start <= offset; i++) { + let item = findNode(children[i]); + if (item) { + return item; + } + } + return node; + } + return null; + }; + return findNode(this); + } + + public getNodeCollectorCount(offset: number): Number { + let collector = []; + let findNode = (node: ASTNode): ASTNode => { + let children = node.getChildNodes(); + for (let i = 0; i < children.length; i++) { + let item = findNode(children[i]); + if (item && item.type === "property") { + collector.push(item); + } + } + return node; + }; + let foundNode = findNode(this); + return collector.length; + } + + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + let collector = []; + let findNode = (node: ASTNode): ASTNode => { + if (offset >= node.start && offset <= node.end) { + let children = node.getChildNodes(); + for (let i = 0; i < children.length && children[i].start <= offset; i++) { + let item = findNode(children[i]); + if (item) { + collector.push(item); + } + } + return node; + } + return null; + }; + let foundNode = findNode(this); + let currMinDist = Number.MAX_VALUE; + let currMinNode = null; + for(let possibleNode in collector){ + let currNode = collector[possibleNode]; + let minDist = (currNode.end - offset) + (offset - currNode.start); + if(minDist < currMinDist){ + currMinNode = currNode; + currMinDist = minDist; + } + } + return currMinNode || foundNode; + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + + if (Array.isArray(schema.type)) { + if ((schema.type).indexOf(this.type) === -1) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: schema.errorMessage || localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}.', (schema.type).join(', ')) + }); + } + } + else if (schema.type) { + if (this.type !== schema.type) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: schema.errorMessage || localize('typeMismatchWarning', 'Incorrect type. Expected "{0}".', schema.type) + }); + } + } + if (Array.isArray(schema.allOf)) { + schema.allOf.forEach((subSchema) => { + this.validate(subSchema, validationResult, matchingSchemas); + }); + } + if (schema.not) { + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + this.validate(schema.not, subValidationResult, subMatchingSchemas); + if (!subValidationResult.hasProblems()) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('notSchemaWarning', "Matches a schema that is not allowed.") + }); + } + subMatchingSchemas.schemas.forEach((ms) => { + ms.inverted = !ms.inverted; + matchingSchemas.add(ms); + }); + } + + let testAlternatives = (alternatives: JSONSchema[], maxOneMatch: boolean) => { + let matches = []; + + // remember the best match that is used for error messages + let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } = null; + alternatives.forEach((subSchema) => { + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + + this.validate(subSchema, subValidationResult, subMatchingSchemas); + if (!subValidationResult.hasProblems()) { + matches.push(subSchema); + } + if (!bestMatch) { + bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; + } else if(this.parserSettings.isKubernetes) { + bestMatch = alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas); + } else { + bestMatch = genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas); + } + }); + + if (matches.length > 1 && maxOneMatch && !this.parserSettings.isKubernetes) { + validationResult.problems.push({ + location: { start: this.start, end: this.start + 1 }, + severity: ProblemSeverity.Warning, + message: localize('oneOfWarning', "Matches multiple schemas when only one must validate.") + }); + } + if (bestMatch !== null) { + validationResult.merge(bestMatch.validationResult); + validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; + validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; + matchingSchemas.merge(bestMatch.matchingSchemas); + } + return matches.length; + }; + if (Array.isArray(schema.anyOf)) { + testAlternatives(schema.anyOf, false); + } + if (Array.isArray(schema.oneOf)) { + testAlternatives(schema.oneOf, true); + } + + if (Array.isArray(schema.enum)) { + let val = this.getValue(); + let enumValueMatch = false; + for (let e of schema.enum) { + if (objects.equals(val, e)) { + enumValueMatch = true; + break; + } + } + validationResult.enumValues = schema.enum; + validationResult.enumValueMatch = enumValueMatch; + if (!enumValueMatch) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + message: schema.errorMessage || localize('enumWarning', 'Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', ')) + }); + } + } + + if (schema.deprecationMessage && this.parent) { + validationResult.problems.push({ + location: { start: this.parent.start, end: this.parent.end }, + severity: ProblemSeverity.Warning, + message: schema.deprecationMessage + }); + } + matchingSchemas.add({ node: this, schema: schema }); + } +} + +export class NullASTNode extends ASTNode { + + constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { + super(parent, 'null', name, start, end); + } + + public getValue(): any { + return null; + } +} + +export class BooleanASTNode extends ASTNode { + + private value: boolean | string; + + constructor(parent: ASTNode, name: Json.Segment, value: boolean | string, start: number, end?: number) { + super(parent, 'boolean', name, start, end); + this.value = value; + } + + public getValue(): any { + return this.value; + } + +} + +export class ArrayASTNode extends ASTNode { + + public items: ASTNode[]; + + constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { + super(parent, 'array', name, start, end); + this.items = []; + } + + public getChildNodes(): ASTNode[] { + return this.items; + } + + public getLastChild(): ASTNode { + return this.items[this.items.length - 1]; + } + + public getValue(): any { + return this.items.map((v) => v.getValue()); + } + + public addItem(item: ASTNode): boolean { + if (item) { + this.items.push(item); + return true; + } + return false; + } + + public visit(visitor: (node: ASTNode) => boolean): boolean { + let ctn = visitor(this); + for (let i = 0; i < this.items.length && ctn; i++) { + ctn = this.items[i].visit(visitor); + } + return ctn; + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + super.validate(schema, validationResult, matchingSchemas); + + if (Array.isArray(schema.items)) { + let subSchemas = schema.items; + subSchemas.forEach((subSchema, index) => { + let itemValidationResult = new ValidationResult(); + let item = this.items[index]; + if (item) { + item.validate(subSchema, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + } else if (this.items.length >= subSchemas.length) { + validationResult.propertiesValueMatches++; + } + }); + if (this.items.length > subSchemas.length) { + if (typeof schema.additionalItems === 'object') { + for (let i = subSchemas.length; i < this.items.length; i++) { + let itemValidationResult = new ValidationResult(); + this.items[i].validate(schema.additionalItems, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + } + } else if (schema.additionalItems === false) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length) + }); + } + } + } + else if (schema.items) { + this.items.forEach((item) => { + let itemValidationResult = new ValidationResult(); + item.validate(schema.items, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + }); + } + + if (schema.minItems && this.items.length < schema.minItems) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('minItemsWarning', 'Array has too few items. Expected {0} or more.', schema.minItems) + }); + } + + if (schema.maxItems && this.items.length > schema.maxItems) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer.', schema.minItems) + }); + } + + if (schema.uniqueItems === true) { + let values = this.items.map((node) => { + return node.getValue(); + }); + let duplicates = values.some((value, index) => { + return index !== values.lastIndexOf(value); + }); + if (duplicates) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('uniqueItemsWarning', 'Array has duplicate items.') + }); + } + } + } +} + +export class NumberASTNode extends ASTNode { + + public isInteger: boolean; + public value: number; + + constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { + super(parent, 'number', name, start, end); + this.isInteger = true; + this.value = Number.NaN; + } + + public getValue(): any { + return this.value; + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + + // work around type validation in the base class + let typeIsInteger = false; + if (schema.type === 'integer' || (Array.isArray(schema.type) && (schema.type).indexOf('integer') !== -1)) { + typeIsInteger = true; + } + if (typeIsInteger && this.isInteger === true) { + this.type = 'integer'; + } + super.validate(schema, validationResult, matchingSchemas); + this.type = 'number'; + + let val = this.getValue(); + + if (typeof schema.multipleOf === 'number') { + if (val % schema.multipleOf !== 0) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('multipleOfWarning', 'Value is not divisible by {0}.', schema.multipleOf) + }); + } + } + + if (typeof schema.minimum === 'number') { + if (schema.exclusiveMinimum && val <= schema.minimum) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}.', schema.minimum) + }); + } + if (!schema.exclusiveMinimum && val < schema.minimum) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('minimumWarning', 'Value is below the minimum of {0}.', schema.minimum) + }); + } + } + + if (typeof schema.maximum === 'number') { + if (schema.exclusiveMaximum && val >= schema.maximum) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}.', schema.maximum) + }); + } + if (!schema.exclusiveMaximum && val > schema.maximum) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('maximumWarning', 'Value is above the maximum of {0}.', schema.maximum) + }); + } + } + + } +} + +export class StringASTNode extends ASTNode { + public isKey: boolean; + public value: string; + + constructor(parent: ASTNode, name: Json.Segment, isKey: boolean, start: number, end?: number) { + super(parent, 'string', name, start, end); + this.isKey = isKey; + this.value = ''; + } + + public getValue(): any { + return this.value; + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + super.validate(schema, validationResult, matchingSchemas); + + if (schema.minLength && this.value.length < schema.minLength) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('minLengthWarning', 'String is shorter than the minimum length of {0}.', schema.minLength) + }); + } + + if (schema.maxLength && this.value.length > schema.maxLength) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('maxLengthWarning', 'String is longer than the maximum length of {0}.', schema.maxLength) + }); + } + + if (schema.pattern) { + let regex = new RegExp(schema.pattern); + if (!regex.test(this.value)) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || localize('patternWarning', 'String does not match the pattern of "{0}".', schema.pattern) + }); + } + } + + } +} + +export class PropertyASTNode extends ASTNode { + public key: StringASTNode; + public value: ASTNode; + public colonOffset: number; + + constructor(parent: ASTNode, key: StringASTNode) { + super(parent, 'property', null, key.start); + this.key = key; + key.parent = this; + key.location = key.value; + this.colonOffset = -1; + } + + public getChildNodes(): ASTNode[] { + return this.value ? [this.key, this.value] : [this.key]; + } + + public getLastChild(): ASTNode { + return this.value; + } + + public setValue(value: ASTNode): boolean { + this.value = value; + return value !== null; + } + + public visit(visitor: (node: ASTNode) => boolean): boolean { + return visitor(this) && this.key.visit(visitor) && this.value && this.value.visit(visitor); + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + if (this.value) { + this.value.validate(schema, validationResult, matchingSchemas); + } + } +} + +export class ObjectASTNode extends ASTNode { + public properties: PropertyASTNode[]; + + constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { + super(parent, 'object', name, start, end); + + this.properties = []; + } + + public getChildNodes(): ASTNode[] { + return this.properties; + } + + public getLastChild(): ASTNode { + return this.properties[this.properties.length - 1]; + } + + public addProperty(node: PropertyASTNode): boolean { + if (!node) { + return false; + } + this.properties.push(node); + return true; + } + + public getFirstProperty(key: string): PropertyASTNode { + for (let i = 0; i < this.properties.length; i++) { + if (this.properties[i].key.value === key) { + return this.properties[i]; + } + } + return null; + } + + public getKeyList(): string[] { + return this.properties.map((p) => p.key.getValue()); + } + + public getValue(): any { + let value: any = Object.create(null); + this.properties.forEach((p) => { + let v = p.value && p.value.getValue(); + if (typeof v !== 'undefined') { + value[p.key.getValue()] = v; + } + }); + return value; + } + + public visit(visitor: (node: ASTNode) => boolean): boolean { + let ctn = visitor(this); + for (let i = 0; i < this.properties.length && ctn; i++) { + ctn = this.properties[i].visit(visitor); + } + return ctn; + } + + public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (!matchingSchemas.include(this)) { + return; + } + + super.validate(schema, validationResult, matchingSchemas); + let seenKeys: { [key: string]: ASTNode } = Object.create(null); + let unprocessedProperties: string[] = []; + this.properties.forEach((node) => { + + let key = node.key.value; + + //Replace the merge key with the actual values of what the node value points to in seen keys + if(key === "<<" && node.value) { + + switch(node.value.type) { + case "object": { + node.value["properties"].forEach(propASTNode => { + let propKey = propASTNode.key.value; + seenKeys[propKey] = propASTNode.value; + unprocessedProperties.push(propKey); + }); + break; + } + case "array": { + node.value["items"].forEach(sequenceNode => { + sequenceNode["properties"].forEach(propASTNode => { + let seqKey = propASTNode.key.value; + seenKeys[seqKey] = propASTNode.value; + unprocessedProperties.push(seqKey); + }); + }); + break; + } + default: { + break; + } + } + }else{ + seenKeys[key] = node.value; + unprocessedProperties.push(key); + } + + }); + + if (Array.isArray(schema.required)) { + schema.required.forEach((propertyName: string) => { + if (!seenKeys[propertyName]) { + let key = this.parent && this.parent && (this.parent).key; + let location = key ? { start: key.start, end: key.end } : { start: this.start, end: this.start + 1 }; + validationResult.problems.push({ + location: location, + severity: ProblemSeverity.Warning, + message: localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName) + }); + } + }); + } + + let propertyProcessed = (prop: string) => { + let index = unprocessedProperties.indexOf(prop); + while (index >= 0) { + unprocessedProperties.splice(index, 1); + index = unprocessedProperties.indexOf(prop); + } + }; + + if (schema.properties) { + Object.keys(schema.properties).forEach((propertyName: string) => { + propertyProcessed(propertyName); + let prop = schema.properties[propertyName]; + let child = seenKeys[propertyName]; + if (child) { + let propertyValidationResult = new ValidationResult(); + child.validate(prop, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + + }); + } + + if (schema.patternProperties) { + Object.keys(schema.patternProperties).forEach((propertyPattern: string) => { + let regex = new RegExp(propertyPattern); + unprocessedProperties.slice(0).forEach((propertyName: string) => { + if (regex.test(propertyName)) { + propertyProcessed(propertyName); + let child = seenKeys[propertyName]; + if (child) { + let propertyValidationResult = new ValidationResult(); + child.validate(schema.patternProperties[propertyPattern], propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + + } + }); + }); + } + + if (typeof schema.additionalProperties === 'object') { + unprocessedProperties.forEach((propertyName: string) => { + let child = seenKeys[propertyName]; + if (child) { + let propertyValidationResult = new ValidationResult(); + child.validate(schema.additionalProperties, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + }); + } else if (schema.additionalProperties === false) { + if (unprocessedProperties.length > 0) { + unprocessedProperties.forEach((propertyName: string) => { + let child = seenKeys[propertyName]; + if (child) { + let propertyNode = null; + if(child.type !== "property"){ + propertyNode = child.parent; + if(propertyNode.type === "object"){ + propertyNode = propertyNode.properties[0]; + } + }else{ + propertyNode = child; + } + validationResult.problems.push({ + location: { start: propertyNode.key.start, end: propertyNode.key.end }, + severity: ProblemSeverity.Warning, + message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Unexpected property {0}', propertyName) + }); + } + }); + } + } + + if (schema.maxProperties) { + if (this.properties.length > schema.maxProperties) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties) + }); + } + } + + if (schema.minProperties) { + if (this.properties.length < schema.minProperties) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties) + }); + } + } + + if (schema.dependencies) { + Object.keys(schema.dependencies).forEach((key: string) => { + let prop = seenKeys[key]; + if (prop) { + let propertyDep = schema.dependencies[key] + if (Array.isArray(propertyDep)) { + propertyDep.forEach((requiredProp: string) => { + if (!seenKeys[requiredProp]) { + validationResult.problems.push({ + location: { start: this.start, end: this.end }, + severity: ProblemSeverity.Warning, + message: localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key) + }); + } else { + validationResult.propertiesValueMatches++; + } + }); + } else if (propertyDep) { + let propertyvalidationResult = new ValidationResult(); + this.validate(propertyDep, propertyvalidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyvalidationResult); + } + } + }); + } + } +} + +export interface IApplicableSchema { + node: ASTNode; + inverted?: boolean; + schema: JSONSchema; +} + +export enum EnumMatch { + Key, Enum +} + +export interface ISchemaCollector { + schemas: IApplicableSchema[]; + add(schema: IApplicableSchema): void; + merge(other: ISchemaCollector): void; + include(node: ASTNode): void; + newSub(): ISchemaCollector; +} + +class SchemaCollector implements ISchemaCollector { + schemas: IApplicableSchema[] = []; + constructor(private focusOffset = -1, private exclude: ASTNode = null) { + } + add(schema: IApplicableSchema) { + this.schemas.push(schema); + } + merge(other: ISchemaCollector) { + this.schemas.push(...other.schemas); + } + include(node: ASTNode) { + return (this.focusOffset === -1 || node.contains(this.focusOffset)) && (node !== this.exclude); + } + newSub(): ISchemaCollector { + return new SchemaCollector(-1, this.exclude); + } +} + +class NoOpSchemaCollector implements ISchemaCollector { + get schemas() { return []; } + add(schema: IApplicableSchema) { } + merge(other: ISchemaCollector) { } + include(node: ASTNode) { return true; } + newSub(): ISchemaCollector { return this; } +} + +export class ValidationResult { + public problems: IProblem[]; + + public propertiesMatches: number; + public propertiesValueMatches: number; + public primaryValueMatches: number; + public enumValueMatch: boolean; + public enumValues: any[]; + public warnings; + public errors; + + constructor() { + this.problems = []; + this.propertiesMatches = 0; + this.propertiesValueMatches = 0; + this.primaryValueMatches = 0; + this.enumValueMatch = false; + this.enumValues = null; + this.warnings = []; + this.errors = []; + } + + public hasProblems(): boolean { + return !!this.problems.length; + } + + public mergeAll(validationResults: ValidationResult[]): void { + validationResults.forEach((validationResult) => { + this.merge(validationResult); + }); + } + + public merge(validationResult: ValidationResult): void { + this.problems = this.problems.concat(validationResult.problems); + } + + public mergeEnumValues(validationResult: ValidationResult): void { + if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { + this.enumValues = this.enumValues.concat(validationResult.enumValues); + for (let error of this.problems) { + if (error.code === ErrorCode.EnumValueMismatch) { + error.message = localize('enumWarning', 'Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', ')); + } + } + } + } + + public mergePropertyMatch(propertyValidationResult: ValidationResult): void { + this.merge(propertyValidationResult); + this.propertiesMatches++; + if (propertyValidationResult.enumValueMatch || !this.hasProblems() && propertyValidationResult.propertiesMatches) { + this.propertiesValueMatches++; + } + if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues && propertyValidationResult.enumValues.length === 1) { + this.primaryValueMatches++; + } + } + + public compareGeneric(other: ValidationResult): number { + let hasProblems = this.hasProblems(); + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + return this.propertiesMatches - other.propertiesMatches; + } + + public compareKubernetes(other: ValidationResult): number { + let hasProblems = this.hasProblems(); + if(this.propertiesMatches !== other.propertiesMatches){ + return this.propertiesMatches - other.propertiesMatches; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + return this.propertiesMatches - other.propertiesMatches; + } + +} + +export class JSONDocument { + + constructor(public readonly root: ASTNode, public readonly syntaxErrors: IProblem[]) { + } + + public getNodeFromOffset(offset: number): ASTNode { + return this.root && this.root.getNodeFromOffset(offset); + } + + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + return this.root && this.root.getNodeFromOffsetEndInclusive(offset); + } + + public visit(visitor: (node: ASTNode) => boolean): void { + if (this.root) { + this.root.visit(visitor); + } + } + + public configureSettings(parserSettings: LanguageSettings){ + if(this.root) { + this.root.setParserSettings(parserSettings); + } + } + + public validate(schema: JSONSchema): IProblem[] { + if (this.root && schema) { + let validationResult = new ValidationResult(); + this.root.validate(schema, validationResult, new NoOpSchemaCollector()); + return validationResult.problems; + } + return null; + } + + public getMatchingSchemas(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null): IApplicableSchema[] { + let matchingSchemas = new SchemaCollector(focusOffset, exclude); + let validationResult = new ValidationResult(); + if (this.root && schema) { + this.root.validate(schema, validationResult, matchingSchemas); + } + return matchingSchemas.schemas; + } + + public getValidationProblems(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null) { + let matchingSchemas = new SchemaCollector(focusOffset, exclude); + let validationResult = new ValidationResult(); + if (this.root && schema) { + this.root.validate(schema, validationResult, matchingSchemas); + } + return validationResult.problems; + } +} + +//Alternative comparison is specifically used by the kubernetes/openshift schema but may lead to better results then genericComparison depending on the schema +function alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas){ + let compareResult = subValidationResult.compareKubernetes(bestMatch.validationResult); + if (compareResult > 0) { + // our node is the best matching so far + bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; + } else if (compareResult === 0) { + // there's already a best matching but we are as good + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.mergeEnumValues(subValidationResult); + } + return bestMatch; +} + +//genericComparison tries to find the best matching schema using a generic comparison +function genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas){ + if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { + // no errors, both are equally good matches + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; + bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + } else { + let compareResult = subValidationResult.compareGeneric(bestMatch.validationResult); + if (compareResult > 0) { + // our node is the best matching so far + bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; + } else if (compareResult === 0) { + // there's already a best matching but we are as good + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.mergeEnumValues(subValidationResult); + } + } + return bestMatch; +} \ No newline at end of file diff --git a/src/yaml-languageservice/parser/yamlParser.ts b/src/yaml-languageservice/parser/yamlParser.ts new file mode 100644 index 0000000..c1752f2 --- /dev/null +++ b/src/yaml-languageservice/parser/yamlParser.ts @@ -0,0 +1,242 @@ +'use strict'; + +import { JSONSchema } from 'vscode-json-languageservice/lib/jsonSchema'; +import { ASTNode, ErrorCode, BooleanASTNode, NullASTNode, ArrayASTNode, NumberASTNode, ObjectASTNode, PropertyASTNode, StringASTNode, IApplicableSchema, JSONDocument } from './jsonParser'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import * as Yaml from '../../yaml-ast-parser/index' +import { Kind } from '../../yaml-ast-parser/index' + +import { getLineStartPositions, getPosition } from '../utils/documentPositionCalculator' + +export class SingleYAMLDocument extends JSONDocument { + private lines; + public root; + public errors; + public warnings; + + constructor(lines: number[]) { + super(null, []); + this.lines = lines; + this.root = null; + this.errors = []; + this.warnings = []; + } + + public getSchemas(schema, doc, node) { + let matchingSchemas = []; + doc.validate(schema, matchingSchemas, node.start); + return matchingSchemas; + } + + public getNodeFromOffset(offset: number): ASTNode { + return this.getNodeFromOffsetEndInclusive(offset); + } + + private getNodeByIndent = (lines: number[], offset: number, node: ASTNode) => { + + const { line, column: indent } = getPosition(offset, this.lines) + + const children = node.getChildNodes() + + function findNode(children) { + for (var idx = 0; idx < children.length; idx++) { + var child = children[idx]; + + const { line: childLine, column: childCol } = getPosition(child.start, lines); + + if (childCol > indent) { + return null; + } + + const newChildren = child.getChildNodes() + const foundNode = findNode(newChildren) + + if (foundNode) { + return foundNode; + } + + // We have the right indentation, need to return based on line + if (childLine == line) { + return child; + } + if (childLine > line) { + // Get previous + (idx - 1) >= 0 ? children[idx - 1] : child; + } + // Else continue loop to try next element + } + + // Special case, we found the correct + return children[children.length - 1] + } + + return findNode(children) || node + } +} + + +function recursivelyBuildAst(parent: ASTNode, node: Yaml.YAMLNode): ASTNode { + + if (!node) { + return; + } + + switch (node.kind) { + case Yaml.Kind.MAP: { + const instance = node; + + const result = new ObjectASTNode(parent, null, node.startPosition, node.endPosition) + result.addProperty + + for (const mapping of instance.mappings) { + result.addProperty(recursivelyBuildAst(result, mapping)) + } + + return result; + } + case Yaml.Kind.MAPPING: { + const instance = node; + const key = instance.key; + + // Technically, this is an arbitrary node in YAML + // I doubt we would get a better string representation by parsing it + const keyNode = new StringASTNode(null, null, true, key.startPosition, key.endPosition); + keyNode.value = key.value; + + const result = new PropertyASTNode(parent, keyNode) + result.end = instance.endPosition + + const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNode(parent, key.value, instance.endPosition, instance.endPosition) + valueNode.location = key.value + + result.setValue(valueNode) + + return result; + } + case Yaml.Kind.SEQ: { + const instance = node; + + const result = new ArrayASTNode(parent, null, instance.startPosition, instance.endPosition); + + let count = 0; + for (const item of instance.items) { + if (item === null && count === instance.items.length - 1) { + break; + } + + // Be aware of https://github.com/nodeca/js-yaml/issues/321 + // Cannot simply work around it here because we need to know if we are in Flow or Block + var itemNode = (item === null) ? new NullASTNode(parent, null, instance.endPosition, instance.endPosition) : recursivelyBuildAst(result, item); + + itemNode.location = count++; + result.addItem(itemNode); + } + + return result; + } + case Yaml.Kind.SCALAR: { + const instance = node; + const type = Yaml.determineScalarType(instance) + + // The name is set either by the sequence or the mapping case. + const name = null; + const value = instance.value; + + //This is a patch for redirecting values with these strings to be boolean nodes because its not supported in the parser. + let possibleBooleanValues = ['y', 'Y', 'yes', 'Yes', 'YES', 'n', 'N', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF']; + if (possibleBooleanValues.indexOf(value.toString()) !== -1) { + return new BooleanASTNode(parent, name, value, node.startPosition, node.endPosition) + } + + switch (type) { + case Yaml.ScalarType.null: { + return new NullASTNode(parent, name, instance.startPosition, instance.endPosition); + } + case Yaml.ScalarType.bool: { + return new BooleanASTNode(parent, name, Yaml.parseYamlBoolean(value), node.startPosition, node.endPosition) + } + case Yaml.ScalarType.int: { + const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); + result.value = Yaml.parseYamlInteger(value); + result.isInteger = true; + return result; + } + case Yaml.ScalarType.float: { + const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); + result.value = Yaml.parseYamlFloat(value); + result.isInteger = false; + return result; + } + case Yaml.ScalarType.string: { + const result = new StringASTNode(parent, name, false, node.startPosition, node.endPosition); + result.value = node.value; + return result; + } + } + + break; + } + case Yaml.Kind.ANCHOR_REF: { + const instance = (node).value + + return recursivelyBuildAst(parent, instance) || + new NullASTNode(parent, null, node.startPosition, node.endPosition); + } + case Yaml.Kind.INCLUDE_REF: { + const result = new StringASTNode(parent, null, false, node.startPosition, node.endPosition); + result.value = node.value; + return result; + } + } +} + +function convertError(e: Yaml.YAMLException) { + return { message: `${e.reason}`, location: { start: e.mark.position, end: e.mark.position + e.mark.column, code: ErrorCode.Undefined } } +} + +function createJSONDocument(yamlDoc: Yaml.YAMLNode, startPositions: number[]) { + let _doc = new SingleYAMLDocument(startPositions); + _doc.root = recursivelyBuildAst(null, yamlDoc) + + if (!_doc.root) { + // TODO: When this is true, consider not pushing the other errors. + _doc.errors.push({ message: localize('Invalid symbol', 'Expected a YAML object, array or literal'), code: ErrorCode.Undefined, location: { start: yamlDoc.startPosition, end: yamlDoc.endPosition } }); + } + + const duplicateKeyReason = 'duplicate key' + + const errors = yamlDoc.errors.filter(e => e.reason !== duplicateKeyReason && !e.isWarning).map(e => convertError(e)) + const warnings = yamlDoc.errors.filter(e => e.reason === duplicateKeyReason || e.isWarning).map(e => convertError(e)) + + errors.forEach(e => _doc.errors.push(e)); + warnings.forEach(e => _doc.warnings.push(e)); + + return _doc; +} + +export class YAMLDocument { + public documents: JSONDocument[] + private errors; + private warnings; + + constructor(documents: JSONDocument[]) { + this.documents = documents; + this.errors = []; + this.warnings = []; + } + +} + +export function parse(text: string): YAMLDocument { + + const startPositions = getLineStartPositions(text) + // This is documented to return a YAMLNode even though the + // typing only returns a YAMLDocument + const yamlDocs = [] + Yaml.loadAll(text, doc => yamlDocs.push(doc), {}) + + return new YAMLDocument(yamlDocs.map(doc => createJSONDocument(doc, startPositions))); +} \ No newline at end of file diff --git a/src/yaml-languageservice/services/documentSymbols.ts b/src/yaml-languageservice/services/documentSymbols.ts new file mode 100644 index 0000000..da0de0c --- /dev/null +++ b/src/yaml-languageservice/services/documentSymbols.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import Parser = require('../parser/jsonParser'); +import Strings = require('../utils/strings'); + +import { SymbolInformation, SymbolKind, TextDocument, Range, Location } from 'vscode-languageserver-types'; +import { Thenable } from "../yamlLanguageService"; +import { IJSONSchemaService } from "./jsonSchemaService"; + +export class YAMLDocumentSymbols { + + public findDocumentSymbols(document: TextDocument, doc: Parser.JSONDocument): SymbolInformation[] { + + if(!doc || doc["documents"].length === 0){ + return null; + } + + let collectOutlineEntries = (result: SymbolInformation[], node: Parser.ASTNode, containerName: string): SymbolInformation[] => { + if (node.type === 'array') { + (node).items.forEach((node: Parser.ASTNode) => { + collectOutlineEntries(result, node, containerName); + }); + } else if (node.type === 'object') { + let objectNode = node; + + objectNode.properties.forEach((property: Parser.PropertyASTNode) => { + let location = Location.create(document.uri, Range.create(document.positionAt(property.start), document.positionAt(property.end))); + let valueNode = property.value; + if (valueNode) { + let childContainerName = containerName ? containerName + '.' + property.key.value : property.key.value; + result.push({ name: property.key.getValue(), kind: this.getSymbolKind(valueNode.type), location: location, containerName: containerName }); + collectOutlineEntries(result, valueNode, childContainerName); + } + }); + } + return result; + }; + + let results = []; + for(let yamlDoc in doc["documents"]){ + let currentYAMLDoc = doc["documents"][yamlDoc]; + if(currentYAMLDoc.root){ + let result = collectOutlineEntries([], currentYAMLDoc.root, void 0); + results = results.concat(result); + } + } + + return results; + } + + private getSymbolKind(nodeType: string): SymbolKind { + switch (nodeType) { + case 'object': + return SymbolKind.Module; + case 'string': + return SymbolKind.String; + case 'number': + return SymbolKind.Number; + case 'array': + return SymbolKind.Array; + case 'boolean': + return SymbolKind.Boolean; + default: // 'null' + return SymbolKind.Variable; + } + } + +} \ No newline at end of file diff --git a/src/yaml-languageservice/services/jsonSchemaService.ts b/src/yaml-languageservice/services/jsonSchemaService.ts new file mode 100644 index 0000000..9d998c3 --- /dev/null +++ b/src/yaml-languageservice/services/jsonSchemaService.ts @@ -0,0 +1,534 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import Json = require('jsonc-parser'); +import {JSONSchema, JSONSchemaMap} from '../jsonSchema'; +import URI from 'vscode-uri'; +import Strings = require('../utils/strings'); +import Parser = require('../parser/jsonParser'); +import {SchemaRequestService, WorkspaceContextService, PromiseConstructor, Thenable} from '../yamlLanguageService'; + + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export interface IJSONSchemaService { + + /** + * Registers a schema file in the current workspace to be applicable to files that match the pattern + */ + registerExternalSchema(uri: string, filePatterns?: string[], unresolvedSchema?: JSONSchema): ISchemaHandle; + + /** + * Clears all cached schema files + */ + clearExternalSchemas(): void; + + /** + * Registers contributed schemas + */ + setSchemaContributions(schemaContributions: ISchemaContributions): void; + + /** + * Looks up the appropriate schema for the given URI + */ + getSchemaForResource(resource: string): Thenable; + + /** + * Returns all registered schema ids + */ + getRegisteredSchemaIds(filter?: (scheme) => boolean): string[]; +} + +export interface ISchemaAssociations { + [pattern: string]: string[]; +} + +export interface ISchemaContributions { + schemas?: { [id: string]: JSONSchema }; + schemaAssociations?: ISchemaAssociations; +} + +export declare type CustomSchemaProvider = (uri: string) => Thenable; + +export interface ISchemaHandle { + /** + * The schema id + */ + url: string; + + /** + * The schema from the file, with potential $ref references + */ + getUnresolvedSchema(): Thenable; + + /** + * The schema from the file, with references resolved + */ + getResolvedSchema(): Thenable; +} + + +export class FilePatternAssociation { + + private schemas: string[]; + private combinedSchemaId: string; + private patternRegExp: RegExp; + private combinedSchema: ISchemaHandle; + + constructor(pattern: string) { + this.combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(pattern); + try { + this.patternRegExp = new RegExp(Strings.convertSimple2RegExpPattern(pattern) + '$'); + } catch (e) { + // invalid pattern + this.patternRegExp = null; + } + this.schemas = []; + this.combinedSchema = null; + } + + public addSchema(id: string) { + this.schemas.push(id); + this.combinedSchema = null; + } + + public matchesPattern(fileName: string): boolean { + return this.patternRegExp && this.patternRegExp.test(fileName); + } + + public getCombinedSchema(service: JSONSchemaService): ISchemaHandle { + if (!this.combinedSchema) { + this.combinedSchema = service.createCombinedSchema(this.combinedSchemaId, this.schemas); + } + return this.combinedSchema; + } +} + +class SchemaHandle implements ISchemaHandle { + + public url: string; + + private resolvedSchema: Thenable; + private unresolvedSchema: Thenable; + private service: JSONSchemaService; + + constructor(service: JSONSchemaService, url: string, unresolvedSchemaContent?: JSONSchema) { + this.service = service; + this.url = url; + if (unresolvedSchemaContent) { + this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent)); + } + } + + public getUnresolvedSchema(): Thenable { + if (!this.unresolvedSchema) { + this.unresolvedSchema = this.service.loadSchema(this.url); + } + return this.unresolvedSchema; + } + + public getResolvedSchema(): Thenable { + if (!this.resolvedSchema) { + this.resolvedSchema = this.getUnresolvedSchema().then(unresolved => { + return this.service.resolveSchemaContent(unresolved, this.url); + }); + } + return this.resolvedSchema; + } + + public clearSchema(): void { + this.resolvedSchema = null; + this.unresolvedSchema = null; + } +} + +export class UnresolvedSchema { + public schema: JSONSchema; + public errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } +} + +export class ResolvedSchema { + public schema: JSONSchema; + public errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } + + public getSection(path: string[]): JSONSchema { + return this.getSectionRecursive(path, this.schema); + } + + private getSectionRecursive(path: string[], schema: JSONSchema): JSONSchema { + if (!schema || path.length === 0) { + return schema; + } + let next = path.shift(); + + if (schema.properties && schema.properties[next]) { + return this.getSectionRecursive(path, schema.properties[next]); + } else if (schema.patternProperties) { + Object.keys(schema.patternProperties).forEach((pattern) => { + let regex = new RegExp(pattern); + if (regex.test(next)) { + return this.getSectionRecursive(path, schema.patternProperties[pattern]); + } + }); + } else if (schema.additionalProperties) { + return this.getSectionRecursive(path, schema.additionalProperties); + } else if (next.match('[0-9]+')) { + if (schema.items) { + return this.getSectionRecursive(path, schema.items); + } else if (Array.isArray(schema.items)) { + try { + let index = parseInt(next, 10); + if (schema.items[index]) { + return this.getSectionRecursive(path, schema.items[index]); + } + return null; + } + catch (e) { + return null; + } + } + } + + return null; + } +} + +export class JSONSchemaService implements IJSONSchemaService { + + private contributionSchemas: { [id: string]: SchemaHandle }; + private contributionAssociations: { [id: string]: string[] }; + + private schemasById: { [id: string]: SchemaHandle }; + private filePatternAssociations: FilePatternAssociation[]; + private filePatternAssociationById: { [id: string]: FilePatternAssociation }; + private registeredSchemasIds: { [id: string]: boolean }; + + private contextService: WorkspaceContextService; + private callOnDispose: Function[]; + private requestService: SchemaRequestService; + private promiseConstructor: PromiseConstructor; + private customSchemaProvider: CustomSchemaProvider; + + constructor(requestService: SchemaRequestService, contextService?: WorkspaceContextService, customSchemaProvider?: CustomSchemaProvider, promiseConstructor?: PromiseConstructor) { + this.contextService = contextService; + this.requestService = requestService; + this.promiseConstructor = promiseConstructor || Promise; + this.callOnDispose = []; + this.customSchemaProvider = customSchemaProvider; + this.contributionSchemas = {}; + this.contributionAssociations = {}; + this.schemasById = {}; + this.filePatternAssociations = []; + this.filePatternAssociationById = {}; + this.registeredSchemasIds = {}; + } + + public getRegisteredSchemaIds(filter?: (scheme) => boolean): string[] { + return Object.keys(this.registeredSchemasIds).filter(id => { + let scheme = URI.parse(id).scheme; + return scheme !== 'schemaservice' && (!filter || filter(scheme)); + }); + } + + public get promise() { + return this.promiseConstructor; + } + + public dispose(): void { + while (this.callOnDispose.length > 0) { + this.callOnDispose.pop()(); + } + } + + public onResourceChange(uri: string): boolean { + uri = this.normalizeId(uri); + let schemaFile = this.schemasById[uri]; + if (schemaFile) { + schemaFile.clearSchema(); + return true; + } + return false; + } + + private normalizeId(id: string) { + // remove trailing '#', normalize drive capitalization + return URI.parse(id).toString(); + } + + public setSchemaContributions(schemaContributions: ISchemaContributions): void { + if (schemaContributions.schemas) { + let schemas = schemaContributions.schemas; + for (let id in schemas) { + let normalizedId = this.normalizeId(id); + this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]); + } + } + if (schemaContributions.schemaAssociations) { + let schemaAssociations = schemaContributions.schemaAssociations; + for (let pattern in schemaAssociations) { + let associations = schemaAssociations[pattern]; + this.contributionAssociations[pattern] = associations; + + var fpa = this.getOrAddFilePatternAssociation(pattern); + associations.forEach(schemaId => { + let id = this.normalizeId(schemaId); + fpa.addSchema(id); + }); + } + } + } + + private addSchemaHandle(id: string, unresolvedSchemaContent?: JSONSchema): SchemaHandle { + let schemaHandle = new SchemaHandle(this, id, unresolvedSchemaContent); + this.schemasById[id] = schemaHandle; + return schemaHandle; + } + + private getOrAddSchemaHandle(id: string, unresolvedSchemaContent?: JSONSchema): ISchemaHandle { + return this.schemasById[id] || this.addSchemaHandle(id, unresolvedSchemaContent); + } + + private getOrAddFilePatternAssociation(pattern: string) { + let fpa = this.filePatternAssociationById[pattern]; + if (!fpa) { + fpa = new FilePatternAssociation(pattern); + this.filePatternAssociationById[pattern] = fpa; + this.filePatternAssociations.push(fpa); + } + return fpa; + } + + public registerExternalSchema(uri: string, filePatterns: string[] = null, unresolvedSchemaContent?: JSONSchema): ISchemaHandle { + let id = this.normalizeId(uri); + this.registeredSchemasIds[id] = true; + + if (filePatterns) { + filePatterns.forEach(pattern => { + this.getOrAddFilePatternAssociation(pattern).addSchema(id); + }); + } + return unresolvedSchemaContent ? this.addSchemaHandle(id, unresolvedSchemaContent) : this.getOrAddSchemaHandle(id); + } + + public clearExternalSchemas(): void { + this.schemasById = {}; + this.filePatternAssociations = []; + this.filePatternAssociationById = {}; + this.registeredSchemasIds = {}; + + for (let id in this.contributionSchemas) { + this.schemasById[id] = this.contributionSchemas[id]; + this.registeredSchemasIds[id] = true; + } + for (let pattern in this.contributionAssociations) { + var fpa = this.getOrAddFilePatternAssociation(pattern); + + this.contributionAssociations[pattern].forEach(schemaId => { + let id = this.normalizeId(schemaId); + fpa.addSchema(id); + }); + } + } + + public getResolvedSchema(schemaId: string): Thenable { + let id = this.normalizeId(schemaId); + let schemaHandle = this.schemasById[id]; + if (schemaHandle) { + return schemaHandle.getResolvedSchema(); + } + return this.promise.resolve(null); + } + + public loadSchema(url: string): Thenable { + if (!this.requestService) { + let errorMessage = localize('json.schema.norequestservice', 'Unable to load schema from \'{0}\'. No schema request service available', toDisplayString(url)); + return this.promise.resolve(new UnresolvedSchema({}, [errorMessage])); + } + return this.requestService(url).then( + content => { + if (!content) { + let errorMessage = localize('json.schema.nocontent', 'Unable to load schema from \'{0}\': No content.', toDisplayString(url)); + return new UnresolvedSchema({}, [errorMessage]); + } + + let schemaContent: JSONSchema = {}; + let jsonErrors = []; + schemaContent = Json.parse(content, jsonErrors); + let errors = jsonErrors.length ? [localize('json.schema.invalidFormat', 'Unable to parse content from \'{0}\': {1}.', toDisplayString(url), Json.getParseErrorMessage(jsonErrors[0]))] : []; + return new UnresolvedSchema(schemaContent, errors); + }, + (error: any) => { + let errorMessage = localize('json.schema.unabletoload', 'Unable to load schema from \'{0}\': {1}', toDisplayString(url), error.toString()); + return new UnresolvedSchema({}, [errorMessage]); + } + ); + } + + public resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaURL: string): Thenable { + + let resolveErrors: string[] = schemaToResolve.errors.slice(0); + let schema = schemaToResolve.schema; + let contextService = this.contextService; + + let findSection = (schema: JSONSchema, path: string): any => { + if (!path) { + return schema; + } + let current: any = schema; + if (path[0] === '/') { + path = path.substr(1); + } + path.split('/').some((part) => { + current = current[part]; + return !current; + }); + return current; + }; + + let resolveLink = (node: any, linkedSchema: JSONSchema, linkPath: string): void => { + let section = findSection(linkedSchema, linkPath); + if (section) { + for (let key in section) { + if (section.hasOwnProperty(key) && !node.hasOwnProperty(key)) { + node[key] = section[key]; + } + } + } else { + resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in {1} can not be resolved.', linkPath, linkedSchema.id)); + } + delete node.$ref; + }; + + let resolveExternalLink = (node: any, uri: string, linkPath: string, parentSchemaURL: string): Thenable => { + if (contextService && !/^\w+:\/\/.*/.test(uri)) { + uri = contextService.resolveRelativePath(uri, parentSchemaURL); + } + uri = this.normalizeId(uri); + return this.getOrAddSchemaHandle(uri).getUnresolvedSchema().then(unresolvedSchema => { + if (unresolvedSchema.errors.length) { + let loc = linkPath ? uri + '#' + linkPath : uri; + resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0])); + } + resolveLink(node, unresolvedSchema.schema, linkPath); + return resolveRefs(node, unresolvedSchema.schema, uri); + }); + }; + + let resolveRefs = (node: JSONSchema, parentSchema: JSONSchema, parentSchemaURL: string): Thenable => { + if (!node) { + return Promise.resolve(null); + } + + let toWalk: JSONSchema[] = [node]; + let seen: JSONSchema[] = []; + + let openPromises: Thenable[] = []; + + let collectEntries = (...entries: JSONSchema[]) => { + for (let entry of entries) { + if (typeof entry === 'object') { + toWalk.push(entry); + } + } + }; + let collectMapEntries = (...maps: JSONSchemaMap[]) => { + for (let map of maps) { + if (typeof map === 'object') { + for (let key in map) { + let entry = map[key]; + toWalk.push(entry); + } + } + } + }; + let collectArrayEntries = (...arrays: JSONSchema[][]) => { + for (let array of arrays) { + if (Array.isArray(array)) { + toWalk.push.apply(toWalk, array); + } + } + }; + while (toWalk.length) { + let next = toWalk.pop(); + if (seen.indexOf(next) >= 0) { + continue; + } + seen.push(next); + if (next.$ref) { + let segments = next.$ref.split('#', 2); + if (segments[0].length > 0) { + openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL)); + continue; + } else { + resolveLink(next, parentSchema, segments[1]); + } + } + collectEntries(next.items, next.additionalProperties, next.not); + collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies); + collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items); + } + return this.promise.all(openPromises); + }; + + return resolveRefs(schema, schema, schemaURL).then(_ => new ResolvedSchema(schema, resolveErrors)); + } + + public getSchemaForResource(resource: string ): Thenable { + const resolveSchema = () => { + // check for matching file names, last to first + for (let i = this.filePatternAssociations.length - 1; i >= 0; i--) { + let entry = this.filePatternAssociations[i]; + if (entry.matchesPattern(resource)) { + return entry.getCombinedSchema(this).getResolvedSchema(); + } + } + return null; + }; + if (this.customSchemaProvider) { + return this.customSchemaProvider(resource).then(schemaUri => { + return this.loadSchema(schemaUri).then(unsolvedSchema => this.resolveSchemaContent(unsolvedSchema, schemaUri)); + }).then(schema => schema, err => { + return resolveSchema(); + }); + } else { + return resolveSchema(); + } + } + + public createCombinedSchema(combinedSchemaId: string, schemaIds: string[]): ISchemaHandle { + if (schemaIds.length === 1) { + return this.getOrAddSchemaHandle(schemaIds[0]); + } else { + let combinedSchema: JSONSchema = { + allOf: schemaIds.map(schemaId => ({ $ref: schemaId })) + }; + return this.addSchemaHandle(combinedSchemaId, combinedSchema); + } + } +} + +function toDisplayString(url: string) { + try { + let uri = URI.parse(url); + if (uri.scheme === 'file') { + return uri.fsPath; + } + } catch (e) { + // ignore + } + return url; +} diff --git a/src/yaml-languageservice/services/yamlCompletion.ts b/src/yaml-languageservice/services/yamlCompletion.ts new file mode 100644 index 0000000..eb5c8bb --- /dev/null +++ b/src/yaml-languageservice/services/yamlCompletion.ts @@ -0,0 +1,438 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + + +import Parser = require('../parser/jsonParser'); +import Json = require('jsonc-parser'); +import SchemaService = require('./jsonSchemaService'); +import { JSONSchema } from '../jsonSchema'; +import { JSONWorkerContribution, CompletionsCollector } from '../jsonContributions'; +import { PromiseConstructor, Thenable } from 'vscode-json-languageservice'; + +import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, Position, Range, TextEdit } from 'vscode-languageserver-types'; + +import * as nls from 'vscode-nls'; +import { matchOffsetToDocument } from '../utils/arrUtils'; +const localize = nls.loadMessageBundle(); + + +export class YAMLCompletion { + + private schemaService: SchemaService.IJSONSchemaService; + private contributions: JSONWorkerContribution[]; + private promise: PromiseConstructor; + + constructor(schemaService: SchemaService.IJSONSchemaService, contributions: JSONWorkerContribution[] = [], promiseConstructor?: PromiseConstructor) { + this.schemaService = schemaService; + this.contributions = contributions; + this.promise = promiseConstructor || Promise; + } + + public doResolve(item: CompletionItem): Thenable { + for (let i = this.contributions.length - 1; i >= 0; i--) { + if (this.contributions[i].resolveCompletion) { + let resolver = this.contributions[i].resolveCompletion(item); + if (resolver) { + return resolver; + } + } + } + return this.promise.resolve(item); + } + + public doComplete(document: TextDocument, position: Position, doc: Parser.JSONDocument): Thenable { + + let result: CompletionList = { + items: [], + isIncomplete: false + }; + + let offset = document.offsetAt(position); + if(document.getText()[offset] === ":"){ + return null; + } + + let currentDoc = matchOffsetToDocument(offset, doc); + if(currentDoc === null){ + return null; + } + let node = currentDoc.getNodeFromOffsetEndInclusive(offset); + if (this.isInComment(document, node ? node.start : 0, offset)) { + return Promise.resolve(result); + } + + let currentWord = this.getCurrentWord(document, offset); + + let proposed: { [key: string]: CompletionItem } = {}; + let collector: CompletionsCollector = { + add: (suggestion: CompletionItem) => { + let existing = proposed[suggestion.label]; + if (!existing) { + proposed[suggestion.label] = suggestion; + result.items.push(suggestion); + } else if (!existing.documentation) { + existing.documentation = suggestion.documentation; + } + }, + setAsIncomplete: () => { + result.isIncomplete = true; + }, + error: (message: string) => { + console.error(message); + }, + log: (message: string) => { + console.log(message); + }, + getNumberOfProposals: () => { + return result.items.length; + } + }; + + return this.schemaService.getSchemaForResource(document.uri).then((schema) => { + + if(!schema){ + return null; + } + + let collectionPromises: Thenable[] = []; + + let addValue = true; + let currentKey = ''; + + let currentProperty: Parser.PropertyASTNode = null; + if (node) { + + if (node.type === 'string') { + let stringNode = node; + if (stringNode.isKey) { + addValue = !(node.parent && ((node.parent).value)); + currentProperty = node.parent ? node.parent : null; + currentKey = document.getText().substring(node.start + 1, node.end - 1); + if (node.parent) { + node = node.parent.parent; + } + } + } + } + + // proposals for properties + if (node && node.type === 'object') { + // don't suggest properties that are already present + let properties = (node).properties; + properties.forEach(p => { + if (!currentProperty || currentProperty !== p) { + proposed[p.key.value] = CompletionItem.create('__'); + } + }); + + if (schema) { + // property proposals with schema + this.getPropertyCompletions(schema, currentDoc, node, addValue, collector); + } + + let location = node.getPath(); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, false, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + if ((!schema && currentWord.length > 0 && document.getText().charAt(offset - currentWord.length - 1) !== '"')) { + collector.add({ + kind: CompletionItemKind.Property, + label: this.getLabelForValue(currentWord) + }); + } + } + + // proposals for values + let types: { [type: string]: boolean } = {}; + if (schema) { + this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types); + } + if (this.contributions.length > 0) { + this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); + } + + return this.promise.all(collectionPromises).then(() => { + return result; + }); + }); + } + + private getPropertyCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, addValue: boolean, collector: CompletionsCollector): void { + let matchingSchemas = doc.getMatchingSchemas(schema.schema); + matchingSchemas.forEach((s) => { + if (s.node === node && !s.inverted) { + let schemaProperties = s.schema.properties; + if (schemaProperties) { + Object.keys(schemaProperties).forEach((key: string) => { + let propertySchema = schemaProperties[key]; + if (!propertySchema.deprecationMessage && !propertySchema["doNotSuggest"]) { + collector.add({ + kind: CompletionItemKind.Property, + label: key, + filterText: this.getFilterTextForValue(key), + documentation: propertySchema.description || '' + }); + } + }); + } + } + }); + } + + private getValueCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, types: { [type: string]: boolean }): void { + let offsetForSeparator = offset; + let parentKey: string = null; + let valueNode: Parser.ASTNode = null; + + if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean')) { + offsetForSeparator = node.end; + valueNode = node; + node = node.parent; + } + + if(node && node.type === 'null'){ + let nodeParent = node.parent; + + /* + * This is going to be an object for some reason and we need to find the property + * Its an issue with the null node + */ + if(nodeParent && nodeParent.type === "object"){ + for(let prop in nodeParent["properties"]){ + let currNode = nodeParent["properties"][prop]; + if(currNode.key && currNode.key.location === node.location){ + node = currNode; + } + } + } + } + + if (!node) { + this.addSchemaValueCompletions(schema.schema, collector, types); + return; + } + + if ((node.type === 'property') && offset > (node).colonOffset) { + let propertyNode = node; + let valueNode = propertyNode.value; + if (valueNode && offset > valueNode.end) { + return; // we are past the value node + } + parentKey = propertyNode.key.value; + node = node.parent; + } + + if (node && (parentKey !== null || node.type === 'array')) { + let matchingSchemas = doc.getMatchingSchemas(schema.schema); + matchingSchemas.forEach(s => { + if (s.node === node && !s.inverted && s.schema) { + if (s.schema.items) { + if (Array.isArray(s.schema.items)) { + let index = this.findItemAtOffset(node, document, offset); + if (index < s.schema.items.length) { + this.addSchemaValueCompletions(s.schema.items[index], collector, types); + } + } else { + this.addSchemaValueCompletions(s.schema.items, collector, types); + } + } + if (s.schema.properties) { + let propertySchema = s.schema.properties[parentKey]; + if (propertySchema) { + this.addSchemaValueCompletions(propertySchema, collector, types); + } + } + } + }); + } + if(node){ + if (types['boolean']) { + this.addBooleanValueCompletion(true, collector); + this.addBooleanValueCompletion(false, collector); + } + if (types['null']) { + this.addNullValueCompletion(collector); + } + } + } + + private getContributedValueCompletions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, collectionPromises: Thenable[]) { + if (!node) { + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectDefaultCompletions(document.uri, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } else { + if (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') { + node = node.parent; + } + if ((node.type === 'property') && offset > (node).colonOffset) { + let parentKey = (node).key.value; + + let valueNode = (node).value; + if (!valueNode || offset <= valueNode.end) { + let location = node.parent.getPath(); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } + } + } + } + + private addSchemaValueCompletions(schema: JSONSchema, collector: CompletionsCollector, types: { [type: string]: boolean }): void { + this.addDefaultValueCompletions(schema, collector); + this.addEnumValueCompletions(schema, collector); + this.collectTypes(schema, types); + if (Array.isArray(schema.allOf)) { + schema.allOf.forEach(s => this.addSchemaValueCompletions(s, collector, types)); + } + if (Array.isArray(schema.anyOf)) { + schema.anyOf.forEach(s => this.addSchemaValueCompletions(s, collector, types)); + } + if (Array.isArray(schema.oneOf)) { + schema.oneOf.forEach(s => this.addSchemaValueCompletions(s, collector, types)); + } + } + + private addDefaultValueCompletions(schema: JSONSchema, collector: CompletionsCollector, arrayDepth = 0): void { + let hasProposals = false; + if (schema.default) { + let type = schema.type; + let value = schema.default; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = 'array'; + } + collector.add({ + kind: this.getSuggestionKind(type), + label: this.getLabelForValue(value), + detail: localize('json.suggest.default', 'Default value'), + }); + hasProposals = true; + } + if (!hasProposals && schema.items && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, collector, arrayDepth + 1); + } + } + + private addEnumValueCompletions(schema: JSONSchema, collector: CompletionsCollector): void { + if (Array.isArray(schema.enum)) { + for (let i = 0, length = schema.enum.length; i < length; i++) { + let enm = schema.enum[i]; + let documentation = schema.description; + if (schema.enumDescriptions && i < schema.enumDescriptions.length) { + documentation = schema.enumDescriptions[i]; + } + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(enm), + documentation + }); + } + } + } + + private collectTypes(schema: JSONSchema, types: { [type: string]: boolean }) { + let type = schema.type; + if (Array.isArray(type)) { + type.forEach(t => types[t] = true); + } else { + types[type] = true; + } + } + + private addBooleanValueCompletion(value: boolean, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('boolean'), + label: value ? 'true' : 'false', + documentation: '' + }); + } + + private addNullValueCompletion(collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('null'), + label: 'null', + documentation: '' + }); + } + + private getLabelForValue(value: any): string { + let label = typeof value === "string" ? value : JSON.stringify(value); + if (label.length > 57) { + return label.substr(0, 57).trim() + '...'; + } + return label; + } + + private getFilterTextForValue(value): string { + return JSON.stringify(value); + } + + private getSuggestionKind(type: any): CompletionItemKind { + if (Array.isArray(type)) { + let array = type; + type = array.length > 0 ? array[0] : null; + } + if (!type) { + return CompletionItemKind.Value; + } + switch (type) { + case 'string': return CompletionItemKind.Value; + case 'object': return CompletionItemKind.Module; + case 'property': return CompletionItemKind.Property; + default: return CompletionItemKind.Value; + } + } + + private getCurrentWord(document: TextDocument, offset: number) { + var i = offset - 1; + var text = document.getText(); + while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { + i--; + } + return text.substring(i + 1, offset); + } + + private findItemAtOffset(node: Parser.ASTNode, document: TextDocument, offset: number) { + let scanner = Json.createScanner(document.getText(), true); + let children = node.getChildNodes(); + for (let i = children.length - 1; i >= 0; i--) { + let child = children[i]; + if (offset > child.end) { + scanner.setPosition(child.end); + let token = scanner.scan(); + if (token === Json.SyntaxKind.CommaToken && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) { + return i + 1; + } + return i; + } else if (offset >= child.start) { + return i; + } + } + return 0; + } + + private isInComment(document: TextDocument, start: number, offset: number) { + let scanner = Json.createScanner(document.getText(), false); + scanner.setPosition(start); + let token = scanner.scan(); + while (token !== Json.SyntaxKind.EOF && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) { + token = scanner.scan(); + } + return (token === Json.SyntaxKind.LineCommentTrivia || token === Json.SyntaxKind.BlockCommentTrivia) && scanner.getTokenOffset() <= offset; + } +} \ No newline at end of file diff --git a/src/yaml-languageservice/services/yamlHover.ts b/src/yaml-languageservice/services/yamlHover.ts new file mode 100644 index 0000000..a332d7b --- /dev/null +++ b/src/yaml-languageservice/services/yamlHover.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + + +import Parser = require('../parser/jsonParser'); +import SchemaService = require('./jsonSchemaService'); +import {JSONWorkerContribution} from '../jsonContributions'; +import {PromiseConstructor, Thenable} from 'vscode-json-languageservice'; + +import {Hover, TextDocument, Position, Range, MarkedString} from 'vscode-languageserver-types'; +import { matchOffsetToDocument } from '../utils/arrUtils'; + +export class YAMLHover { + + private schemaService: SchemaService.IJSONSchemaService; + private contributions: JSONWorkerContribution[]; + private promise: PromiseConstructor; + + constructor(schemaService: SchemaService.IJSONSchemaService, contributions: JSONWorkerContribution[] = [], promiseConstructor: PromiseConstructor) { + this.schemaService = schemaService; + this.contributions = contributions; + this.promise = promiseConstructor || Promise; + } + + public doHover(document: TextDocument, position: Position, doc: Parser.JSONDocument): Thenable { + + let offset = document.offsetAt(position); + let currentDoc = matchOffsetToDocument(offset, doc); + if(currentDoc === null){ + return null; + } + let node = currentDoc.getNodeFromOffset(offset); + if (!node || (node.type === 'object' || node.type === 'array') && offset > node.start + 1 && offset < node.end - 1) { + return this.promise.resolve(void 0); + } + let hoverRangeNode = node; + + // use the property description when hovering over an object key + if (node.type === 'string') { + let stringNode = node; + if (stringNode.isKey) { + let propertyNode = node.parent; + node = propertyNode.value; + if (!node) { + return this.promise.resolve(void 0); + } + } + } + + let hoverRange = Range.create(document.positionAt(hoverRangeNode.start), document.positionAt(hoverRangeNode.end)); + + var createHover = (contents: MarkedString[]) => { + let result: Hover = { + contents: contents, + range: hoverRange + }; + return result; + }; + + let location = node.getPath(); + for (let i = this.contributions.length - 1; i >= 0; i--) { + let contribution = this.contributions[i]; + let promise = contribution.getInfoContribution(document.uri, location); + if (promise) { + return promise.then(htmlContent => createHover(htmlContent)); + } + } + + return this.schemaService.getSchemaForResource(document.uri).then((schema) => { + if (schema) { + + let matchingSchemas = currentDoc.getMatchingSchemas(schema.schema, node.start); + + let title: string = null; + let markdownDescription: string = null; + let markdownEnumValueDescription = null, enumValue = null; + matchingSchemas.every((s) => { + if (s.node === node && !s.inverted && s.schema) { + title = title || s.schema.title; + markdownDescription = markdownDescription || s.schema["markdownDescription"] || toMarkdown(s.schema.description); + if (s.schema.enum) { + let idx = s.schema.enum.indexOf(node.getValue()); + if (s.schema["markdownEnumDescriptions"]) { + markdownEnumValueDescription = s.schema["markdownEnumDescriptions"][idx]; + } else if (s.schema.enumDescriptions) { + markdownEnumValueDescription = toMarkdown(s.schema.enumDescriptions[idx]); + } + if (markdownEnumValueDescription) { + enumValue = s.schema.enum[idx]; + if (typeof enumValue !== 'string') { + enumValue = JSON.stringify(enumValue); + } + } + } + } + return true; + }); + let result = ''; + if (title) { + result = toMarkdown(title); + } + if (markdownDescription) { + if (result.length > 0) { + result += "\n\n"; + } + result += markdownDescription; + } + if (markdownEnumValueDescription) { + if (result.length > 0) { + result += "\n\n"; + } + result += `\`${toMarkdown(enumValue)}\`: ${markdownEnumValueDescription}`; + } + return createHover([result]); + } + return void 0; + }); + } +} + +function toMarkdown(plain: string) { + if (plain) { + let res = plain.replace(/([^\n\r])(\r?\n)([^\n\r])/gm, '$1\n\n$3'); // single new lines to \n\n (Markdown paragraph) + return res.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + } + return void 0; +} \ No newline at end of file diff --git a/src/yaml-languageservice/services/yamlValidation.ts b/src/yaml-languageservice/services/yamlValidation.ts new file mode 100644 index 0000000..2f83c83 --- /dev/null +++ b/src/yaml-languageservice/services/yamlValidation.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { JSONSchemaService } from './jsonSchemaService'; +import { JSONDocument, ObjectASTNode, IProblem, ProblemSeverity } from '../parser/jsonParser'; +import { TextDocument, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; +import { PromiseConstructor, Thenable } from '../yamlLanguageService'; +import { LanguageSettings } from '../../vscode-yaml-languageservice/yamlLanguageService'; + +export class YAMLValidation { + + private jsonSchemaService: JSONSchemaService; + private promise: PromiseConstructor; + private comments: boolean; + private validationEnabled: boolean; + + public constructor(jsonSchemaService, promiseConstructor) { + this.jsonSchemaService = jsonSchemaService; + this.promise = promiseConstructor; + this.validationEnabled = true; + } + + public configure(shouldValidate: LanguageSettings) { + if (shouldValidate) { + this.validationEnabled = shouldValidate.validate; + } + } + + public doValidation(textDocument, yamlDocument) { + + if (!this.validationEnabled) { + return this.promise.resolve([]); + } + + return this.jsonSchemaService.getSchemaForResource(textDocument.uri).then(function (schema) { + if (schema) { + + for (let currentYAMLDoc in yamlDocument.documents) { + let currentDoc = yamlDocument.documents[currentYAMLDoc]; + let diagnostics = currentDoc.getValidationProblems(schema.schema); + for (let diag in diagnostics) { + let curDiagnostic = diagnostics[diag]; + currentDoc.errors.push({ location: { start: curDiagnostic.location.start, end: curDiagnostic.location.end }, message: curDiagnostic.message }) + } + } + + + } + var diagnostics = []; + var added = {}; + for (let currentYAMLDoc in yamlDocument.documents) { + let currentDoc = yamlDocument.documents[currentYAMLDoc]; + currentDoc.errors.concat(currentDoc.warnings).forEach(function (error, idx) { + // remove duplicated messages + var signature = error.location.start + ' ' + error.location.end + ' ' + error.message; + if (!added[signature]) { + added[signature] = true; + var range = { + start: textDocument.positionAt(error.location.start), + end: textDocument.positionAt(error.location.end) + }; + diagnostics.push({ + severity: idx >= currentDoc.errors.length ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error, + range: range, + message: error.message + }); + } + }); + } + return diagnostics; + }); + } +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/arrUtils.ts b/src/yaml-languageservice/utils/arrUtils.ts new file mode 100644 index 0000000..37a900c --- /dev/null +++ b/src/yaml-languageservice/utils/arrUtils.ts @@ -0,0 +1,72 @@ +import { YAMLDocument } from "../../vscode-yaml-languageservice/yamlLanguageService"; +import { SingleYAMLDocument } from "../parser/yamlParser"; + +export function removeDuplicates(arr, prop) { + var new_arr = []; + var lookup = {}; + + for (var i in arr) { + lookup[arr[i][prop]] = arr[i]; + } + + for (i in lookup) { + new_arr.push(lookup[i]); + } + + return new_arr; +} + +export function getLineOffsets(textDocString: String): number[] { + + let lineOffsets: number[] = []; + let text = textDocString; + let isLineStart = true; + for (let i = 0; i < text.length; i++) { + if (isLineStart) { + lineOffsets.push(i); + isLineStart = false; + } + let ch = text.charAt(i); + isLineStart = (ch === '\r' || ch === '\n'); + if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { + i++; + } + } + if (isLineStart && text.length > 0) { + lineOffsets.push(text.length); + } + + return lineOffsets; +} + +export function removeDuplicatesObj(objArray) { + + let nonDuplicateSet = new Set(); + let nonDuplicateArr = []; + for (let obj in objArray) { + + let currObj = objArray[obj]; + let stringifiedObj = JSON.stringify(currObj); + if (!nonDuplicateSet.has(stringifiedObj)) { + nonDuplicateArr.push(currObj); + nonDuplicateSet.add(stringifiedObj); + } + + } + + return nonDuplicateArr; + +} + +export function matchOffsetToDocument(offset: number, jsonDocuments): SingleYAMLDocument { + + for (let jsonDoc in jsonDocuments.documents) { + let currJsonDoc = jsonDocuments.documents[jsonDoc]; + if (currJsonDoc.root && currJsonDoc.root.end >= offset && currJsonDoc.root.start <= offset) { + return currJsonDoc; + } + } + + return null; + +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/documentPositionCalculator.ts b/src/yaml-languageservice/utils/documentPositionCalculator.ts new file mode 100644 index 0000000..b408a38 --- /dev/null +++ b/src/yaml-languageservice/utils/documentPositionCalculator.ts @@ -0,0 +1,62 @@ +"use strict" + +export function insertionPointReturnValue(pt: number) { + return ((-pt) - 1) +} + +export function binarySearch(array: number[], sought: number) { + + let lower = 0 + let upper = array.length - 1 + + while (lower <= upper) { + let idx = Math.floor((lower + upper) / 2) + const value = array[idx] + + if (value === sought) { + return idx; + } + + if (lower === upper) { + const insertionPoint = (value < sought) ? idx + 1 : idx + return insertionPointReturnValue(insertionPoint) + } + + if (sought > value) { + lower = idx + 1; + } else if (sought < value) { + upper = idx - 1; + } + } +} + +export function getLineStartPositions(text: string) { + const lineStartPositions = [0]; + for (var i = 0; i < text.length; i++) { + const c = text[i]; + + if (c === '\r') { + // Check for Windows encoding, otherwise we are old Mac + if (i + 1 < text.length && text[i + 1] == '\n') { + i++; + } + + lineStartPositions.push(i + 1); + } else if (c === '\n'){ + lineStartPositions.push(i + 1); + } + } + + return lineStartPositions; +} + +export function getPosition(pos: number, lineStartPositions: number[]){ + let line = binarySearch(lineStartPositions, pos) + + if (line < 0){ + const insertionPoint = -1 * line - 1; + line = insertionPoint - 1; + } + + return {line, column: pos - lineStartPositions[line]} +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/errorHandler.ts b/src/yaml-languageservice/utils/errorHandler.ts new file mode 100644 index 0000000..afa865c --- /dev/null +++ b/src/yaml-languageservice/utils/errorHandler.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { DiagnosticSeverity } from "vscode-languageserver/lib/main"; + +export class ErrorHandler { + private errorResultsList; + private textDocument; + + constructor(textDocument){ + this.errorResultsList = []; + this.textDocument = textDocument; + } + + public addErrorResult(errorNode, errorMessage, errorType){ + this.errorResultsList.push({ + severity: errorType, + range: { + start: this.textDocument.positionAt(errorNode.startPosition), + end: this.textDocument.positionAt(errorNode.endPosition) + }, + message: errorMessage + }); + + } + + public getErrorResultsList(){ + return this.errorResultsList; + } + +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/objects.ts b/src/yaml-languageservice/utils/objects.ts new file mode 100644 index 0000000..33cfde0 --- /dev/null +++ b/src/yaml-languageservice/utils/objects.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export function equals(one: any, other: any): boolean { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== 'object') { + return false; + } + if ((Array.isArray(one)) !== (Array.isArray(other))) { + return false; + } + + var i: number, + key: string; + + if (Array.isArray(one)) { + if (one.length !== other.length) { + return false; + } + for (i = 0; i < one.length; i++) { + if (!equals(one[i], other[i])) { + return false; + } + } + } else { + var oneKeys: string[] = []; + + for (key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + var otherKeys: string[] = []; + for (key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!equals(oneKeys, otherKeys)) { + return false; + } + for (i = 0; i < oneKeys.length; i++) { + if (!equals(one[oneKeys[i]], other[oneKeys[i]])) { + return false; + } + } + } + return true; +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/strings.ts b/src/yaml-languageservice/utils/strings.ts new file mode 100644 index 0000000..bd1bee1 --- /dev/null +++ b/src/yaml-languageservice/utils/strings.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } + + for (let i = 0; i < needle.length; i++) { + if (haystack[i] !== needle[i]) { + return false; + } + } + + return true; +} + +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + let diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.lastIndexOf(needle) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } +} + +export function convertSimple2RegExpPattern(pattern: string): string { + return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); +} \ No newline at end of file diff --git a/src/yaml-languageservice/utils/uri.ts b/src/yaml-languageservice/utils/uri.ts new file mode 100644 index 0000000..4a76b8b --- /dev/null +++ b/src/yaml-languageservice/utils/uri.ts @@ -0,0 +1,351 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +function _encode(ch: string): string { + return '%' + ch.charCodeAt(0).toString(16).toUpperCase(); +} + +// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +function encodeURIComponent2(str: string): string { + return encodeURIComponent(str).replace(/[!'()*]/g, _encode); +} + +function encodeNoop(str: string): string { + return str; +} + + +/** + * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component paths + * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + * + * + */ +export default class URI { + + private static _empty = ''; + private static _slash = '/'; + private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + private static _driveLetterPath = /^\/[a-zA-z]:/; + private static _upperCaseDrive = /^(\/)?([A-Z]:)/; + + private _scheme: string; + private _authority: string; + private _path: string; + private _query: string; + private _fragment: string; + private _formatted: string; + private _fsPath: string; + + constructor() { + this._scheme = URI._empty; + this._authority = URI._empty; + this._path = URI._empty; + this._query = URI._empty; + this._fragment = URI._empty; + + this._formatted = null; + this._fsPath = null; + } + + /** + * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + get scheme() { + return this._scheme; + } + + /** + * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. + * The part between the first double slashes and the next slash. + */ + get authority() { + return this._authority; + } + + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + get path() { + return this._path; + } + + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + get query() { + return this._query; + } + + /** + * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. + */ + get fragment() { + return this._fragment; + } + + // ---- filesystem path ----------------------- + + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths and normalize windows drive letters to lower-case. Also + * uses the platform specific path separator. Will *not* validate the path for + * invalid characters and semantics. Will *not* look at the scheme of this URI. + */ + get fsPath() { + if (!this._fsPath) { + var value: string; + if (this._authority && this.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${this._authority}${this._path}`; + } else if (URI._driveLetterPath.test(this._path)) { + // windows drive letter: file:///c:/far/boo + value = this._path[1].toLowerCase() + this._path.substr(2); + } else { + // other path + value = this._path; + } + if (process.platform === 'win32') { + value = value.replace(/\//g, '\\'); + } + this._fsPath = value; + } + return this._fsPath; + } + + // ---- modify to new ------------------------- + + public with(scheme: string, authority: string, path: string, query: string, fragment: string): URI { + var ret = new URI(); + ret._scheme = scheme || this.scheme; + ret._authority = authority || this.authority; + ret._path = path || this.path; + ret._query = query || this.query; + ret._fragment = fragment || this.fragment; + URI._validate(ret); + return ret; + } + + public withScheme(value: string): URI { + return this.with(value, undefined, undefined, undefined, undefined); + } + + public withAuthority(value: string): URI { + return this.with(undefined, value, undefined, undefined, undefined); + } + + public withPath(value: string): URI { + return this.with(undefined, undefined, value, undefined, undefined); + } + + public withQuery(value: string): URI { + return this.with(undefined, undefined, undefined, value, undefined); + } + + public withFragment(value: string): URI { + return this.with(undefined, undefined, undefined, undefined, value); + } + + // ---- parse & validate ------------------------ + + public static parse(value: string): URI { + const ret = new URI(); + const data = URI._parseComponents(value); + ret._scheme = data.scheme; + ret._authority = decodeURIComponent(data.authority); + ret._path = decodeURIComponent(data.path); + ret._query = decodeURIComponent(data.query); + ret._fragment = decodeURIComponent(data.fragment); + URI._validate(ret); + return ret; + } + + public static file(path: string): URI { + + const ret = new URI(); + ret._scheme = 'file'; + + // normalize to fwd-slashes + path = path.replace(/\\/g, URI._slash); + + // check for authority as used in UNC shares + // or use the path as given + if (path[0] === URI._slash && path[0] === path[1]) { + let idx = path.indexOf(URI._slash, 2); + if (idx === -1) { + ret._authority = path.substring(2); + } else { + ret._authority = path.substring(2, idx); + ret._path = path.substring(idx); + } + } else { + ret._path = path; + } + + // Ensure that path starts with a slash + // or that it is at least a slash + if (ret._path[0] !== URI._slash) { + ret._path = URI._slash + ret._path; + } + + URI._validate(ret); + + return ret; + } + + private static _parseComponents(value: string): UriComponents { + + const ret: UriComponents = { + scheme: URI._empty, + authority: URI._empty, + path: URI._empty, + query: URI._empty, + fragment: URI._empty, + }; + + const match = URI._regexp.exec(value); + if (match) { + ret.scheme = match[2] || ret.scheme; + ret.authority = match[4] || ret.authority; + ret.path = match[5] || ret.path; + ret.query = match[7] || ret.query; + ret.fragment = match[9] || ret.fragment; + } + return ret; + } + + public static create(scheme?: string, authority?: string, path?: string, query?: string, fragment?: string): URI { + return new URI().with(scheme, authority, path, query, fragment); + } + + private static _validate(ret: URI): void { + + // validation + // path, http://tools.ietf.org/html/rfc3986#section-3.3 + // If a URI contains an authority component, then the path component + // must either be empty or begin with a slash ("/") character. If a URI + // does not contain an authority component, then the path cannot begin + // with two slash characters ("//"). + if (ret.authority && ret.path && ret.path[0] !== '/') { + throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'); + } + if (!ret.authority && ret.path.indexOf('//') === 0) { + throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'); + } + } + + // ---- printing/externalize --------------------------- + + /** + * + * @param skipEncoding Do not encode the result, default is `false` + */ + public toString(skipEncoding: boolean = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = URI._asFormatted(this, false); + } + return this._formatted; + } else { + // we don't cache that + return URI._asFormatted(this, true); + } + } + + private static _asFormatted(uri: URI, skipEncoding: boolean): string { + + const encoder = !skipEncoding + ? encodeURIComponent2 + : encodeNoop; + + const parts: string[] = []; + + let { scheme, authority, path, query, fragment } = uri; + if (scheme) { + parts.push(scheme, ':'); + } + if (authority || scheme === 'file') { + parts.push('//'); + } + if (authority) { + authority = authority.toLowerCase(); + let idx = authority.indexOf(':'); + if (idx === -1) { + parts.push(encoder(authority)); + } else { + parts.push(encoder(authority.substr(0, idx)), authority.substr(idx)); + } + } + if (path) { + // lower-case windown drive letters in /C:/fff + const m = URI._upperCaseDrive.exec(path); + if (m) { + path = m[1] + m[2].toLowerCase() + path.substr(m[1].length + m[2].length); + } + + // encode every segement but not slashes + // make sure that # and ? are always encoded + // when occurring in paths - otherwise the result + // cannot be parsed back again + let lastIdx = 0; + while (true) { + let idx = path.indexOf(URI._slash, lastIdx); + if (idx === -1) { + parts.push(encoder(path.substring(lastIdx)).replace(/[#?]/, _encode)); + break; + } + parts.push(encoder(path.substring(lastIdx, idx)).replace(/[#?]/, _encode), URI._slash); + lastIdx = idx + 1; + }; + } + if (query) { + parts.push('?', encoder(query)); + } + if (fragment) { + parts.push('#', encoder(fragment)); + } + + return parts.join(URI._empty); + } + + public toJSON(): any { + return { + scheme: this.scheme, + authority: this.authority, + path: this.path, + fsPath: this.fsPath, + query: this.query, + fragment: this.fragment, + external: this.toString(), + $mid: 1 + }; + } +} + +interface UriComponents { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; +} + +interface UriState extends UriComponents { + $mid: number; + fsPath: string; + external: string; +} \ No newline at end of file diff --git a/src/yaml-languageservice/yamlLanguageService.ts b/src/yaml-languageservice/yamlLanguageService.ts new file mode 100644 index 0000000..73dfbe9 --- /dev/null +++ b/src/yaml-languageservice/yamlLanguageService.ts @@ -0,0 +1,125 @@ +import { JSONSchemaService } from './services/jsonSchemaService' +import { TextDocument, Position, CompletionList } from 'vscode-languageserver-types'; +import { JSONSchema } from './jsonSchema'; +import { parse as parseYAML } from "./parser/yamlParser"; +import { YAMLDocumentSymbols } from './services/documentSymbols'; +import { YAMLCompletion } from "./services/yamlCompletion"; +import { YAMLHover } from "./services/yamlHover"; +import { YAMLValidation } from "./services/yamlValidation"; +import { LanguageSettings, YAMLDocument, Diagnostic } from '../vscode-yaml-languageservice/yamlLanguageService'; + +export interface LanguageSettings { + validate?: boolean; //Setting for whether we want to validate the schema + schemas?: any[]; //List of schemas +} + +export interface PromiseConstructor { + /** + * Creates a new Promise. + * @param executor A callback used to initialize the promise. This callback is passed two arguments: + * a resolve callback used resolve the spromise with a value or the result of another promise, + * and a reject callback used to reject the promise with a provided reason or error. + */ + new (executor: (resolve: (value?: T | Thenable) => void, reject: (reason?: any) => void) => void): Thenable; + + /** + * Creates a Promise that is resolved with an array of results when all of the provided Promises + * resolve, or rejected when any Promise is rejected. + * @param values An array of Promises. + * @returns A new Promise. + */ + all(values: Array>): Thenable; + /** + * Creates a new rejected promise for the provided reason. + * @param reason The reason the promise was rejected. + * @returns A new rejected Promise. + */ + reject(reason: any): Thenable; + + /** + * Creates a new resolved promise for the provided value. + * @param value A promise. + * @returns A promise whose internal state matches the provided promise. + */ + resolve(value: T | Thenable): Thenable; + +} + +export interface Thenable { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; +} + +export interface WorkspaceContextService { + resolveRelativePath(relativePath: string, resource: string): string; +} +/** + * The schema request service is used to fetch schemas. The result should the schema file comment, or, + * in case of an error, a displayable error string + */ +export interface SchemaRequestService { + (uri: string): Thenable; +} + +export interface SchemaConfiguration { + /** + * The URI of the schema, which is also the identifier of the schema. + */ + uri: string; + /** + * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' + */ + fileMatch?: string[]; + /** + * The schema for the given URI. + * If no schema is provided, the schema will be fetched with the schema request service (if available). + */ + schema?: JSONSchema; +} + +export interface LanguageService { + configure(settings): void; + parseYAMLDocument(document: TextDocument): YAMLDocument, + doComplete(document: TextDocument, position: Position, doc): Thenable; + doValidation(document: TextDocument, yamlDocument): Thenable; + doHover(document: TextDocument, position: Position, doc); + findDocumentSymbols(document: TextDocument, doc); + doResolve(completionItem); + resetSchema(uri: string): boolean; +} + +export function getLanguageService(schemaRequestService, workspaceContext, contributions, customSchemaProvider, promiseConstructor?): LanguageService { + let promise = promiseConstructor || Promise; + + let schemaService = new JSONSchemaService(schemaRequestService, workspaceContext, customSchemaProvider); + + let completer = new YAMLCompletion(schemaService, contributions, promise); + let hover = new YAMLHover(schemaService, contributions, promise); + let yamlDocumentSymbols = new YAMLDocumentSymbols(); + let yamlValidation = new YAMLValidation(schemaService, promise); + + return { + configure: (settings) => { + schemaService.clearExternalSchemas(); + if (settings.schemas) { + settings.schemas.forEach(settings => { + schemaService.registerExternalSchema(settings.uri, settings.fileMatch, settings.schema); + }); + } + yamlValidation.configure(settings); + }, + parseYAMLDocument: (document: TextDocument) => parseYAML(document.getText()), + doComplete: completer.doComplete.bind(completer), + doResolve: completer.doResolve.bind(completer), + doValidation: yamlValidation.doValidation.bind(yamlValidation), + doHover: hover.doHover.bind(hover), + findDocumentSymbols: yamlDocumentSymbols.findDocumentSymbols.bind(yamlDocumentSymbols), + resetSchema: (uri: string) => schemaService.onResourceChange(uri) + } +}