From 0e52744c10ca0b7b2608e06b0f84d11464dea62a Mon Sep 17 00:00:00 2001 From: Lilian Saget-Lethias Date: Wed, 3 Jul 2019 13:11:40 +0200 Subject: [PATCH] :sparkles: Add yaml --- .gitignore | 1 + test.ts | 1 + tsconfig.json | 2 +- yaml/Mark.ts | 72 ++ yaml/README.md | 16 + yaml/Schema.ts | 95 ++ yaml/State.ts | 6 + yaml/Type.ts | 50 + yaml/devDeps.ts | 3 + yaml/dumper/DumperState.ts | 111 ++ yaml/dumper/dumper.ts | 868 +++++++++++++++ yaml/error/YAMLError.ts | 17 + yaml/example/dump.ts | 21 + yaml/example/inout.ts | 25 + yaml/example/parse.ts | 17 + yaml/example/sample_document.ts | 20 + yaml/example/sample_document.yml | 197 ++++ yaml/loader/LoaderState.ts | 68 ++ yaml/loader/loader.ts | 1757 ++++++++++++++++++++++++++++++ yaml/mod.ts | 5 + yaml/parse.ts | 18 + yaml/parse_test.ts | 22 + yaml/schema/core.ts | 12 + yaml/schema/default_full.ts | 20 + yaml/schema/default_safe.ts | 15 + yaml/schema/failsafe.ts | 9 + yaml/schema/json.ts | 15 + yaml/schema/mod.ts | 5 + yaml/stringify.ts | 10 + yaml/stringify_test.ts | 39 + yaml/test.ts | 8 + yaml/type/binary.ts | 132 +++ yaml/type/bool.ts | 34 + yaml/type/float.ts | 122 +++ yaml/type/int.ts | 185 ++++ yaml/type/map.ts | 8 + yaml/type/merge.ts | 10 + yaml/type/mod.ts | 13 + yaml/type/nil.ts | 40 + yaml/type/omap.ts | 40 + yaml/type/pairs.ts | 43 + yaml/type/seq.ts | 8 + yaml/type/set.ts | 25 + yaml/type/str.ts | 8 + yaml/type/timestamp.ts | 91 ++ yaml/utils.ts | 76 ++ 46 files changed, 4359 insertions(+), 1 deletion(-) create mode 100644 yaml/Mark.ts create mode 100644 yaml/README.md create mode 100644 yaml/Schema.ts create mode 100644 yaml/State.ts create mode 100644 yaml/Type.ts create mode 100644 yaml/devDeps.ts create mode 100644 yaml/dumper/DumperState.ts create mode 100644 yaml/dumper/dumper.ts create mode 100644 yaml/error/YAMLError.ts create mode 100644 yaml/example/dump.ts create mode 100644 yaml/example/inout.ts create mode 100644 yaml/example/parse.ts create mode 100644 yaml/example/sample_document.ts create mode 100644 yaml/example/sample_document.yml create mode 100644 yaml/loader/LoaderState.ts create mode 100644 yaml/loader/loader.ts create mode 100644 yaml/mod.ts create mode 100644 yaml/parse.ts create mode 100644 yaml/parse_test.ts create mode 100644 yaml/schema/core.ts create mode 100644 yaml/schema/default_full.ts create mode 100644 yaml/schema/default_safe.ts create mode 100644 yaml/schema/failsafe.ts create mode 100644 yaml/schema/json.ts create mode 100644 yaml/schema/mod.ts create mode 100644 yaml/stringify.ts create mode 100644 yaml/stringify_test.ts create mode 100644 yaml/test.ts create mode 100644 yaml/type/binary.ts create mode 100644 yaml/type/bool.ts create mode 100644 yaml/type/float.ts create mode 100644 yaml/type/int.ts create mode 100644 yaml/type/map.ts create mode 100644 yaml/type/merge.ts create mode 100644 yaml/type/mod.ts create mode 100644 yaml/type/nil.ts create mode 100644 yaml/type/omap.ts create mode 100644 yaml/type/pairs.ts create mode 100644 yaml/type/seq.ts create mode 100644 yaml/type/set.ts create mode 100644 yaml/type/str.ts create mode 100644 yaml/type/timestamp.ts create mode 100644 yaml/utils.ts diff --git a/.gitignore b/.gitignore index af7c9e4793ff..41c93c7bd84d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ deno.d.ts node_modules package.json package-lock.json +yarn.lock .vscode diff --git a/test.ts b/test.ts index 1f41003f3d6f..79ed5f1e01bc 100755 --- a/test.ts +++ b/test.ts @@ -24,6 +24,7 @@ import "./util/test.ts"; import "./ws/test.ts"; import "./encoding/test.ts"; import "./os/test.ts"; +import "./yaml/test.ts"; import { xrun } from "./prettier/util.ts"; import { red, green } from "./colors/mod.ts"; diff --git a/tsconfig.json b/tsconfig.json index f438205b013d..732e8322ce5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "tsconfig.test.json", + "extends": "./tsconfig.test.json", "compilerOptions": { "allowJs": true, "baseUrl": ".", diff --git a/yaml/Mark.ts b/yaml/Mark.ts new file mode 100644 index 000000000000..ac91562d8554 --- /dev/null +++ b/yaml/Mark.ts @@ -0,0 +1,72 @@ +import { repeat } from "./utils.ts"; + +export class Mark { + constructor( + public name: string, + public buffer: string, + public position: number, + public line: number, + public column: number + ) {} + + public getSnippet(indent = 4, maxLength = 75) { + if (!this.buffer) return null; + + let head = ""; + let start = this.position; + + while ( + start > 0 && + "\x00\r\n\x85\u2028\u2029".indexOf(this.buffer.charAt(start - 1)) === -1 + ) { + start -= 1; + if (this.position - start > maxLength / 2 - 1) { + head = " ... "; + start += 5; + break; + } + } + + let tail = ""; + let end = this.position; + + while ( + end < this.buffer.length && + "\x00\r\n\x85\u2028\u2029".indexOf(this.buffer.charAt(end)) === -1 + ) { + end += 1; + if (end - this.position > maxLength / 2 - 1) { + tail = " ... "; + end -= 5; + break; + } + } + + const snippet = this.buffer.slice(start, end); + return `${repeat(" ", indent)}${head}${snippet}${tail}\n${repeat( + " ", + indent + this.position - start + head.length + )}^`; + } + + public toString(compact?: boolean) { + let 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; + } +} diff --git a/yaml/README.md b/yaml/README.md new file mode 100644 index 000000000000..b87164462ecf --- /dev/null +++ b/yaml/README.md @@ -0,0 +1,16 @@ +# deno-yaml +YAML parser / dumper for Deno + +Heavily inspired from [js-yaml] + +# Example +See `./example` folder and [js-yaml] repository. + +# Limitations +- `binary` type is currently not stable +- `function`, `regexp`, and `undefined` type are currently not supported + +# Basic usage +TBD + +[js-yaml]: https://github.com/nodeca/js-yaml diff --git a/yaml/Schema.ts b/yaml/Schema.ts new file mode 100644 index 000000000000..ba8cec60e842 --- /dev/null +++ b/yaml/Schema.ts @@ -0,0 +1,95 @@ +import { YAMLError } from "./error/YAMLError.ts"; +import { KindType, Type } from "./Type.ts"; +import { ArrayObject } from "./utils.ts"; + +function compileList( + schema: Schema, + name: "implicit" | "explicit", + result: Type[] +): Type[] { + const exclude: number[] = []; + + for (const includedSchema of schema.include) { + result = compileList(includedSchema, name, result); + } + + for (const currentType of schema[name]) { + for ( + let previousIndex = 0; + previousIndex < result.length; + previousIndex++ + ) { + const previousType = result[previousIndex]; + if ( + previousType.tag === currentType.tag && + previousType.kind === currentType.kind + ) { + exclude.push(previousIndex); + } + } + + result.push(currentType); + } + + return result.filter((type, index) => !exclude.includes(index)); +} + +export type TypeMap = { [k in KindType | "fallback"]: ArrayObject }; +function compileMap(...typesList: Type[][]) { + const result: TypeMap = { + fallback: {}, + mapping: {}, + scalar: {}, + sequence: {} + }; + + for (const types of typesList) { + for (const type of types) { + if (type.kind !== null) { + result[type.kind][type.tag] = result["fallback"][type.tag] = type; + } + } + } + return result; +} + +export class Schema implements SchemaDefinition { + public static SCHEMA_DEFAULT?: Schema; + + public implicit: Type[]; + public explicit: Type[]; + public include: Schema[]; + + public compiledImplicit: Type[]; + public compiledExplicit: Type[]; + public compiledTypeMap: TypeMap; + + constructor(definition: SchemaDefinition) { + this.explicit = definition.explicit || []; + this.implicit = definition.implicit || []; + this.include = definition.include || []; + + for (const type of this.implicit) { + if (type.loadKind && type.loadKind !== "scalar") { + throw new YAMLError( + "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 + ); + } + + public static create() {} +} + +export interface SchemaDefinition { + implicit?: any[]; + explicit?: Type[]; + include?: Schema[]; +} diff --git a/yaml/State.ts b/yaml/State.ts new file mode 100644 index 000000000000..b81917699405 --- /dev/null +++ b/yaml/State.ts @@ -0,0 +1,6 @@ +import { SchemaDefinition } from "./Schema.ts"; +import { DEFAULT_FULL_SCHEMA } from "./schema/mod.ts"; + +export abstract class State { + constructor(public schema: SchemaDefinition = DEFAULT_FULL_SCHEMA) {} +} diff --git a/yaml/Type.ts b/yaml/Type.ts new file mode 100644 index 000000000000..6e3f143c075e --- /dev/null +++ b/yaml/Type.ts @@ -0,0 +1,50 @@ +import { ArrayObject } from "./utils.ts"; + +export type KindType = "sequence" | "scalar" | "mapping"; +export type StyleVariant = "lowercase" | "uppercase" | "camelcase" | "decimal"; +export type RepresentFn = (data: any, style?: StyleVariant) => any; + +const DEFAULT_RESOLVE = () => true; +const DEFAULT_CONSTRUCT = (data: any) => data; + +interface TypeOptions { + kind: KindType; + resolve?: (data: any) => boolean; + construct?: (data: string) => any; + instanceOf?: any; + predicate?: (data: object) => boolean; + represent?: RepresentFn | ArrayObject; + defaultStyle?: StyleVariant; + styleAliases?: { [x: string]: any }; +} + +function checkTagFormat(tag: string): string { + return tag; +} + +export class Type { + public tag: string; + public kind: KindType | null = null; + public instanceOf: any; + public predicate?: (data: object) => boolean; + public represent?: RepresentFn | ArrayObject; + public defaultStyle?: StyleVariant; + public styleAliases?: { [x: string]: any }; + public loadKind?: KindType; + + constructor(tag: string, options?: TypeOptions) { + this.tag = checkTagFormat(tag); + if (options) { + this.kind = options.kind; + this.resolve = options.resolve || DEFAULT_RESOLVE; + this.construct = options.construct || DEFAULT_CONSTRUCT; + this.instanceOf = options.instanceOf; + this.predicate = options.predicate; + this.represent = options.represent; + this.defaultStyle = options.defaultStyle; + this.styleAliases = options.styleAliases; + } + } + public resolve: (data?: any) => boolean = () => true; + public construct: (data?: any) => any = data => data; +} diff --git a/yaml/devDeps.ts b/yaml/devDeps.ts new file mode 100644 index 000000000000..15fb2dccf1e2 --- /dev/null +++ b/yaml/devDeps.ts @@ -0,0 +1,3 @@ +import * as asserts from "../testing/asserts.ts"; +import * as testing from "../testing/mod.ts"; +export { testing, asserts }; diff --git a/yaml/dumper/DumperState.ts b/yaml/dumper/DumperState.ts new file mode 100644 index 000000000000..a970a8fdb561 --- /dev/null +++ b/yaml/dumper/DumperState.ts @@ -0,0 +1,111 @@ +import { Schema, SchemaDefinition } from "../Schema.ts"; +import { State } from "../State.ts"; +import { StyleVariant, Type } from "../Type.ts"; +import { ArrayObject } from "../utils.ts"; + +const _hasOwnProperty = Object.prototype.hasOwnProperty; + +function compileStyleMap( + schema: Schema, + map?: ArrayObject | null +): ArrayObject { + if (typeof map === "undefined" || map === null) return {}; + + let type: Type; + const result: ArrayObject = {}; + const keys = Object.keys(map); + let tag: string, style: StyleVariant; + for (let index = 0, length = keys.length; index < length; index += 1) { + tag = keys[index]; + style = String(map[tag]) as StyleVariant; + if (tag.slice(0, 2) === "!!") { + tag = `tag:yaml.org,2002:${tag.slice(2)}`; + } + type = schema.compiledTypeMap.fallback[tag]; + + if (type && _hasOwnProperty.call(type.styleAliases, style)) { + style = type.styleAliases[style]; + } + + result[tag] = style; + } + + return result; +} + +export interface DumperStateOptions { + /** indentation width to use (in spaces). */ + indent?: number; + /** when true, will not add an indentation level to array elements */ + noArrayIndent?: boolean; + /** do not throw on invalid types (like function in the safe schema) and skip pairs and single values with such types. */ + skipInvalid?: boolean; + /** specifies level of nesting, when to switch from block to flow style for collections. -1 means block style everwhere */ + flowLevel?: number; + /** Each tag may have own set of styles. - "tag" => "style" map. */ + styles?: ArrayObject | null; + /** specifies a schema to use. */ + schema?: SchemaDefinition; + /** if true, sort keys when dumping YAML. If a function, use the function to sort the keys. (default: false) */ + sortKeys?: boolean | ((a: any, b: any) => number); + /** set max line width. (default: 80) */ + lineWidth?: number; + /** if true, don't convert duplicate objects into references (default: false) */ + noRefs?: boolean; + /** if true don't try to be compatible with older yaml versions. Currently: don't quote "yes", "no" and so on, as required for YAML 1.1 (default: false) */ + noCompatMode?: boolean; + /** + * if true flow sequences will be condensed, omitting the space between `key: value` or `a, b`. Eg. `'[a,b]'` or `{a:{b:c}}`. + * Can be useful when using yaml for pretty URL query params as spaces are %-encoded. (default: false). + */ + condenseFlow?: boolean; +} + +export class DumperState extends State { + public indent: number; + public noArrayIndent: boolean; + public skipInvalid: boolean; + public flowLevel: number; + public sortKeys: boolean | ((a: any, b: any) => number); + public lineWidth: number; + public noRefs: boolean; + public noCompatMode: boolean; + public condenseFlow: boolean; + public implicitTypes: Type[]; + public explicitTypes: Type[]; + public tag: string | null = null; + public result: string = ""; + public duplicates = []; + public usedDuplicates = null; + public styleMap: ArrayObject; + public dump: any; + + constructor({ + schema, + indent = 2, + noArrayIndent = false, + skipInvalid = false, + flowLevel = -1, + styles = null, + sortKeys = false, + lineWidth = 80, + noRefs = false, + noCompatMode = false, + condenseFlow = false + }: DumperStateOptions) { + super(schema); + this.indent = Math.max(1, indent); + this.noArrayIndent = noArrayIndent; + this.skipInvalid = skipInvalid; + this.flowLevel = flowLevel; + this.styleMap = compileStyleMap(this.schema as Schema, styles); + this.sortKeys = sortKeys; + this.lineWidth = lineWidth; + this.noRefs = noRefs; + this.noCompatMode = noCompatMode; + this.condenseFlow = condenseFlow; + + this.implicitTypes = (this.schema as Schema).compiledImplicit; + this.explicitTypes = (this.schema as Schema).compiledExplicit; + } +} diff --git a/yaml/dumper/dumper.ts b/yaml/dumper/dumper.ts new file mode 100644 index 000000000000..97bf7e5ac418 --- /dev/null +++ b/yaml/dumper/dumper.ts @@ -0,0 +1,868 @@ +import { YAMLError } from "../error/YAMLError.ts"; +import { DEFAULT_SAFE_SCHEMA } from "../schema/mod.ts"; +import { RepresentFn, StyleVariant, Type } from "../Type.ts"; +import * as common from "../utils.ts"; +import { DumperState, DumperStateOptions } from "./DumperState.ts"; + +const _toString = Object.prototype.toString; +const _hasOwnProperty = Object.prototype.hasOwnProperty; + +const CHAR_TAB = 0x09; /* Tab */ +const CHAR_LINE_FEED = 0x0a; /* LF */ +const CHAR_SPACE = 0x20; /* Space */ +const CHAR_EXCLAMATION = 0x21; /* ! */ +const CHAR_DOUBLE_QUOTE = 0x22; /* " */ +const CHAR_SHARP = 0x23; /* # */ +const CHAR_PERCENT = 0x25; /* % */ +const CHAR_AMPERSAND = 0x26; /* & */ +const CHAR_SINGLE_QUOTE = 0x27; /* ' */ +const CHAR_ASTERISK = 0x2a; /* * */ +const CHAR_COMMA = 0x2c; /* , */ +const CHAR_MINUS = 0x2d; /* - */ +const CHAR_COLON = 0x3a; /* : */ +const CHAR_GREATER_THAN = 0x3e; /* > */ +const CHAR_QUESTION = 0x3f; /* ? */ +const CHAR_COMMERCIAL_AT = 0x40; /* @ */ +const CHAR_LEFT_SQUARE_BRACKET = 0x5b; /* [ */ +const CHAR_RIGHT_SQUARE_BRACKET = 0x5d; /* ] */ +const CHAR_GRAVE_ACCENT = 0x60; /* ` */ +const CHAR_LEFT_CURLY_BRACKET = 0x7b; /* { */ +const CHAR_VERTICAL_LINE = 0x7c; /* | */ +const CHAR_RIGHT_CURLY_BRACKET = 0x7d; /* } */ + +const ESCAPE_SEQUENCES: { [char: number]: string } = {}; + +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"; + +const DEPRECATED_BOOLEANS_SYNTAX = [ + "y", + "Y", + "yes", + "Yes", + "YES", + "on", + "On", + "ON", + "n", + "N", + "no", + "No", + "NO", + "off", + "Off", + "OFF" +]; + +function encodeHex(character: number): string { + const string = character.toString(16).toUpperCase(); + + let handle: string; + let length: number; + 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 YAMLError( + "code point within a string may not be greater than 0xFFFFFFFF" + ); + } + + return `\\${handle}${common.repeat("0", length - string.length)}${string}`; +} + +// Indents every line in a string. Empty lines (\n only) are not indented. +function indentString(string: string, spaces: number) { + const ind = common.repeat(" ", spaces), + length = string.length; + let position = 0, + next = -1, + result = "", + line: string; + + 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: DumperState, level: number) { + return `\n${common.repeat(" ", state.indent * level)}`; +} + +function testImplicitResolving(state: DumperState, str: string) { + let type: Type; + for ( + let index = 0, length = state.implicitTypes.length; + index < length; + index += 1 + ) { + type = state.implicitTypes[index]; + + if (type.resolve(str)) { + return true; + } + } + + return false; +} + +// [33] s-white ::= s-space | s-tab +function isWhitespace(c: number) { + return c === CHAR_SPACE || c === CHAR_TAB; +} + +// Returns true if the character can be printed without escaping. +// From YAML 1.2: "any allowed characters known to be non-printable +// should also be escaped. [However,] This isn’t mandatory" +// Derived from nb-char - \t - #x85 - #xA0 - #x2028 - #x2029. +function isPrintable(c: number) { + return ( + (0x00020 <= c && c <= 0x00007e) || + (0x000a1 <= c && c <= 0x00d7ff && c !== 0x2028 && c !== 0x2029) || + (0x0e000 <= c && c <= 0x00fffd && c !== 0xfeff) /* BOM */ || + (0x10000 <= c && c <= 0x10ffff) + ); +} + +// Simplified test for values allowed after the first character in plain style. +function isPlainSafe(c: number) { + // Uses a subset of nb-char - c-flow-indicator - ":" - "#" + // where nb-char ::= c-printable - b-char - c-byte-order-mark. + return ( + isPrintable(c) && + c !== 0xfeff && + // - c-flow-indicator + c !== CHAR_COMMA && + c !== CHAR_LEFT_SQUARE_BRACKET && + c !== CHAR_RIGHT_SQUARE_BRACKET && + c !== CHAR_LEFT_CURLY_BRACKET && + c !== CHAR_RIGHT_CURLY_BRACKET && + // - ":" - "#" + c !== CHAR_COLON && + c !== CHAR_SHARP + ); +} + +// Simplified test for values allowed as the first character in plain style. +function isPlainSafeFirst(c: number) { + // Uses a subset of ns-char - c-indicator + // where ns-char = nb-char - s-white. + return ( + isPrintable(c) && + c !== 0xfeff && + !isWhitespace(c) && // - s-white + // - (c-indicator ::= + // “-” | “?” | “:” | “,” | “[” | “]” | “{” | “}” + c !== CHAR_MINUS && + c !== CHAR_QUESTION && + c !== CHAR_COLON && + c !== CHAR_COMMA && + c !== CHAR_LEFT_SQUARE_BRACKET && + c !== CHAR_RIGHT_SQUARE_BRACKET && + c !== CHAR_LEFT_CURLY_BRACKET && + c !== CHAR_RIGHT_CURLY_BRACKET && + // | “#” | “&” | “*” | “!” | “|” | “>” | “'” | “"” + c !== CHAR_SHARP && + c !== CHAR_AMPERSAND && + c !== CHAR_ASTERISK && + c !== CHAR_EXCLAMATION && + c !== CHAR_VERTICAL_LINE && + c !== CHAR_GREATER_THAN && + c !== CHAR_SINGLE_QUOTE && + c !== CHAR_DOUBLE_QUOTE && + // | “%” | “@” | “`”) + c !== CHAR_PERCENT && + c !== CHAR_COMMERCIAL_AT && + c !== CHAR_GRAVE_ACCENT + ); +} + +// Determines whether block indentation indicator is required. +function needIndentIndicator(string: string) { + const leadingSpaceRe = /^\n* /; + return leadingSpaceRe.test(string); +} + +const STYLE_PLAIN = 1, + STYLE_SINGLE = 2, + STYLE_LITERAL = 3, + STYLE_FOLDED = 4, + STYLE_DOUBLE = 5; + +// Determines which scalar styles are possible and returns the preferred style. +// lineWidth = -1 => no limit. +// Pre-conditions: str.length > 0. +// Post-conditions: +// STYLE_PLAIN or STYLE_SINGLE => no \n are in the string. +// STYLE_LITERAL => no lines are suitable for folding (or lineWidth is -1). +// STYLE_FOLDED => a line > lineWidth and can be folded (and lineWidth != -1). +function chooseScalarStyle( + string: string, + singleLineOnly: boolean, + indentPerLevel: number, + lineWidth: number, + testAmbiguousType: (...args: any[]) => any +) { + const shouldTrackWidth = lineWidth !== -1; + let hasLineBreak = false, + hasFoldableLine = false, // only checked if shouldTrackWidth + previousLineBreak = -1, // count the first line correctly + plain = + isPlainSafeFirst(string.charCodeAt(0)) && + !isWhitespace(string.charCodeAt(string.length - 1)); + + let char: number, i: number; + if (singleLineOnly) { + // Case: no block styles. + // Check for disallowed characters to rule out plain and single. + for (i = 0; i < string.length; i++) { + char = string.charCodeAt(i); + if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char); + } + } else { + // Case: block styles permitted. + for (i = 0; i < string.length; i++) { + char = string.charCodeAt(i); + if (char === CHAR_LINE_FEED) { + hasLineBreak = true; + // Check if any line can be folded. + if (shouldTrackWidth) { + hasFoldableLine = + hasFoldableLine || + // Foldable line = too long, and not more-indented. + (i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== " "); + previousLineBreak = i; + } + } else if (!isPrintable(char)) { + return STYLE_DOUBLE; + } + plain = plain && isPlainSafe(char); + } + // in case the end is missing a \n + hasFoldableLine = + hasFoldableLine || + (shouldTrackWidth && + (i - previousLineBreak - 1 > lineWidth && + string[previousLineBreak + 1] !== " ")); + } + // Although every style can represent \n without escaping, prefer block styles + // for multiline, since they're more readable and they don't add empty lines. + // Also prefer folding a super-long line. + if (!hasLineBreak && !hasFoldableLine) { + // Strings interpretable as another type have to be quoted; + // e.g. the string 'true' vs. the boolean true. + return plain && !testAmbiguousType(string) ? STYLE_PLAIN : STYLE_SINGLE; + } + // Edge case: block indentation indicator can only have one digit. + if (indentPerLevel > 9 && needIndentIndicator(string)) { + return STYLE_DOUBLE; + } + // At this point we know block styles are valid. + // Prefer literal style unless we want to fold. + return hasFoldableLine ? STYLE_FOLDED : STYLE_LITERAL; +} + +// Note: line breaking/folding is implemented for only the folded style. +// NB. We drop the last trailing newline (if any) of a returned block scalar +// since the dumper adds its own newline. This always works: +// • No ending newline => unaffected; already using strip "-" chomping. +// • Ending newline => removed then restored. +// Importantly, this keeps the "+" chomp indicator from gaining an extra line. +function writeScalar( + state: DumperState, + string: string, + level: number, + iskey: boolean +) { + state.dump = (() => { + if (string.length === 0) { + return "''"; + } + if ( + !state.noCompatMode && + DEPRECATED_BOOLEANS_SYNTAX.indexOf(string) !== -1 + ) { + return `'${string}'`; + } + + const indent = state.indent * Math.max(1, level); // no 0-indent scalars + // As indentation gets deeper, let the width decrease monotonically + // to the lower bound min(state.lineWidth, 40). + // Note that this implies + // state.lineWidth ≤ 40 + state.indent: width is fixed at the lower bound. + // state.lineWidth > 40 + state.indent: width decreases until the lower bound. + // This behaves better than a constant minimum width which disallows narrower options, + // or an indent threshold which causes the width to suddenly increase. + const lineWidth = + state.lineWidth === -1 + ? -1 + : Math.max(Math.min(state.lineWidth, 40), state.lineWidth - indent); + + // Without knowing if keys are implicit/explicit, assume implicit for safety. + const singleLineOnly = + iskey || + // No block styles in flow mode. + (state.flowLevel > -1 && level >= state.flowLevel); + function testAmbiguity(str: string) { + return testImplicitResolving(state, str); + } + + switch ( + chooseScalarStyle( + string, + singleLineOnly, + state.indent, + lineWidth, + testAmbiguity + ) + ) { + case STYLE_PLAIN: + return string; + case STYLE_SINGLE: + return `'${string.replace(/'/g, "''")}'`; + case STYLE_LITERAL: + return `|${blockHeader(string, state.indent)}${dropEndingNewline( + indentString(string, indent) + )}`; + case STYLE_FOLDED: + return `>${blockHeader(string, state.indent)}${dropEndingNewline( + indentString(foldString(string, lineWidth), indent) + )}`; + case STYLE_DOUBLE: + return `"${escapeString(string)}"`; + default: + throw new YAMLError("impossible error: invalid scalar style"); + } + })(); +} + +// Pre-conditions: string is valid for a block scalar, 1 <= indentPerLevel <= 9. +function blockHeader(string: string, indentPerLevel: number) { + const indentIndicator = needIndentIndicator(string) + ? String(indentPerLevel) + : ""; + + // note the special case: the string '\n' counts as a "trailing" empty line. + const clip = string[string.length - 1] === "\n"; + const keep = clip && (string[string.length - 2] === "\n" || string === "\n"); + const chomp = keep ? "+" : clip ? "" : "-"; + + return `${indentIndicator}${chomp}\n`; +} + +// (See the note for writeScalar.) +function dropEndingNewline(string: string) { + return string[string.length - 1] === "\n" ? string.slice(0, -1) : string; +} + +// Note: a long line without a suitable break point will exceed the width limit. +// Pre-conditions: every char in str isPrintable, str.length > 0, width > 0. +function foldString(string: string, width: number) { + // In folded style, $k$ consecutive newlines output as $k+1$ newlines— + // unless they're before or after a more-indented line, or at the very + // beginning or end, in which case $k$ maps to $k$. + // Therefore, parse each chunk as newline(s) followed by a content line. + const lineRe = /(\n+)([^\n]*)/g; + + // first line (possibly an empty line) + let result = (() => { + let nextLF = string.indexOf("\n"); + nextLF = nextLF !== -1 ? nextLF : string.length; + lineRe.lastIndex = nextLF; + return foldLine(string.slice(0, nextLF), width); + })(); + // If we haven't reached the first content line yet, don't add an extra \n. + let prevMoreIndented = string[0] === "\n" || string[0] === " "; + let moreIndented; + + // rest of the lines + let match; + // tslint:disable-next-line:no-conditional-assignment + while ((match = lineRe.exec(string))) { + const prefix = match[1], + line = match[2]; + moreIndented = line[0] === " "; + result += + prefix + + (!prevMoreIndented && !moreIndented && line !== "" ? "\n" : "") + + foldLine(line, width); + prevMoreIndented = moreIndented; + } + + return result; +} + +// Greedy line breaking. +// Picks the longest line under the limit each time, +// otherwise settles for the shortest line over the limit. +// NB. More-indented lines *cannot* be folded, as that would add an extra \n. +function foldLine(line: string, width: number) { + if (line === "" || line[0] === " ") return line; + + // Since a more-indented line adds a \n, breaks can't be followed by a space. + const breakRe = / [^ ]/g; // note: the match index will always be <= length-2. + let match; + // start is an inclusive index. end, curr, and next are exclusive. + let start = 0, + end, + curr = 0, + next = 0; + let result = ""; + + // Invariants: 0 <= start <= length-1. + // 0 <= curr <= next <= max(0, length-2). curr - start <= width. + // Inside the loop: + // A match implies length >= 2, so curr and next are <= length-2. + // tslint:disable-next-line:no-conditional-assignment + while ((match = breakRe.exec(line))) { + next = match.index; + // maintain invariant: curr - start <= width + if (next - start > width) { + end = curr > start ? curr : next; // derive end <= length-2 + result += `\n${line.slice(start, end)}`; + // skip the space that was output as \n + start = end + 1; // derive start <= length-1 + } + curr = next; + } + + // By the invariants, start <= length-1, so there is something left over. + // It is either the whole string or a part starting from non-whitespace. + result += "\n"; + // Insert a break if the remainder is too long and there is a break available. + if (line.length - start > width && curr > start) { + result += `${line.slice(start, curr)}\n${line.slice(curr + 1)}`; + } else { + result += line.slice(start); + } + + return result.slice(1); // drop extra \n joiner +} + +// Escapes a double-quoted string. +function escapeString(string: string) { + let result = ""; + let char, nextChar; + let escapeSeq; + + for (let i = 0; i < string.length; i++) { + char = string.charCodeAt(i); + // Check for surrogate pairs (reference Unicode 3.0 section "3.7 Surrogates"). + if (char >= 0xd800 && char <= 0xdbff /* high surrogate */) { + nextChar = string.charCodeAt(i + 1); + if (nextChar >= 0xdc00 && nextChar <= 0xdfff /* low surrogate */) { + // Combine the surrogate pair and store it escaped. + result += encodeHex( + (char - 0xd800) * 0x400 + nextChar - 0xdc00 + 0x10000 + ); + // Advance index one extra since we already used that char here. + i++; + continue; + } + } + escapeSeq = ESCAPE_SEQUENCES[char]; + result += + !escapeSeq && isPrintable(char) + ? string[i] + : escapeSeq || encodeHex(char); + } + + return result; +} + +function writeFlowSequence(state: DumperState, level: number, object: any) { + let _result = ""; + const _tag = state.tag; + + for (let index = 0, length = object.length; index < length; index += 1) { + // Write only valid elements. + if (writeNode(state, level, object[index], false, false)) { + if (index !== 0) _result += `,${!state.condenseFlow ? " " : ""}`; + _result += state.dump; + } + } + + state.tag = _tag; + state.dump = `[${_result}]`; +} + +function writeBlockSequence( + state: DumperState, + level: number, + object: any, + compact = false +) { + let _result = ""; + const _tag = state.tag; + + for (let index = 0, length = object.length; index < length; index += 1) { + // Write only valid elements. + if (writeNode(state, level + 1, object[index], true, true)) { + if (!compact || index !== 0) { + _result += generateNextLine(state, level); + } + + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + _result += "-"; + } else { + _result += "- "; + } + + _result += state.dump; + } + } + + state.tag = _tag; + state.dump = _result || "[]"; // Empty sequence if no valid values. +} + +function writeFlowMapping(state: DumperState, level: number, object: any) { + let _result = ""; + const _tag = state.tag, + objectKeyList = Object.keys(object); + + let pairBuffer: string, objectKey: string, objectValue: any; + for ( + let index = 0, length = objectKeyList.length; + index < length; + index += 1 + ) { + pairBuffer = state.condenseFlow ? '"' : ""; + + if (index !== 0) 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}${state.condenseFlow ? '"' : ""}:${ + state.condenseFlow ? "" : " " + }`; + + 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: DumperState, + level: number, + object: object, + compact = false +) { + const _tag = state.tag, + objectKeyList = Object.keys(object); + let _result = ""; + + // Allow sorting keys so that the output file is deterministic + if (state.sortKeys === true) { + // Default sorting + objectKeyList.sort(); + } else if (typeof state.sortKeys === "function") { + // Custom sort function + objectKeyList.sort(state.sortKeys); + } else if (state.sortKeys) { + // Something is wrong + throw new YAMLError("sortKeys must be a boolean or a function"); + } + + let pairBuffer = "", + objectKey: string, + objectValue: any, + explicitPair: boolean; + for ( + let index = 0, length = objectKeyList.length; + index < length; + index += 1 + ) { + pairBuffer = ""; + + if (!compact || index !== 0) { + pairBuffer += generateNextLine(state, level); + } + + objectKey = objectKeyList[index]; + objectValue = object[objectKey]; + + if (!writeNode(state, level + 1, objectKey, true, true, true)) { + continue; // Skip this pair because of invalid key. + } + + explicitPair = + (state.tag !== null && state.tag !== "?") || + (state.dump && state.dump.length > 1024); + + if (explicitPair) { + if (state.dump && CHAR_LINE_FEED === state.dump.charCodeAt(0)) { + pairBuffer += "?"; + } else { + pairBuffer += "? "; + } + } + + pairBuffer += state.dump; + + if (explicitPair) { + pairBuffer += generateNextLine(state, level); + } + + if (!writeNode(state, level + 1, objectValue, true, explicitPair)) { + continue; // 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: DumperState, object: any, explicit = false) { + const typeList = explicit ? state.explicitTypes : state.implicitTypes; + + let type: Type; + let style: StyleVariant; + let _result: string; + for (let index = 0, length = typeList.length; index < length; index += 1) { + type = typeList[index]; + + if ( + (type.instanceOf || type.predicate) && + (!type.instanceOf || + (typeof object === "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 (_toString.call(type.represent) === "[object Function]") { + _result = (type.represent as RepresentFn)(object, style); + } else if (_hasOwnProperty.call(type.represent, style)) { + _result = type.represent[style](object, style); + } else { + throw new YAMLError( + `!<${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: DumperState, + level: number, + object: any, + block: boolean, + compact: boolean, + iskey = false +) { + state.tag = null; + state.dump = object; + + if (!detectType(state, object, false)) { + detectType(state, object, true); + } + + const type = _toString.call(state.dump); + + if (block) { + block = state.flowLevel < 0 || state.flowLevel > level; + } + + const objectOrArray = type === "[object Object]" || type === "[object Array]"; + + let duplicateIndex: number; + let duplicate: boolean; + if (objectOrArray) { + duplicateIndex = state.duplicates.indexOf(object); + duplicate = duplicateIndex !== -1; + } + + if ( + (state.tag !== null && state.tag !== "?") || + duplicate || + (state.indent !== 2 && level > 0) + ) { + compact = false; + } + + if (duplicate && state.usedDuplicates[duplicateIndex]) { + state.dump = `*ref_${duplicateIndex}`; + } else { + if (objectOrArray && duplicate && !state.usedDuplicates[duplicateIndex]) { + state.usedDuplicates[duplicateIndex] = true; + } + if (type === "[object Object]") { + if (block && Object.keys(state.dump).length !== 0) { + writeBlockMapping(state, level, state.dump, compact); + if (duplicate) { + state.dump = `&ref_${duplicateIndex}${state.dump}`; + } + } else { + writeFlowMapping(state, level, state.dump); + if (duplicate) { + state.dump = `&ref_${duplicateIndex} ${state.dump}`; + } + } + } else if (type === "[object Array]") { + const arrayLevel = state.noArrayIndent && level > 0 ? level - 1 : level; + if (block && state.dump.length !== 0) { + writeBlockSequence(state, arrayLevel, state.dump, compact); + if (duplicate) { + state.dump = `&ref_${duplicateIndex}${state.dump}`; + } + } else { + writeFlowSequence(state, arrayLevel, state.dump); + if (duplicate) { + state.dump = `&ref_${duplicateIndex} ${state.dump}`; + } + } + } else if (type === "[object String]") { + if (state.tag !== "?") { + writeScalar(state, state.dump, level, iskey); + } + } else { + if (state.skipInvalid) return false; + throw new YAMLError(`unacceptable kind of an object to dump ${type}`); + } + + if (state.tag !== null && state.tag !== "?") { + state.dump = `!<${state.tag}> ${state.dump}`; + } + } + + return true; +} + +function getDuplicateReferences(object: object, state: DumperState) { + const objects = [], + duplicatesIndexes = []; + + inspectNode(object, objects, duplicatesIndexes); + + const length = duplicatesIndexes.length; + for (let index = 0; index < length; index += 1) { + state.duplicates.push(objects[duplicatesIndexes[index]]); + } + state.usedDuplicates = new Array(length); +} + +function inspectNode( + object: object, + objects: object[], + duplicatesIndexes: number[] +) { + if (object !== null && typeof object === "object") { + const index = objects.indexOf(object); + if (index !== -1) { + if (duplicatesIndexes.indexOf(index) === -1) { + duplicatesIndexes.push(index); + } + } else { + objects.push(object); + + if (Array.isArray(object)) { + for (let idx = 0, length = object.length; idx < length; idx += 1) { + inspectNode(object[idx], objects, duplicatesIndexes); + } + } else { + const objectKeyList = Object.keys(object); + + for ( + let idx = 0, length = objectKeyList.length; + idx < length; + idx += 1 + ) { + inspectNode(object[objectKeyList[idx]], objects, duplicatesIndexes); + } + } + } + } +} + +export function dump(input: any, options?: DumperStateOptions) { + options = options || {}; + + const state = new DumperState(options); + + if (!state.noRefs) getDuplicateReferences(input, state); + + if (writeNode(state, 0, input, true, true)) return `${state.dump}\n`; + + return ""; +} + +export function safeDump(input: object, options?: DumperStateOptions) { + return dump(input, { schema: DEFAULT_SAFE_SCHEMA, ...options }); +} diff --git a/yaml/error/YAMLError.ts b/yaml/error/YAMLError.ts new file mode 100644 index 000000000000..1fa4036905f7 --- /dev/null +++ b/yaml/error/YAMLError.ts @@ -0,0 +1,17 @@ +import { Mark } from "../Mark.ts"; + +const { DenoError, ErrorKind } = Deno; + +export class YAMLError extends DenoError { + constructor( + message = "(unknown reason)", + protected mark: Mark | string = "" + ) { + super(ErrorKind.Other, `${message} ${mark}`); + this.name = this.constructor.name; + } + + public toString(compact: boolean) { + return `${this.name}: ${this.message} ${this.mark}`; + } +} diff --git a/yaml/example/dump.ts b/yaml/example/dump.ts new file mode 100644 index 000000000000..fbe4425521f2 --- /dev/null +++ b/yaml/example/dump.ts @@ -0,0 +1,21 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { stringify } from "../mod.ts"; + +console.log( + stringify({ + foo: { + bar: true, + test: [ + "a", + "b", + { + a: false + }, + { + a: false + } + ] + }, + test: "foobar" + }) +); diff --git a/yaml/example/inout.ts b/yaml/example/inout.ts new file mode 100644 index 000000000000..70bf3022263d --- /dev/null +++ b/yaml/example/inout.ts @@ -0,0 +1,25 @@ +import { parse, stringify } from "../mod.ts"; + +const test = { + foo: { + bar: true, + test: [ + "a", + "b", + { + a: false + }, + { + a: false + } + ] + }, + test: "foobar" +}; + +const string = stringify(test); +if (Deno.inspect(test) === Deno.inspect(parse(string))) { + console.log("In-Out as expected."); +} else { + console.log("Someting went wrong."); +} diff --git a/yaml/example/parse.ts b/yaml/example/parse.ts new file mode 100644 index 000000000000..8dd1cc08e25b --- /dev/null +++ b/yaml/example/parse.ts @@ -0,0 +1,17 @@ +import { parse } from "../mod.ts"; + +const result = parse(` +test: toto +foo: + bar: True + baz: 1 + qux: ~ +`); +console.log(result); + +const expected = '{ test: "toto", foo: { bar: true, baz: 1, qux: null } }'; +if (Deno.inspect(result) === expected) { + console.log("Output is as expected."); +} else { + console.error("Error during parse. Output is not as expect.", expected); +} diff --git a/yaml/example/sample_document.ts b/yaml/example/sample_document.ts new file mode 100644 index 000000000000..685308361ec1 --- /dev/null +++ b/yaml/example/sample_document.ts @@ -0,0 +1,20 @@ +import { parse } from "../mod.ts"; + +const { readFileSync, cwd } = Deno; + +(async () => { + const yml = readFileSync(`${cwd()}/example/sample_document.yml`); + + const document = new TextDecoder().decode(yml); + const obj = parse(document); + console.log(obj); + + let i = 0; + for (const o of Object.values(obj)) { + console.log(`======${i}`); + for (const [key, value] of Object.entries(o)) { + console.log(key, value); + } + i++; + } +})(); diff --git a/yaml/example/sample_document.yml b/yaml/example/sample_document.yml new file mode 100644 index 000000000000..1f3c2eb3e4d6 --- /dev/null +++ b/yaml/example/sample_document.yml @@ -0,0 +1,197 @@ +--- +# Collection Types ############################################################# +################################################################################ + +# http://yaml.org/type/map.html -----------------------------------------------# + +map: + # Unordered set of key: value pairs. + Block style: !!map + Clark : Evans + Ingy : döt Net + Oren : Ben-Kiki + Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki } + +# http://yaml.org/type/omap.html ----------------------------------------------# + +omap: + # Explicitly typed ordered map (dictionary). + Bestiary: !!omap + - aardvark: African pig-like ant eater. Ugly. + - anteater: South-American ant eater. Two species. + - anaconda: South-American constrictor snake. Scaly. + # Etc. + # Flow style + Numbers: !!omap [ one: 1, two: 2, three : 3 ] + +# http://yaml.org/type/pairs.html ---------------------------------------------# + +pairs: + # Explicitly typed pairs. + Block tasks: !!pairs + - meeting: with team. + - meeting: with boss. + - break: lunch. + - meeting: with client. + Flow tasks: !!pairs [ meeting: with team, meeting: with boss ] + +# http://yaml.org/type/set.html -----------------------------------------------# + +set: + # Explicitly typed set. + baseball players: !!set + ? Mark McGwire + ? Sammy Sosa + ? Ken Griffey + # Flow style + baseball teams: !!set { Boston Red Sox, Detroit Tigers, New York Yankees } + +# http://yaml.org/type/seq.html -----------------------------------------------# + +seq: + # Ordered sequence of nodes + Block style: !!seq + - Mercury # Rotates - no light/dark sides. + - Venus # Deadliest. Aptly named. + - Earth # Mostly dirt. + - Mars # Seems empty. + - Jupiter # The king. + - Saturn # Pretty. + - Uranus # Where the sun hardly shines. + - Neptune # Boring. No rings. + - Pluto # You call this a planet? + Flow style: !!seq [ Mercury, Venus, Earth, Mars, # Rocks + Jupiter, Saturn, Uranus, Neptune, # Gas + Pluto ] # Overrated + + +# Scalar Types ################################################################# +################################################################################ + +# http://yaml.org/type/binary.html --------------------------------------------# + +binary: + canonical: !!binary "\ + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\ + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\ + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" + generic: !!binary | + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5 + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs= + description: + The binary value above is a tiny arrow encoded as a gif image. + +# http://yaml.org/type/bool.html ----------------------------------------------# + +bool: + - true + - True + - TRUE + - false + - False + - FALSE + +# http://yaml.org/type/float.html ---------------------------------------------# + +float: + canonical: 6.8523015e+5 + exponential: 685.230_15e+03 + fixed: 685_230.15 + sexagesimal: 190:20:30.15 + negative infinity: -.inf + not a number: .NaN + +# http://yaml.org/type/int.html -----------------------------------------------# + +int: + canonical: 685230 + decimal: +685_230 + octal: 02472256 + hexadecimal: 0x_0A_74_AE + binary: 0b1010_0111_0100_1010_1110 + sexagesimal: 190:20:30 + +# http://yaml.org/type/merge.html ---------------------------------------------# + +merge: + - &CENTER { x: 1, y: 2 } + - &LEFT { x: 0, y: 2 } + - &BIG { r: 10 } + - &SMALL { r: 1 } + + # All the following maps are equal: + + - # Explicit keys + x: 1 + y: 2 + r: 10 + label: nothing + + - # Merge one map + << : *CENTER + r: 10 + label: center + + - # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + + - # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: big/left/small + +# http://yaml.org/type/null.html ----------------------------------------------# + +null: + # This mapping has four keys, + # one has a value. + empty: + canonical: ~ + english: null + ~: null key + # This sequence has five + # entries, two have values. + sparse: + - ~ + - 2nd entry + - + - 4th entry + - Null + +# http://yaml.org/type/str.html -----------------------------------------------# + +string: abcd + +# http://yaml.org/type/timestamp.html -----------------------------------------# + +timestamp: + canonical: 2001-12-15T02:59:43.1Z + valid iso8601: 2001-12-14t21:59:43.10-05:00 + space separated: 2001-12-14 21:59:43.10 -5 + no time zone (Z): 2001-12-15 2:59:43.10 + date (00:00:00Z): 2002-12-14 + + +# JavaScript Specific Types #################################################### +################################################################################ + +# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp + +# regexp: +# simple: !!js/regexp foobar +# modifiers: !!js/regexp /foobar/mi + +# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/undefined + +# undefined: !!js/undefined ~ + +# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function + +# function: !!js/function > +# function foobar() { +# return 'Wow! JS-YAML Rocks!'; +# } diff --git a/yaml/loader/LoaderState.ts b/yaml/loader/LoaderState.ts new file mode 100644 index 000000000000..4d3b7f046428 --- /dev/null +++ b/yaml/loader/LoaderState.ts @@ -0,0 +1,68 @@ +import { YAMLError } from "../error/YAMLError.ts"; +import { Schema, SchemaDefinition, TypeMap } from "../Schema.ts"; +import { State } from "../State.ts"; +import { Type } from "../Type.ts"; + +export interface LoaderStateOptions { + legacy?: boolean; + listener?: () => any; + /** string to be used as a file path in error/warning messages. */ + filename?: string; + /** specifies a schema to use. */ + schema?: SchemaDefinition; + /** compatibility with JSON.parse behaviour. */ + json?: boolean; + /** function to call on warning messages. */ + onWarning?(this: null, e?: YAMLError): void; +} + +export type ResultType = [] | {} | string; + +export class LoaderState extends State { + public documents = []; + public length: number; + public lineIndent = 0; + public lineStart = 0; + public position = 0; + public line = 0; + public filename?: string; + public onWarning?: () => any; + public legacy: boolean; + public json: boolean; + public listener?: (...args: any[]) => any; + public implicitTypes: Type[]; + public typeMap: TypeMap; + + public version; + public checkLineBreaks; + public tagMap; + public anchorMap; + public tag; + public anchor; + public kind; + public result: ResultType = ""; + + constructor( + public input: string, + { + filename, + schema, + onWarning, + legacy = false, + json = false, + listener = null + }: LoaderStateOptions + ) { + super(schema); + this.filename = filename; + this.onWarning = onWarning; + this.legacy = legacy; + this.json = json; + this.listener = listener; + + this.implicitTypes = (this.schema as Schema).compiledImplicit; + this.typeMap = (this.schema as Schema).compiledTypeMap; + + this.length = input.length; + } +} diff --git a/yaml/loader/loader.ts b/yaml/loader/loader.ts new file mode 100644 index 000000000000..d31b6d2cbf0e --- /dev/null +++ b/yaml/loader/loader.ts @@ -0,0 +1,1757 @@ +// tslint:disable:no-conditional-assignment +import { YAMLError } from "../error/YAMLError.ts"; +import { Mark } from "../Mark.ts"; +import { DEFAULT_SAFE_SCHEMA } from "../schema/mod.ts"; +import { Type } from "../Type.ts"; +import * as common from "../utils.ts"; +import { LoaderState, LoaderStateOptions, ResultType } from "./LoaderState.ts"; + +type ArrayObject = common.ArrayObject; + +const _hasOwnProperty = Object.prototype.hasOwnProperty; + +const CONTEXT_FLOW_IN = 1; +const CONTEXT_FLOW_OUT = 2; +const CONTEXT_BLOCK_IN = 3; +const CONTEXT_BLOCK_OUT = 4; + +const CHOMPING_CLIP = 1; +const CHOMPING_STRIP = 2; +const CHOMPING_KEEP = 3; + +const PATTERN_NON_PRINTABLE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; +const PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/; +const PATTERN_FLOW_INDICATORS = /[,\[\]\{\}]/; +const PATTERN_TAG_HANDLE = /^(?:!|!!|![a-z\-]+!)$/i; +const PATTERN_TAG_URI = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; + +function _class(obj: any) { + return Object.prototype.toString.call(obj); +} + +function is_EOL(c: number) { + return c === 0x0a /* LF */ || c === 0x0d /* CR */; +} + +function is_WHITE_SPACE(c: number) { + return c === 0x09 /* Tab */ || c === 0x20 /* Space */; +} + +function is_WS_OR_EOL(c: number) { + return ( + c === 0x09 /* Tab */ || + c === 0x20 /* Space */ || + c === 0x0a /* LF */ || + c === 0x0d /* CR */ + ); +} + +function is_FLOW_INDICATOR(c: number) { + return ( + c === 0x2c /* , */ || + c === 0x5b /* [ */ || + c === 0x5d /* ] */ || + c === 0x7b /* { */ || + c === 0x7d /* } */ + ); +} + +function fromHexCode(c: number) { + if (0x30 /* 0 */ <= c && c <= 0x39 /* 9 */) { + return c - 0x30; + } + + const lc = c | 0x20; + + if (0x61 /* a */ <= lc && lc <= 0x66 /* f */) { + return lc - 0x61 + 10; + } + + return -1; +} + +function escapedHexLen(c: number) { + if (c === 0x78 /* x */) { + return 2; + } + if (c === 0x75 /* u */) { + return 4; + } + if (c === 0x55 /* U */) { + return 8; + } + return 0; +} + +function fromDecimalCode(c: number) { + if (0x30 /* 0 */ <= c && c <= 0x39 /* 9 */) { + return c - 0x30; + } + + return -1; +} + +function simpleEscapeSequence(c: number) { + /* tslint:disable:prettier */ + 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" + : ""; + /* tslint:enable:prettier */ +} + +function charFromCodepoint(c: number) { + 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 + ); +} + +const simpleEscapeCheck = new Array(256); // integer, for fast access +const simpleEscapeMap = new Array(256); +for (let i = 0; i < 256; i++) { + simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0; + simpleEscapeMap[i] = simpleEscapeSequence(i); +} + +function generateError(state, message) { + return new YAMLError( + message, + new Mark( + state.filename, + state.input, + state.position, + state.line, + state.position - state.lineStart + ) + ); +} + +function throwError(state, message) { + throw generateError(state, message); +} + +function throwWarning(state, message) { + if (state.onWarning) { + state.onWarning.call(null, generateError(state, message)); + } +} + +const directiveHandlers = { + YAML: function handleYamlDirective(state, name, args) { + let match, major, minor; + + if (state.version !== null) { + throwError(state, "duplication of %YAML directive"); + } + + if (args.length !== 1) { + throwError(state, "YAML directive accepts exactly one argument"); + } + + match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); + + if (match === null) { + throwError(state, "ill-formed argument of the YAML directive"); + } + + major = parseInt(match[1], 10); + minor = parseInt(match[2], 10); + + if (major !== 1) { + throwError(state, "unacceptable YAML version of the document"); + } + + state.version = args[0]; + state.checkLineBreaks = minor < 2; + + if (minor !== 1 && minor !== 2) { + throwWarning(state, "unsupported YAML version of the document"); + } + }, + + TAG: function handleTagDirective(state, name, args) { + let handle, prefix; + + if (args.length !== 2) { + throwError(state, "TAG directive accepts exactly two arguments"); + } + + handle = args[0]; + prefix = args[1]; + + if (!PATTERN_TAG_HANDLE.test(handle)) { + throwError( + state, + "ill-formed tag handle (first argument) of the TAG directive" + ); + } + + if (_hasOwnProperty.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: LoaderState, + start: number, + end: number, + checkJson: boolean +) { + let result: string; + if (start < end) { + result = state.input.slice(start, end); + + if (checkJson) { + for ( + let position = 0, length = result.length; + position < length; + position++ + ) { + const character = result.charCodeAt(position); + if ( + !(character === 0x09 || (0x20 <= character && character <= 0x10ffff)) + ) { + throwError(state, "expected valid JSON character"); + } + } + } else if (PATTERN_NON_PRINTABLE.test(result)) { + throwError(state, "the stream contains non-printable characters"); + } + + state.result += result; + } +} + +function mergeMappings( + state: LoaderState, + destination: {}, + source: {}, + overridableKeys: ArrayObject +) { + if (!common.isObject(source)) { + throwError( + state, + "cannot merge mappings; the provided source object is unacceptable" + ); + } + + const keys = Object.keys(source); + for (let i = 0, len = keys.length; i < len; i++) { + const key = keys[i]; + if (!_hasOwnProperty.call(destination, key)) { + destination[key] = source[key]; + overridableKeys[key] = true; + } + } +} + +function storeMappingPair( + state: LoaderState, + result: {}, + overridableKeys: ArrayObject, + keyTag: string, + keyNode: any, + valueNode: {}, + startLine?: number, + startPos?: number +) { + // The output is a plain object here, so keys can only be strings. + // We need to convert keyNode to a string, but doing so can hang the process + // (deeply nested arrays that explode exponentially using aliases). + if (Array.isArray(keyNode)) { + keyNode = Array.prototype.slice.call(keyNode); + + for (let index = 0, quantity = keyNode.length; index < quantity; index++) { + if (Array.isArray(keyNode[index])) { + throwError(state, "nested arrays are not supported inside keys"); + } + + if ( + typeof keyNode === "object" && + _class(keyNode[index]) === "[object Object]" + ) { + keyNode[index] = "[object Object]"; + } + } + } + + // Avoid code execution in load() via toString property + // (still use its own toString for arrays, timestamps, + // and whatever user schema extensions happen to have @@toStringTag) + if (typeof keyNode === "object" && _class(keyNode) === "[object Object]") { + keyNode = "[object Object]"; + } + + keyNode = String(keyNode); + + if (result === null) { + result = {}; + } + + if (keyTag === "tag:yaml.org,2002:merge") { + if (Array.isArray(valueNode)) { + for ( + let index = 0, quantity = valueNode.length; + index < quantity; + index++ + ) { + mergeMappings(state, result, valueNode[index], overridableKeys); + } + } else { + mergeMappings(state, result, valueNode, overridableKeys); + } + } else { + if ( + !state.json && + !_hasOwnProperty.call(overridableKeys, keyNode) && + _hasOwnProperty.call(result, keyNode) + ) { + state.line = startLine || state.line; + state.position = startPos || state.position; + throwError(state, "duplicated mapping key"); + } + result[keyNode] = valueNode; + delete overridableKeys[keyNode]; + } + + return result; +} + +function readLineBreak(state) { + let ch; + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x0a /* LF */) { + state.position++; + } else if (ch === 0x0d /* CR */) { + state.position++; + if (state.input.charCodeAt(state.position) === 0x0a /* LF */) { + state.position++; + } + } else { + throwError(state, "a line break is expected"); + } + + state.line += 1; + state.lineStart = state.position; +} + +function skipSeparationSpace(state, allowComments, checkIndent) { + let lineBreaks = 0, + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (allowComments && ch === 0x23 /* # */) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0x0a /* LF */ && ch !== 0x0d /* CR */ && ch !== 0); + } + + if (is_EOL(ch)) { + readLineBreak(state); + + ch = state.input.charCodeAt(state.position); + lineBreaks++; + state.lineIndent = 0; + + while (ch === 0x20 /* Space */) { + state.lineIndent++; + ch = state.input.charCodeAt(++state.position); + } + } else { + break; + } + } + + if ( + checkIndent !== -1 && + lineBreaks !== 0 && + state.lineIndent < checkIndent + ) { + throwWarning(state, "deficient indentation"); + } + + return lineBreaks; +} + +function testDocumentSeparator(state) { + let _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 ( + (ch === 0x2d /* - */ || ch === 0x2e) /* . */ && + ch === state.input.charCodeAt(_position + 1) && + ch === state.input.charCodeAt(_position + 2) + ) { + _position += 3; + + ch = state.input.charCodeAt(_position); + + if (ch === 0 || is_WS_OR_EOL(ch)) { + return true; + } + } + + return false; +} + +function writeFoldedLines(state, count) { + if (count === 1) { + state.result += " "; + } else if (count > 1) { + state.result += common.repeat("\n", count - 1); + } +} + +function readPlainScalar( + state: LoaderState, + nodeIndent: number, + withinFlowCollection: boolean +) { + const kind = state.kind; + const result = state.result; + let ch = state.input.charCodeAt(state.position); + + if ( + is_WS_OR_EOL(ch) || + is_FLOW_INDICATOR(ch) || + ch === 0x23 /* # */ || + ch === 0x26 /* & */ || + ch === 0x2a /* * */ || + ch === 0x21 /* ! */ || + ch === 0x7c /* | */ || + ch === 0x3e /* > */ || + ch === 0x27 /* ' */ || + ch === 0x22 /* " */ || + ch === 0x25 /* % */ || + ch === 0x40 /* @ */ || + ch === 0x60 /* ` */ + ) { + return false; + } + + let following: number; + if (ch === 0x3f /* ? */ || ch === 0x2d /* - */) { + 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 = ""; + let captureEnd: number, + captureStart = (captureEnd = state.position); + let hasPendingContent = false; + let line = 0; + while (ch !== 0) { + if (ch === 0x3a /* : */) { + following = state.input.charCodeAt(state.position + 1); + + if ( + is_WS_OR_EOL(following) || + (withinFlowCollection && is_FLOW_INDICATOR(following)) + ) { + break; + } + } else if (ch === 0x23 /* # */) { + const 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; + const lineStart = state.lineStart; + const lineIndent = state.lineIndent; + skipSeparationSpace(state, false, -1); + + if (state.lineIndent >= nodeIndent) { + hasPendingContent = true; + ch = state.input.charCodeAt(state.position); + continue; + } else { + state.position = captureEnd; + state.line = line; + state.lineStart = lineStart; + state.lineIndent = lineIndent; + break; + } + } + + if (hasPendingContent) { + captureSegment(state, captureStart, captureEnd, false); + writeFoldedLines(state, state.line - line); + captureStart = captureEnd = state.position; + hasPendingContent = false; + } + + if (!is_WHITE_SPACE(ch)) { + captureEnd = state.position + 1; + } + + ch = state.input.charCodeAt(++state.position); + } + + captureSegment(state, captureStart, captureEnd, false); + + if (!common.isNullOrUndefined(state.result)) { + return true; + } + + state.kind = kind; + state.result = result; + return false; +} + +function readSingleQuotedScalar(state, nodeIndent) { + let ch, captureStart, captureEnd; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x27 /* ' */) { + return false; + } + + state.kind = "scalar"; + state.result = ""; + state.position++; + captureStart = captureEnd = state.position; + + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 0x27 /* ' */) { + captureSegment(state, captureStart, state.position, true); + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x27 /* ' */) { + captureStart = state.position; + state.position++; + captureEnd = state.position; + } else { + return true; + } + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + } else if ( + state.position === state.lineStart && + testDocumentSeparator(state) + ) { + throwError( + state, + "unexpected end of the document within a single quoted scalar" + ); + } else { + state.position++; + captureEnd = state.position; + } + } + + throwError( + state, + "unexpected end of the stream within a single quoted scalar" + ); +} + +function readDoubleQuotedScalar(state: LoaderState, nodeIndent: number) { + let ch = state.input.charCodeAt(state.position); + + if (ch !== 0x22 /* " */) { + return false; + } + + state.kind = "scalar"; + state.result = ""; + state.position++; + let captureEnd: number, + captureStart = (captureEnd = state.position); + let tmp: number; + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + if (ch === 0x22 /* " */) { + captureSegment(state, captureStart, state.position, true); + state.position++; + return true; + } + if (ch === 0x5c /* \ */) { + 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 && simpleEscapeCheck[ch]) { + state.result += simpleEscapeMap[ch]; + state.position++; + } else if ((tmp = escapedHexLen(ch)) > 0) { + let hexLength = tmp; + let hexResult = 0; + + for (; hexLength > 0; hexLength--) { + ch = state.input.charCodeAt(++state.position); + + if ((tmp = fromHexCode(ch)) >= 0) { + hexResult = (hexResult << 4) + tmp; + } else { + throwError(state, "expected hexadecimal character"); + } + } + + state.result += charFromCodepoint(hexResult); + + state.position++; + } else { + throwError(state, "unknown escape sequence"); + } + + captureStart = captureEnd = state.position; + } else if (is_EOL(ch)) { + captureSegment(state, captureStart, captureEnd, true); + writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); + captureStart = captureEnd = state.position; + } else if ( + state.position === state.lineStart && + testDocumentSeparator(state) + ) { + throwError( + state, + "unexpected end of the document within a double quoted scalar" + ); + } else { + state.position++; + captureEnd = state.position; + } + } + + throwError( + state, + "unexpected end of the stream within a double quoted scalar" + ); +} + +function readFlowCollection(state: LoaderState, nodeIndent: number) { + let ch = state.input.charCodeAt(state.position); + let terminator: number; + let isMapping = true; + let result: ResultType = {}; + if (ch === 0x5b /* [ */) { + terminator = 0x5d; /* ] */ + isMapping = false; + result = []; + } else if (ch === 0x7b /* { */) { + terminator = 0x7d; /* } */ + } else { + return false; + } + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = result; + } + + ch = state.input.charCodeAt(++state.position); + + const tag = state.tag, + anchor = state.anchor; + let readNext = true; + let valueNode, + keyNode, + keyTag = (keyNode = valueNode = null), + isExplicitPair: boolean, + isPair = (isExplicitPair = false); + let following = 0, + line = 0; + const overridableKeys: ArrayObject = {}; + while (ch !== 0) { + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if (ch === terminator) { + state.position++; + state.tag = tag; + state.anchor = anchor; + state.kind = isMapping ? "mapping" : "sequence"; + state.result = result; + return true; + } + if (!readNext) { + throwError(state, "missed comma between flow collection entries"); + } + + keyTag = keyNode = valueNode = null; + isPair = isExplicitPair = false; + + if (ch === 0x3f /* ? */) { + 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) && ch === 0x3a /* : */) { + isPair = true; + ch = state.input.charCodeAt(++state.position); + skipSeparationSpace(state, true, nodeIndent); + composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); + valueNode = state.result; + } + + if (isMapping) { + storeMappingPair( + state, + result, + overridableKeys, + keyTag, + keyNode, + valueNode + ); + } else if (isPair) { + (result as Array<{}>).push( + storeMappingPair( + state, + null, + overridableKeys, + keyTag, + keyNode, + valueNode + ) + ); + } else { + (result as ResultType[]).push(keyNode); + } + + skipSeparationSpace(state, true, nodeIndent); + + ch = state.input.charCodeAt(state.position); + + if (ch === 0x2c /* , */) { + readNext = true; + ch = state.input.charCodeAt(++state.position); + } else { + readNext = false; + } + } + + throwError(state, "unexpected end of the stream within a flow collection"); +} + +function readBlockScalar(state, nodeIndent) { + let chomping = CHOMPING_CLIP, + didReadContent = false, + detectedIndent = false, + textIndent = nodeIndent, + emptyLines = 0, + atMoreIndented = false; + + let ch = state.input.charCodeAt(state.position); + + let folding = false; + if (ch === 0x7c /* | */) { + folding = false; + } else if (ch === 0x3e /* > */) { + folding = true; + } else { + return false; + } + + state.kind = "scalar"; + state.result = ""; + + let tmp = 0; + while (ch !== 0) { + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x2b /* + */ || ch === 0x2d /* - */) { + if (CHOMPING_CLIP === chomping) { + chomping = ch === 0x2b /* + */ ? CHOMPING_KEEP : CHOMPING_STRIP; + } else { + throwError(state, "repeat of a chomping mode identifier"); + } + } else if ((tmp = fromDecimalCode(ch)) >= 0) { + if (tmp === 0) { + throwError( + state, + "bad explicit indentation width of a block scalar; it cannot be less than one" + ); + } else if (!detectedIndent) { + textIndent = nodeIndent + tmp - 1; + detectedIndent = true; + } else { + throwError(state, "repeat of an indentation width identifier"); + } + } else { + break; + } + } + + if (is_WHITE_SPACE(ch)) { + do { + ch = state.input.charCodeAt(++state.position); + } while (is_WHITE_SPACE(ch)); + + if (ch === 0x23 /* # */) { + do { + ch = state.input.charCodeAt(++state.position); + } while (!is_EOL(ch) && ch !== 0); + } + } + + while (ch !== 0) { + readLineBreak(state); + state.lineIndent = 0; + + ch = state.input.charCodeAt(state.position); + + while ( + (!detectedIndent || state.lineIndent < textIndent) && + ch === 0x20 /* Space */ + ) { + 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) { + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + } else if (chomping === CHOMPING_CLIP) { + if (didReadContent) { + // i.e. only if the scalar is not empty. + state.result += "\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; + // except for the first content line (cf. Example 8.1) + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + + // End of more-indented block. + } else if (atMoreIndented) { + atMoreIndented = false; + state.result += common.repeat("\n", emptyLines + 1); + + // Just one line break - perceive as the same line. + } else if (emptyLines === 0) { + if (didReadContent) { + // i.e. only if we have already read some scalar content. + state.result += " "; + } + + // Several line breaks - perceive as different lines. + } else { + state.result += common.repeat("\n", emptyLines); + } + + // Literal style: just add exact number of line breaks between content lines. + } else { + // Keep all line breaks except the header line break. + state.result += common.repeat( + "\n", + didReadContent ? 1 + emptyLines : emptyLines + ); + } + + didReadContent = true; + detectedIndent = true; + emptyLines = 0; + const captureStart = state.position; + + while (!is_EOL(ch) && ch !== 0) { + ch = state.input.charCodeAt(++state.position); + } + + captureSegment(state, captureStart, state.position, false); + } + + return true; +} + +function readBlockSequence(state, nodeIndent) { + let line: number, + following: number, + detected = false, + ch: number; + const tag = state.tag, + anchor = state.anchor, + result = []; + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = result; + } + + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + if (ch !== 0x2d /* - */) { + break; + } + + following = state.input.charCodeAt(state.position + 1); + + if (!is_WS_OR_EOL(following)) { + break; + } + + detected = true; + state.position++; + + if (skipSeparationSpace(state, true, -1)) { + if (state.lineIndent <= nodeIndent) { + result.push(null); + ch = state.input.charCodeAt(state.position); + continue; + } + } + + line = state.line; + composeNode(state, nodeIndent, CONTEXT_BLOCK_IN, false, true); + result.push(state.result); + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if ((state.line === line || state.lineIndent > nodeIndent) && ch !== 0) { + throwError(state, "bad indentation of a sequence entry"); + } else if (state.lineIndent < nodeIndent) { + break; + } + } + + if (detected) { + state.tag = tag; + state.anchor = anchor; + state.kind = "sequence"; + state.result = result; + return true; + } + return false; +} + +function readBlockMapping(state, nodeIndent, flowIndent) { + const tag = state.tag, + anchor = state.anchor, + result = {}, + overridableKeys = {}; + let following: number, + allowCompact: boolean, + line: number, + pos: number, + keyTag = null, + keyNode = null, + valueNode = null, + atExplicitKey = false, + detected = false, + ch: number; + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = result; + } + + ch = state.input.charCodeAt(state.position); + + while (ch !== 0) { + following = state.input.charCodeAt(state.position + 1); + line = state.line; // Save the current line. + pos = state.position; + + // + // Explicit notation case. There are two separate blocks: + // first for the key (denoted by "?") and second for the value (denoted by ":") + // + if ( + (ch === 0x3f /* ? */ || ch === 0x3a) /* : */ && + is_WS_OR_EOL(following) + ) { + if (ch === 0x3f /* ? */) { + if (atExplicitKey) { + storeMappingPair( + state, + result, + overridableKeys, + 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; or followed by a non-tabulated empty line" + ); + } + + 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 (ch === 0x3a /* : */) { + ch = state.input.charCodeAt(++state.position); + + if (!is_WS_OR_EOL(ch)) { + throwError( + state, + "a whitespace character is expected after the key-value separator within a block mapping" + ); + } + + if (atExplicitKey) { + storeMappingPair( + state, + result, + overridableKeys, + keyTag, + keyNode, + null + ); + keyTag = keyNode = valueNode = null; + } + + detected = true; + atExplicitKey = false; + allowCompact = false; + keyTag = state.tag; + keyNode = state.result; + } else if (detected) { + throwError( + state, + "can not read an implicit mapping pair; a colon is missed" + ); + } else { + state.tag = tag; + state.anchor = anchor; + return true; // 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" + ); + } 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, + overridableKeys, + keyTag, + keyNode, + valueNode, + line, + pos + ); + keyTag = keyNode = valueNode = null; + } + + skipSeparationSpace(state, true, -1); + ch = state.input.charCodeAt(state.position); + } + + if (state.lineIndent > nodeIndent && ch !== 0) { + 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, overridableKeys, 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: LoaderState) { + let position: number, + isVerbatim = false, + isNamed = false, + tagHandle: string, + tagName: string, + ch: number; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x21 /* ! */) return false; + + if (state.tag !== null) { + throwError(state, "duplication of a tag property"); + } + + ch = state.input.charCodeAt(++state.position); + + if (ch === 0x3c /* < */) { + isVerbatim = true; + ch = state.input.charCodeAt(++state.position); + } else if (ch === 0x21 /* ! */) { + isNamed = true; + tagHandle = "!!"; + ch = state.input.charCodeAt(++state.position); + } else { + tagHandle = "!"; + } + + position = state.position; + + if (isVerbatim) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0 && ch !== 0x3e /* > */); + + if (state.position < state.length) { + tagName = state.input.slice(position, state.position); + ch = state.input.charCodeAt(++state.position); + } else { + throwError(state, "unexpected end of the stream within a verbatim tag"); + } + } else { + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + if (ch === 0x21 /* ! */) { + 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: LoaderState) { + let position: number, ch: number; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x26 /* & */) return false; + + if (state.anchor !== null) { + throwError(state, "duplication of an anchor property"); + } + + ch = state.input.charCodeAt(++state.position); + position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (state.position === position) { + throwError( + state, + "name of an anchor node must contain at least one character" + ); + } + + state.anchor = state.input.slice(position, state.position); + return true; +} + +function readAlias(state) { + let _position, alias, ch; + + ch = state.input.charCodeAt(state.position); + + if (ch !== 0x2a /* * */) return false; + + ch = state.input.charCodeAt(++state.position); + _position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch) && !is_FLOW_INDICATOR(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (state.position === _position) { + throwError( + state, + "name of an alias node must contain at least one character" + ); + } + + alias = state.input.slice(_position, state.position); + + if (!state.anchorMap.hasOwnProperty(alias)) { + throwError(state, `unidentified alias "${alias}"`); + } + + state.result = state.anchorMap[alias]; + skipSeparationSpace(state, true, -1); + return true; +} + +function composeNode( + state: LoaderState, + parentIndent: number, + nodeContext: number, + allowToSeek: boolean, + allowCompact: boolean +) { + let allowBlockStyles: boolean, + allowBlockScalars: boolean, + allowBlockCollections: boolean, + 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; + } + } + } + + if (indentStatus === 1) { + while (readTagProperty(state) || readAnchorProperty(state)) { + if (skipSeparationSpace(state, true, -1)) { + atNewLine = true; + allowBlockCollections = allowBlockStyles; + + if (state.lineIndent > parentIndent) { + indentStatus = 1; + } else if (state.lineIndent === parentIndent) { + indentStatus = 0; + } else if (state.lineIndent < parentIndent) { + indentStatus = -1; + } + } else { + allowBlockCollections = false; + } + } + } + + if (allowBlockCollections) { + allowBlockCollections = atNewLine || allowCompact; + } + + if (indentStatus === 1 || CONTEXT_BLOCK_OUT === nodeContext) { + const cond = + CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext; + flowIndent = cond ? parentIndent : parentIndent + 1; + + blockIndent = state.position - state.lineStart; + + if (indentStatus === 1) { + if ( + (allowBlockCollections && + (readBlockSequence(state, blockIndent) || + readBlockMapping(state, blockIndent, flowIndent))) || + readFlowCollection(state, flowIndent) + ) { + hasContent = true; + } else { + if ( + (allowBlockScalars && readBlockScalar(state, flowIndent)) || + readSingleQuotedScalar(state, flowIndent) || + readDoubleQuotedScalar(state, flowIndent) + ) { + hasContent = true; + } else if (readAlias(state)) { + hasContent = true; + + if (state.tag !== null || state.anchor !== null) { + throwError(state, "alias node should not have any properties"); + } + } else if ( + readPlainScalar(state, flowIndent, CONTEXT_FLOW_IN === nodeContext) + ) { + hasContent = true; + + if (state.tag === null) { + state.tag = "?"; + } + } + + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } else if (indentStatus === 0) { + // 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 (state.tag !== null && state.tag !== "!") { + if (state.tag === "?") { + for ( + let typeIndex = 0, typeQuantity = state.implicitTypes.length; + typeIndex < typeQuantity; + typeIndex++ + ) { + 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. + + if (type.resolve(state.result)) { + // `state.result` updated in resolver if matched + state.result = type.construct(state.result); + state.tag = type.tag; + if (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + break; + } + } + } else if ( + _hasOwnProperty.call(state.typeMap[state.kind || "fallback"], state.tag) + ) { + type = state.typeMap[state.kind || "fallback"][state.tag]; + + if (state.result !== null && 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 (state.anchor !== null) { + state.anchorMap[state.anchor] = state.result; + } + } + } else { + throwError(state, `unknown tag !<${state.tag}>`); + } + } + + if (state.listener && state.listener !== null) { + state.listener("close", state); + } + return state.tag !== null || state.anchor !== null || hasContent; +} + +function readDocument(state: LoaderState) { + const documentStart = state.position; + let position: number, + directiveName: string, + directiveArgs: any[], + hasDirectives = false, + ch: number; + + state.version = null; + state.checkLineBreaks = state.legacy; + state.tagMap = {}; + state.anchorMap = {}; + + while ((ch = state.input.charCodeAt(state.position)) !== 0) { + skipSeparationSpace(state, true, -1); + + ch = state.input.charCodeAt(state.position); + + if (state.lineIndent > 0 || ch !== 0x25 /* % */) { + break; + } + + hasDirectives = true; + ch = state.input.charCodeAt(++state.position); + position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + directiveName = state.input.slice(position, state.position); + directiveArgs = []; + + if (directiveName.length < 1) { + throwError( + state, + "directive name must not be less than one character in length" + ); + } + + while (ch !== 0) { + while (is_WHITE_SPACE(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + if (ch === 0x23 /* # */) { + do { + ch = state.input.charCodeAt(++state.position); + } while (ch !== 0 && !is_EOL(ch)); + break; + } + + if (is_EOL(ch)) break; + + position = state.position; + + while (ch !== 0 && !is_WS_OR_EOL(ch)) { + ch = state.input.charCodeAt(++state.position); + } + + directiveArgs.push(state.input.slice(position, state.position)); + } + + if (ch !== 0) readLineBreak(state); + + if (_hasOwnProperty.call(directiveHandlers, directiveName)) { + directiveHandlers[directiveName](state, directiveName, directiveArgs); + } else { + throwWarning(state, `unknown document directive "${directiveName}"`); + } + } + + skipSeparationSpace(state, true, -1); + + if ( + state.lineIndent === 0 && + state.input.charCodeAt(state.position) === 0x2d /* - */ && + state.input.charCodeAt(state.position + 1) === 0x2d /* - */ && + state.input.charCodeAt(state.position + 2) === 0x2d /* - */ + ) { + state.position += 3; + skipSeparationSpace(state, true, -1); + } else if (hasDirectives) { + throwError(state, "directives end mark is expected"); + } + + composeNode(state, state.lineIndent - 1, CONTEXT_BLOCK_OUT, false, true); + skipSeparationSpace(state, true, -1); + + if ( + state.checkLineBreaks && + PATTERN_NON_ASCII_LINE_BREAKS.test( + state.input.slice(documentStart, state.position) + ) + ) { + throwWarning(state, "non-ASCII line breaks are interpreted as content"); + } + + state.documents.push(state.result); + + if (state.position === state.lineStart && testDocumentSeparator(state)) { + if (state.input.charCodeAt(state.position) === 0x2e /* . */) { + 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?: LoaderStateOptions) { + input = String(input); + options = options || {}; + + if (input.length !== 0) { + // Add tailing `\n` if not exists + if ( + input.charCodeAt(input.length - 1) !== 0x0a /* LF */ && + input.charCodeAt(input.length - 1) !== 0x0d /* CR */ + ) { + input += "\n"; + } + + // Strip BOM + if (input.charCodeAt(0) === 0xfeff) { + input = input.slice(1); + } + } + + const state = new LoaderState(input, options); + + // Use 0 as string terminator. That significantly simplifies bounds check. + state.input += "\0"; + + while (state.input.charCodeAt(state.position) === 0x20 /* Space */) { + state.lineIndent += 1; + state.position += 1; + } + + while (state.position < state.length - 1) { + readDocument(state); + } + + return state.documents; +} + +export type CbFunction = (doc: any) => void; +function isCbFunction(fn: any): fn is CbFunction { + return typeof fn === "function"; +} + +export function loadAll( + input: string, + iteratorOrOption?: T, + options?: LoaderStateOptions +): T extends CbFunction ? void : any[] { + if (!isCbFunction(iteratorOrOption)) { + return loadDocuments(input, iteratorOrOption as LoaderStateOptions) as any; + } + + const documents = loadDocuments(input, options); + const iterator = iteratorOrOption; + for (let index = 0, length = documents.length; index < length; index++) { + iterator(documents[index]); + } +} + +export function load(input: string, options?: LoaderStateOptions): any { + const documents = loadDocuments(input, options); + + if (documents.length === 0) { + return; + } + if (documents.length === 1) { + return documents[0]; + } + throw new YAMLError( + "expected a single document in the stream, but found more" + ); +} + +export function safeLoadAll( + input: string, + outputOrOptions?: T, + options: LoaderStateOptions = {} +): T extends CbFunction ? void : any[] { + if (isCbFunction(outputOrOptions)) { + loadAll(input, outputOrOptions, { + schema: DEFAULT_SAFE_SCHEMA, + ...options + }); + } else { + options = (outputOrOptions as LoaderStateOptions) || {}; + return loadAll(input, { + schema: DEFAULT_SAFE_SCHEMA, + ...options + } as any); + } +} + +export function safeLoad(input: string, options?: LoaderStateOptions): any { + return load(input, { schema: DEFAULT_SAFE_SCHEMA, ...options }); +} diff --git a/yaml/mod.ts b/yaml/mod.ts new file mode 100644 index 000000000000..58bfd3f482e3 --- /dev/null +++ b/yaml/mod.ts @@ -0,0 +1,5 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +export * from "./parse.ts"; +export * from "./stringify.ts"; +export * from "./schema/mod.ts"; diff --git a/yaml/parse.ts b/yaml/parse.ts new file mode 100644 index 000000000000..0015cae351c2 --- /dev/null +++ b/yaml/parse.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { CbFunction, safeLoad, safeLoadAll } from "./loader/loader.ts"; +import { LoaderStateOptions } from "./loader/LoaderState.ts"; + +export type ParseOptions = LoaderStateOptions; + +export function parse(content: string, options?: ParseOptions) { + return safeLoad(content, options); +} + +export function parseAll( + content: string, + iterator?: CbFunction, + options?: ParseOptions +) { + return safeLoadAll(content, iterator, options); +} diff --git a/yaml/parse_test.ts b/yaml/parse_test.ts new file mode 100644 index 000000000000..b0da3cdc8138 --- /dev/null +++ b/yaml/parse_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { parse } from "./mod.ts"; +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; + +test({ + name: "parsed correctly", + fn(): void { + const FIXTURE = ` + test: toto + foo: + bar: True + baz: 1 + qux: ~ + `; + + const ASSERTS = { test: "toto", foo: { bar: true, baz: 1, qux: null } }; + + assertEquals(parse(FIXTURE), ASSERTS); + } +}); diff --git a/yaml/schema/core.ts b/yaml/schema/core.ts new file mode 100644 index 000000000000..1bc7e77b5986 --- /dev/null +++ b/yaml/schema/core.ts @@ -0,0 +1,12 @@ +import { Schema } from "../Schema.ts"; +import { json } from "./json.ts"; + +// 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. + +export const core = new Schema({ + include: [json] +}); diff --git a/yaml/schema/default_full.ts b/yaml/schema/default_full.ts new file mode 100644 index 000000000000..3a9ef39584ca --- /dev/null +++ b/yaml/schema/default_full.ts @@ -0,0 +1,20 @@ +import { Schema } from "../Schema.ts"; +import { default_safe } from "./default_safe.ts"; + +// 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. + +export const default_full = (Schema.SCHEMA_DEFAULT = new Schema({ + include: [default_safe] + /* TODO: JS */ + // explicit: [ + // require('../type/js/undefined'), + // require('../type/js/regexp'), + // require('../type/js/function') + // ] +})); diff --git a/yaml/schema/default_safe.ts b/yaml/schema/default_safe.ts new file mode 100644 index 000000000000..feaf2fe9e6e6 --- /dev/null +++ b/yaml/schema/default_safe.ts @@ -0,0 +1,15 @@ +import { Schema } from "../Schema.ts"; +import { binary, merge, omap, pairs, set, timestamp } from "../type/mod.ts"; +import { core } from "./core.ts"; + +// 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/) + +export const default_safe = new Schema({ + explicit: [binary, omap, pairs, set], + implicit: [timestamp, merge], + include: [core] +}); diff --git a/yaml/schema/failsafe.ts b/yaml/schema/failsafe.ts new file mode 100644 index 000000000000..4047681dd114 --- /dev/null +++ b/yaml/schema/failsafe.ts @@ -0,0 +1,9 @@ +import { Schema } from "../Schema.ts"; +import { map, seq, str } from "../type/mod.ts"; + +// Standard YAML's Failsafe schema. +// http://www.yaml.org/spec/1.2/spec.html#id2802346 + +export const failsafe = new Schema({ + explicit: [str, seq, map] +}); diff --git a/yaml/schema/json.ts b/yaml/schema/json.ts new file mode 100644 index 000000000000..7dc48b3f5cb9 --- /dev/null +++ b/yaml/schema/json.ts @@ -0,0 +1,15 @@ +import { Schema } from "../Schema.ts"; +import { bool, float, int, nil } from "../type/mod.ts"; +import { failsafe } from "./failsafe.ts"; + +// 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. + +export const json = new Schema({ + implicit: [nil, bool, int, float], + include: [failsafe] +}); diff --git a/yaml/schema/mod.ts b/yaml/schema/mod.ts new file mode 100644 index 000000000000..fc85d2a027b1 --- /dev/null +++ b/yaml/schema/mod.ts @@ -0,0 +1,5 @@ +export { core as CORE_SCHEMA } from "./core.ts"; +export { default_full as DEFAULT_FULL_SCHEMA } from "./default_full.ts"; +export { default_safe as DEFAULT_SAFE_SCHEMA } from "./default_safe.ts"; +export { failsafe as FAILSAFE_SCHEMA } from "./failsafe.ts"; +export { json as JSON_SCHEMA } from "./json.ts"; diff --git a/yaml/stringify.ts b/yaml/stringify.ts new file mode 100644 index 000000000000..233d3a3bf6e4 --- /dev/null +++ b/yaml/stringify.ts @@ -0,0 +1,10 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { safeDump } from "./dumper/dumper.ts"; +import { DumperStateOptions } from "./dumper/DumperState.ts"; + +export type DumpOptions = DumperStateOptions; + +export function stringify(obj: object, options?: DumpOptions): string { + return safeDump(obj, options); +} diff --git a/yaml/stringify_test.ts b/yaml/stringify_test.ts new file mode 100644 index 000000000000..dd03bde29237 --- /dev/null +++ b/yaml/stringify_test.ts @@ -0,0 +1,39 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { stringify } from "./mod.ts"; + +test({ + name: "stringified correctly", + fn(): void { + const FIXTURE = { + foo: { + bar: true, + test: [ + "a", + "b", + { + a: false + }, + { + a: false + } + ] + }, + test: "foobar" + }; + + const ASSERTS = `foo: + bar: true + test: + - a + - b + - a: false + - a: false +test: foobar +`; + + assertEquals(stringify(FIXTURE), ASSERTS); + } +}); diff --git a/yaml/test.ts b/yaml/test.ts new file mode 100644 index 000000000000..aa937caafc4c --- /dev/null +++ b/yaml/test.ts @@ -0,0 +1,8 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { runTests } from "../testing/mod.ts"; + +import "./parse_test.ts"; +import "./stringify_test.ts"; + +runTests(); diff --git a/yaml/type/binary.ts b/yaml/type/binary.ts new file mode 100644 index 000000000000..ace8727be5b5 --- /dev/null +++ b/yaml/type/binary.ts @@ -0,0 +1,132 @@ +import { Type } from "../Type.ts"; + +const { Buffer } = Deno; + +// [ 64, 65, 66 ] -> [ padding, CR, LF ] +const BASE64_MAP = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r"; + +function resolveYamlBinary(data: any) { + if (data === null) return false; + + let code: number; + let bitlen = 0; + const max = data.length; + const map = BASE64_MAP; + + // Convert one by one. + for (let 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: string) { + const input = data.replace(/[\r\n=]/g, ""); // remove CR/LF & padding to simplify scan + const max = input.length; + const map = BASE64_MAP; + + // Collect by 6*4 bits (3 bytes) + + const result = []; + let bits = 0; + for (let 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 + + const 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); + } + + return new Buffer(new Uint8Array(result)); +} + +function representYamlBinary(object: Uint8Array) { + const max = object.length; + const map = BASE64_MAP; + + // Convert every three bytes to 4 ASCII characters. + + let result = ""; + let bits = 0; + for (let 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 + + const 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(obj: any): obj is Deno.Buffer { + const buf = new Buffer(); + try { + if (0 > buf.readFromSync(obj as Deno.Buffer)) return true; + return false; + } catch { + return false; + } finally { + buf.reset(); + } +} + +export const binary = new Type("tag:yaml.org,2002:binary", { + construct: constructYamlBinary, + kind: "scalar", + predicate: isBinary, + represent: representYamlBinary, + resolve: resolveYamlBinary +}); diff --git a/yaml/type/bool.ts b/yaml/type/bool.ts new file mode 100644 index 000000000000..1d031c409480 --- /dev/null +++ b/yaml/type/bool.ts @@ -0,0 +1,34 @@ +import { Type } from "../Type.ts"; +import { isBoolean } from "../utils.ts"; + +function resolveYamlBoolean(data: string) { + const max = data.length; + + return ( + (max === 4 && (data === "true" || data === "True" || data === "TRUE")) || + (max === 5 && (data === "false" || data === "False" || data === "FALSE")) + ); +} + +function constructYamlBoolean(data: string) { + return data === "true" || data === "True" || data === "TRUE"; +} + +export const bool = new Type("tag:yaml.org,2002:bool", { + construct: constructYamlBoolean, + defaultStyle: "lowercase", + kind: "scalar", + predicate: isBoolean, + represent: { + lowercase(object: boolean) { + return object ? "true" : "false"; + }, + uppercase(object: boolean) { + return object ? "TRUE" : "FALSE"; + }, + camelcase(object: boolean) { + return object ? "True" : "False"; + } + }, + resolve: resolveYamlBoolean +}); diff --git a/yaml/type/float.ts b/yaml/type/float.ts new file mode 100644 index 000000000000..bfaf6afdc0c9 --- /dev/null +++ b/yaml/type/float.ts @@ -0,0 +1,122 @@ +import { StyleVariant, Type } from "../Type.ts"; +import { isNegativeZero } from "../utils.ts"; + +const YAML_FLOAT_PATTERN = new RegExp( + // 2.5e4, 2.5 and integers + "^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?" + + // .2e4, .2 + // special case, seems not from spec + "|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?" + + // 20:59 + "|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*" + + // .inf + "|[-+]?\\.(?:inf|Inf|INF)" + + // .nan + "|\\.(?:nan|NaN|NAN))$" +); + +function resolveYamlFloat(data: string) { + if ( + !YAML_FLOAT_PATTERN.test(data) || + // Quick hack to not allow integers end with `_` + // Probably should update regexp & check speed + data[data.length - 1] === "_" + ) { + return false; + } + + return true; +} + +function constructYamlFloat(data: string) { + let value = data.replace(/_/g, "").toLowerCase(); + const sign = value[0] === "-" ? -1 : 1; + const digits: number[] = []; + + if ("+-".indexOf(value[0]) >= 0) { + value = value.slice(1); + } + + if (value === ".inf") { + return sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; + } + if (value === ".nan") { + return NaN; + } + if (value.indexOf(":") >= 0) { + value.split(":").forEach(v => { + digits.unshift(parseFloat(v)); + }); + + let valueNb = 0.0; + let base = 1; + + digits.forEach(d => { + valueNb += d * base; + base *= 60; + }); + + return sign * valueNb; + } + return sign * parseFloat(value); +} + +const SCIENTIFIC_WITHOUT_DOT = /^[-+]?[0-9]+e/; + +function representYamlFloat(object: any, style?: StyleVariant) { + let res; + + if (isNaN(object)) { + switch (style) { + case "lowercase": + return ".nan"; + case "uppercase": + return ".NAN"; + case "camelcase": + return ".NaN"; + } + } else if (Number.POSITIVE_INFINITY === object) { + switch (style) { + case "lowercase": + return ".inf"; + case "uppercase": + return ".INF"; + case "camelcase": + return ".Inf"; + } + } else if (Number.NEGATIVE_INFINITY === object) { + switch (style) { + case "lowercase": + return "-.inf"; + case "uppercase": + return "-.INF"; + case "camelcase": + return "-.Inf"; + } + } else if (isNegativeZero(object)) { + return "-0.0"; + } + + res = object.toString(10); + + // JS stringifier can build scientific format without dots: 5e-100, + // while YAML requres dot: 5.e-100. Fix it with simple hack + + return SCIENTIFIC_WITHOUT_DOT.test(res) ? res.replace("e", ".e") : res; +} + +function isFloat(object: any): boolean { + return ( + Object.prototype.toString.call(object) === "[object Number]" && + (object % 1 !== 0 || isNegativeZero(object)) + ); +} + +export const float = new Type("tag:yaml.org,2002:float", { + construct: constructYamlFloat, + defaultStyle: "lowercase", + kind: "scalar", + predicate: isFloat, + represent: representYamlFloat, + resolve: resolveYamlFloat +}); diff --git a/yaml/type/int.ts b/yaml/type/int.ts new file mode 100644 index 000000000000..898a9fc7caa6 --- /dev/null +++ b/yaml/type/int.ts @@ -0,0 +1,185 @@ +import { Type } from "../Type.ts"; +import { isNegativeZero } from "../utils.ts"; + +function isHexCode(c: number) { + return ( + (0x30 /* 0 */ <= c && c <= 0x39) /* 9 */ || + (0x41 /* A */ <= c && c <= 0x46) /* F */ || + (0x61 /* a */ <= c && c <= 0x66) /* f */ + ); +} + +function isOctCode(c: number) { + return 0x30 /* 0 */ <= c && c <= 0x37 /* 7 */; +} + +function isDecCode(c: number) { + return 0x30 /* 0 */ <= c && c <= 0x39 /* 9 */; +} + +function resolveYamlInteger(data: string) { + const max = data.length; + let index = 0; + let hasDigits = false; + + if (!max) return false; + + let 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 && ch !== "_"; + } + + 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 && ch !== "_"; + } + + // base 8 + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (!isOctCode(data.charCodeAt(index))) return false; + hasDigits = true; + } + return hasDigits && ch !== "_"; + } + + // base 10 (except 0) or base 60 + + // value should not start with `_`; + if (ch === "_") return false; + + for (; index < max; index++) { + ch = data[index]; + if (ch === "_") continue; + if (ch === ":") break; + if (!isDecCode(data.charCodeAt(index))) { + return false; + } + hasDigits = true; + } + + // Should have digits and should not end with `_` + if (!hasDigits || ch === "_") 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: string) { + let value = data; + const digits: number[] = []; + + if (value.indexOf("_") !== -1) { + value = value.replace(/_/g, ""); + } + + let sign = 1; + let ch = value[0]; + if (ch === "-" || ch === "+") { + if (ch === "-") sign = -1; + value = value.slice(1); + ch = value[0]; + } + + if (value === "0") return 0; + + if (ch === "0") { + if (value[1] === "b") return sign * parseInt(value.slice(2), 2); + if (value[1] === "x") return sign * parseInt(value, 16); + return sign * parseInt(value, 8); + } + + if (value.indexOf(":") !== -1) { + value.split(":").forEach(v => { + digits.unshift(parseInt(v, 10)); + }); + + let valueInt = 0; + let base = 1; + + digits.forEach(d => { + valueInt += d * base; + base *= 60; + }); + + return sign * valueInt; + } + + return sign * parseInt(value, 10); +} + +function isInteger(object: any): boolean { + return ( + Object.prototype.toString.call(object) === "[object Number]" && + (object % 1 === 0 && !isNegativeZero(object)) + ); +} + +export const int = new Type("tag:yaml.org,2002:int", { + construct: constructYamlInteger, + defaultStyle: "decimal", + kind: "scalar", + predicate: isInteger, + represent: { + binary(obj: number) { + return obj >= 0 + ? `0b${obj.toString(2)}` + : `-0b${obj.toString(2).slice(1)}`; + }, + octal(obj: number) { + return obj >= 0 ? `0${obj.toString(8)}` : `-0${obj.toString(8).slice(1)}`; + }, + decimal(obj: number) { + return obj.toString(10); + }, + hexadecimal(obj: number) { + return obj >= 0 + ? `0x${obj.toString(16).toUpperCase()}` + : `-0x${obj + .toString(16) + .toUpperCase() + .slice(1)}`; + } + }, + resolve: resolveYamlInteger, + styleAliases: { + binary: [2, "bin"], + decimal: [10, "dec"], + hexadecimal: [16, "hex"], + octal: [8, "oct"] + } +}); diff --git a/yaml/type/map.ts b/yaml/type/map.ts new file mode 100644 index 000000000000..0c79bb08f861 --- /dev/null +++ b/yaml/type/map.ts @@ -0,0 +1,8 @@ +import { Type } from "../Type.ts"; + +export const map = new Type("tag:yaml.org,2002:map", { + construct(data) { + return data !== null ? data : {}; + }, + kind: "mapping" +}); diff --git a/yaml/type/merge.ts b/yaml/type/merge.ts new file mode 100644 index 000000000000..bbf6a03a55c6 --- /dev/null +++ b/yaml/type/merge.ts @@ -0,0 +1,10 @@ +import { Type } from "../Type.ts"; + +function resolveYamlMerge(data: string) { + return data === "<<" || data === null; +} + +export const merge = new Type("tag:yaml.org,2002:merge", { + kind: "scalar", + resolve: resolveYamlMerge +}); diff --git a/yaml/type/mod.ts b/yaml/type/mod.ts new file mode 100644 index 000000000000..865a62dce45b --- /dev/null +++ b/yaml/type/mod.ts @@ -0,0 +1,13 @@ +export { binary } from "./binary.ts"; +export { bool } from "./bool.ts"; +export { float } from "./float.ts"; +export { int } from "./int.ts"; +export { map } from "./map.ts"; +export { merge } from "./merge.ts"; +export { nil } from "./nil.ts"; +export { omap } from "./omap.ts"; +export { pairs } from "./pairs.ts"; +export { seq } from "./seq.ts"; +export { set } from "./set.ts"; +export { str } from "./str.ts"; +export { timestamp } from "./timestamp.ts"; diff --git a/yaml/type/nil.ts b/yaml/type/nil.ts new file mode 100644 index 000000000000..73d6af526052 --- /dev/null +++ b/yaml/type/nil.ts @@ -0,0 +1,40 @@ +import { Type } from "../Type.ts"; + +function resolveYamlNull(data: string) { + const max = data.length; + + return ( + (max === 1 && data === "~") || + (max === 4 && (data === "null" || data === "Null" || data === "NULL")) + ); +} + +function constructYamlNull() { + return null; +} + +function isNull(object: any) { + return object === null; +} + +export const nil = new Type("tag:yaml.org,2002:null", { + construct: constructYamlNull, + defaultStyle: "lowercase", + kind: "scalar", + predicate: isNull, + represent: { + canonical() { + return "~"; + }, + lowercase() { + return "null"; + }, + uppercase() { + return "NULL"; + }, + camelcase() { + return "Null"; + } + }, + resolve: resolveYamlNull +}); diff --git a/yaml/type/omap.ts b/yaml/type/omap.ts new file mode 100644 index 000000000000..50043214c939 --- /dev/null +++ b/yaml/type/omap.ts @@ -0,0 +1,40 @@ +import { Type } from "../Type.ts"; + +const _hasOwnProperty = Object.prototype.hasOwnProperty; +const _toString = Object.prototype.toString; + +function resolveYamlOmap(data: any) { + const objectKeys: string[] = []; + let pairKey = ""; + let pairHasKey = false; + + for (const pair of data) { + pairHasKey = false; + + if (_toString.call(pair) !== "[object Object]") return false; + + for (pairKey in pair) { + if (_hasOwnProperty.call(pair, pairKey)) { + if (!pairHasKey) pairHasKey = true; + else return false; + } + } + + if (!pairHasKey) return false; + + if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey); + else return false; + } + + return true; +} + +function constructYamlOmap(data: any) { + return data !== null ? data : []; +} + +export const omap = new Type("tag:yaml.org,2002:omap", { + construct: constructYamlOmap, + kind: "sequence", + resolve: resolveYamlOmap +}); diff --git a/yaml/type/pairs.ts b/yaml/type/pairs.ts new file mode 100644 index 000000000000..9a3d704dba18 --- /dev/null +++ b/yaml/type/pairs.ts @@ -0,0 +1,43 @@ +import { Type } from "../Type.ts"; + +const _toString = Object.prototype.toString; + +function resolveYamlPairs(data: any[][]) { + const result = new Array(data.length); + + for (let index = 0; index < data.length; index++) { + const pair = data[index]; + + if (_toString.call(pair) !== "[object Object]") return false; + + const keys = Object.keys(pair); + + if (keys.length !== 1) return false; + + result[index] = [keys[0], pair[keys[0] as any]]; + } + + return true; +} + +function constructYamlPairs(data: string) { + if (data === null) return []; + + const result = new Array(data.length); + + for (let index = 0; index < data.length; index += 1) { + const pair = data[index]; + + const keys = Object.keys(pair); + + result[index] = [keys[0], pair[keys[0] as any]]; + } + + return result; +} + +export const pairs = new Type("tag:yaml.org,2002:pairs", { + construct: constructYamlPairs, + kind: "sequence", + resolve: resolveYamlPairs +}); diff --git a/yaml/type/seq.ts b/yaml/type/seq.ts new file mode 100644 index 000000000000..9f006d124975 --- /dev/null +++ b/yaml/type/seq.ts @@ -0,0 +1,8 @@ +import { Type } from "../Type.ts"; + +export const seq = new Type("tag:yaml.org,2002:seq", { + construct(data) { + return data !== null ? data : []; + }, + kind: "sequence" +}); diff --git a/yaml/type/set.ts b/yaml/type/set.ts new file mode 100644 index 000000000000..308c74ecd818 --- /dev/null +++ b/yaml/type/set.ts @@ -0,0 +1,25 @@ +import { Type } from "../Type.ts"; + +const _hasOwnProperty = Object.prototype.hasOwnProperty; + +function resolveYamlSet(data: any) { + if (data === null) return true; + + for (const key in data) { + if (_hasOwnProperty.call(data, key)) { + if (data[key] !== null) return false; + } + } + + return true; +} + +function constructYamlSet(data: string) { + return data !== null ? data : {}; +} + +export const set = new Type("tag:yaml.org,2002:set", { + construct: constructYamlSet, + kind: "mapping", + resolve: resolveYamlSet +}); diff --git a/yaml/type/str.ts b/yaml/type/str.ts new file mode 100644 index 000000000000..f26b47538133 --- /dev/null +++ b/yaml/type/str.ts @@ -0,0 +1,8 @@ +import { Type } from "../Type.ts"; + +export const str = new Type("tag:yaml.org,2002:str", { + construct(data) { + return data !== null ? data : ""; + }, + kind: "scalar" +}); diff --git a/yaml/type/timestamp.ts b/yaml/type/timestamp.ts new file mode 100644 index 000000000000..06e1bea2fdf0 --- /dev/null +++ b/yaml/type/timestamp.ts @@ -0,0 +1,91 @@ +import { Type } from "../Type.ts"; + +const YAML_DATE_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 +); + +const 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: string) { + if (data === null) return false; + if (YAML_DATE_REGEXP.exec(data) !== null) return true; + if (YAML_TIMESTAMP_REGEXP.exec(data) !== null) return true; + return false; +} + +function constructYamlTimestamp(data: string) { + let match = YAML_DATE_REGEXP.exec(data); + if (match === null) match = YAML_TIMESTAMP_REGEXP.exec(data); + + if (match === null) throw new Error("Date resolve error"); + + // match: [1] year [2] month [3] day + + const year = +match[1]; + const month = +match[2] - 1; // JS month starts with 0 + const 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 + + const hour = +match[4]; + const minute = +match[5]; + const second = +match[6]; + + let fraction = 0; + if (match[7]) { + let partFraction = match[7].slice(0, 3); + while (partFraction.length < 3) { + // milli-seconds + partFraction += "0"; + } + fraction = +partFraction; + } + + // match: [8] tz [9] tz_sign [10] tz_hour [11] tz_minute + + let delta = null; + if (match[9]) { + const tz_hour = +match[10]; + const tz_minute = +(match[11] || 0); + delta = (tz_hour * 60 + tz_minute) * 60000; // delta in mili-seconds + if (match[9] === "-") delta = -delta; + } + + const date = new Date( + Date.UTC(year, month, day, hour, minute, second, fraction) + ); + + if (delta) date.setTime(date.getTime() - delta); + + return date; +} + +function representYamlTimestamp(date: Date) { + return date.toISOString(); +} + +export const timestamp = new Type("tag:yaml.org,2002:timestamp", { + construct: constructYamlTimestamp, + instanceOf: Date, + kind: "scalar", + represent: representYamlTimestamp, + resolve: resolveYamlTimestamp +}); diff --git a/yaml/utils.ts b/yaml/utils.ts new file mode 100644 index 000000000000..cd64ccb292e9 --- /dev/null +++ b/yaml/utils.ts @@ -0,0 +1,76 @@ +export function isNothing(subject: any): subject is never { + return typeof subject === "undefined" || subject === null; +} + +export function isArray(value: unknown): value is any[] { + return Array.isArray(value); +} + +export function isBoolean(value: unknown): value is boolean { + return typeof value === "boolean" || value instanceof Boolean; +} + +export function isNull(value: unknown): value is null { + return value === null; +} + +export function isNullOrUndefined(value: unknown): value is null | undefined { + return value === null || value === undefined; +} + +export function isNumber(value: unknown): value is number { + return typeof value === "number" || value instanceof Number; +} + +export function isString(value: unknown): value is string { + return typeof value === "string" || value instanceof String; +} + +export function isSymbol(value: unknown): value is symbol { + return typeof value === "symbol"; +} + +export function isUndefined(value: unknown): value is undefined { + return value === undefined; +} + +export function isObject(value: unknown): value is object { + return value !== null && typeof value === "object"; +} + +export function isError(e: unknown): boolean { + return e instanceof Error; +} + +export function isFunction(value: unknown): value is () => void { + return typeof value === "function"; +} + +export function isRegExp(value: unknown): value is RegExp { + return value instanceof RegExp; +} + +export function toArray(sequence: T): T | [] | [T] { + if (isArray(sequence)) return sequence; + if (isNothing(sequence)) return []; + + return [sequence]; +} + +export function repeat(str: string, count: number) { + let result = ""; + + for (let cycle = 0; cycle < count; cycle++) { + result += str; + } + + return result; +} + +export function isNegativeZero(i: number) { + return i === 0 && Number.NEGATIVE_INFINITY === 1 / i; +} + +export interface ArrayObject { + [P: string]: T; +}