From 1309fe75978e964db6c0cd49c88d5c1671137ee6 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 12:07:17 +0200 Subject: [PATCH 01/24] Refactor more top-level files as TypeScript --- rollup.browser-config.js | 4 +- rollup.node-config.js | 4 +- src/ast/index.d.ts | 5 +++ src/constants.js | 41 ------------------- src/{constants.d.ts => constants.ts} | 30 +++++++++----- src/log.d.ts | 4 -- src/{log.js => log.ts} | 6 +-- ...{stringifyNumber.js => stringifyNumber.ts} | 9 +++- src/stringify/stringifyString.d.ts | 9 ++++ src/{types.js => types.ts} | 9 +--- src/{util.js => util.ts} | 4 +- types.d.ts | 4 +- util.d.ts | 17 +------- 13 files changed, 53 insertions(+), 93 deletions(-) delete mode 100644 src/constants.js rename src/{constants.d.ts => constants.ts} (56%) delete mode 100644 src/log.d.ts rename src/{log.js => log.ts} (62%) rename src/stringify/{stringifyNumber.js => stringifyNumber.ts} (79%) create mode 100644 src/stringify/stringifyString.d.ts rename src/{types.js => types.ts} (57%) rename src/{util.js => util.ts} (75%) diff --git a/rollup.browser-config.js b/rollup.browser-config.js index 79ab32bf..61737ac6 100644 --- a/rollup.browser-config.js +++ b/rollup.browser-config.js @@ -5,8 +5,8 @@ import typescript from '@rollup/plugin-typescript' export default { input: { index: 'src/index.ts', - types: 'src/types.js', - util: 'src/util.js' + types: 'src/types.ts', + util: 'src/util.ts' }, output: { dir: 'browser/dist', format: 'esm', preserveModules: true }, plugins: [ diff --git a/rollup.node-config.js b/rollup.node-config.js index 9f0d36d5..8a2e638c 100644 --- a/rollup.node-config.js +++ b/rollup.node-config.js @@ -6,8 +6,8 @@ export default { input: { index: 'src/index.ts', 'test-events': 'src/test-events.js', - types: 'src/types.js', - util: 'src/util.js' + types: 'src/types.ts', + util: 'src/util.ts' }, output: { dir: 'dist', diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts index 77c84013..62015eba 100644 --- a/src/ast/index.d.ts +++ b/src/ast/index.d.ts @@ -2,6 +2,10 @@ import { Type } from '../constants' import { Document } from '../doc/Document' import { Schema } from '../doc/Schema' +export { ToJSContext, toJS } from './toJS' + +export function findPair(items: any[], key: Scalar | any): Pair | undefined + export class Node { /** A comment on or immediately after this */ comment?: string | null @@ -39,6 +43,7 @@ export class Scalar extends Node { * The YAML 1.1 schema also supports 'BIN' and 'TIME' */ format?: string + minFractionDigits?: number value: any toJSON(arg?: any, ctx?: AST.NodeToJsonContext): any toString(): string diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index 8f19ac35..00000000 --- a/src/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -export const Char = { - ANCHOR: '&', - COMMENT: '#', - TAG: '!', - DIRECTIVES_END: '-', - DOCUMENT_END: '.' -} - -export const LogLevel = Object.assign(['silent', 'error', 'warn', 'debug'], { - SILENT: 0, - ERROR: 1, - WARN: 2, - DEBUG: 3 -}) - -export const Type = { - ALIAS: 'ALIAS', - BLANK_LINE: 'BLANK_LINE', - BLOCK_FOLDED: 'BLOCK_FOLDED', - BLOCK_LITERAL: 'BLOCK_LITERAL', - COMMENT: 'COMMENT', - DIRECTIVE: 'DIRECTIVE', - DOCUMENT: 'DOCUMENT', - FLOW_MAP: 'FLOW_MAP', - FLOW_SEQ: 'FLOW_SEQ', - MAP: 'MAP', - MAP_KEY: 'MAP_KEY', - MAP_VALUE: 'MAP_VALUE', - PLAIN: 'PLAIN', - QUOTE_DOUBLE: 'QUOTE_DOUBLE', - QUOTE_SINGLE: 'QUOTE_SINGLE', - SEQ: 'SEQ', - SEQ_ITEM: 'SEQ_ITEM' -} - -export const defaultTagPrefix = 'tag:yaml.org,2002:' -export const defaultTags = { - MAP: 'tag:yaml.org,2002:map', - SEQ: 'tag:yaml.org,2002:seq', - STR: 'tag:yaml.org,2002:str' -} diff --git a/src/constants.d.ts b/src/constants.ts similarity index 56% rename from src/constants.d.ts rename to src/constants.ts index 7f26d51a..bfe885eb 100644 --- a/src/constants.d.ts +++ b/src/constants.ts @@ -1,12 +1,22 @@ export type LogLevelId = 'silent' | 'error' | 'warn' | 'debug' -export interface LogLevel extends Array { - SILENT: 0 - ERROR: 1 - WARN: 2 - DEBUG: 3 +export const Char = { + ANCHOR: '&', + COMMENT: '#', + TAG: '!', + DIRECTIVES_END: '-', + DOCUMENT_END: '.' } -export const LogLevel: LogLevel + +export const LogLevel = Object.assign< + LogLevelId[], + { SILENT: 0; ERROR: 1; WARN: 2; DEBUG: 3 } +>(['silent', 'error', 'warn', 'debug'], { + SILENT: 0, + ERROR: 1, + WARN: 2, + DEBUG: 3 +}) export enum Type { ALIAS = 'ALIAS', @@ -28,9 +38,9 @@ export enum Type { SEQ_ITEM = 'SEQ_ITEM' } -export const defaultTagPrefix: 'tag:yaml.org,2002:' -export const defaultTags: { - MAP: 'tag:yaml.org,2002:map' - SEQ: 'tag:yaml.org,2002:seq' +export const defaultTagPrefix = 'tag:yaml.org,2002:' +export const defaultTags = { + MAP: 'tag:yaml.org,2002:map', + SEQ: 'tag:yaml.org,2002:seq', STR: 'tag:yaml.org,2002:str' } diff --git a/src/log.d.ts b/src/log.d.ts deleted file mode 100644 index 8b69740d..00000000 --- a/src/log.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { LogLevelId } from './constants.js' - -export function debug(logLevel: LogLevelId, ...messages: any[]): void -export function warn(logLevel: LogLevelId, warning: string | Error): void diff --git a/src/log.js b/src/log.ts similarity index 62% rename from src/log.js rename to src/log.ts index 47320964..802cbdad 100644 --- a/src/log.js +++ b/src/log.ts @@ -1,13 +1,13 @@ /* global console, process */ -import { LogLevel } from './constants.js' +import { LogLevel, LogLevelId } from './constants.js' -export function debug(logLevel, ...messages) { +export function debug(logLevel: LogLevelId, ...messages: any[]) { if (LogLevel.indexOf(logLevel) >= LogLevel.DEBUG) console.log.apply(console, messages) } -export function warn(logLevel, warning) { +export function warn(logLevel: LogLevelId, warning: string | Error) { if (LogLevel.indexOf(logLevel) >= LogLevel.WARN) { if (typeof process !== 'undefined' && process.emitWarning) process.emitWarning(warning) diff --git a/src/stringify/stringifyNumber.js b/src/stringify/stringifyNumber.ts similarity index 79% rename from src/stringify/stringifyNumber.js rename to src/stringify/stringifyNumber.ts index 555d5fbf..2aac473a 100644 --- a/src/stringify/stringifyNumber.js +++ b/src/stringify/stringifyNumber.ts @@ -1,4 +1,11 @@ -export function stringifyNumber({ format, minFractionDigits, tag, value }) { +import { Scalar } from '../ast' + +export function stringifyNumber({ + format, + minFractionDigits, + tag, + value +}: Scalar) { if (typeof value === 'bigint') return String(value) if (!isFinite(value)) return isNaN(value) ? '.nan' : value < 0 ? '-.inf' : '.inf' diff --git a/src/stringify/stringifyString.d.ts b/src/stringify/stringifyString.d.ts new file mode 100644 index 00000000..4aec8bc5 --- /dev/null +++ b/src/stringify/stringifyString.d.ts @@ -0,0 +1,9 @@ +import { Scalar } from '../ast/index' +import { Schema } from '../doc/Schema' + +export function stringifyString( + item: Scalar, + ctx: Schema.StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +): string diff --git a/src/types.js b/src/types.ts similarity index 57% rename from src/types.js rename to src/types.ts index d66b8d09..8163018d 100644 --- a/src/types.js +++ b/src/types.ts @@ -1,11 +1,4 @@ -export { - binaryOptions, - boolOptions, - intOptions, - nullOptions, - strOptions -} from './tags/options.js' - +export * from './tags/options.js' export { Schema } from './doc/Schema.js' export { Alias, diff --git a/src/util.js b/src/util.ts similarity index 75% rename from src/util.js rename to src/util.ts index a2773d0d..71b95a53 100644 --- a/src/util.js +++ b/src/util.ts @@ -1,7 +1,5 @@ export { findPair, toJS } from './ast/index.js' - export { stringifyNumber } from './stringify/stringifyNumber.js' export { stringifyString } from './stringify/stringifyString.js' export { Type } from './constants.js' - -export { YAMLError, YAMLParseError, YAMLWarning } from './errors.js' +export * from './errors.js' diff --git a/types.d.ts b/types.d.ts index 36afbaa3..64a93ddf 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,3 +1 @@ -export * from './dist/ast/index' -export { Schema } from './dist/doc/Schema' -export * from './dist/tags/options' +export * from './dist/types' diff --git a/util.d.ts b/util.d.ts index 62a6690f..be0bfa88 100644 --- a/util.d.ts +++ b/util.d.ts @@ -1,16 +1 @@ -import { Pair, Scalar } from './dist/ast/index' -import { Schema } from './dist/doc/Schema' - -export { ToJSContext, toJS } from './dist/ast/toJS' -export { Type } from './dist/constants' -export * from './dist/errors' - -export function findPair(items: any[], key: Scalar | any): Pair | undefined - -export function stringifyNumber(item: Scalar): string -export function stringifyString( - item: Scalar, - ctx: Schema.StringifyContext, - onComment?: () => void, - onChompKeep?: () => void -): string +export * from './dist/util' From 4297c4e3cbab05148e40fc27f9b9aef68c51473d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 12:46:43 +0200 Subject: [PATCH 02/24] Refactor stringify internals as TypeScript --- src/doc/Schema.d.ts | 8 +- .../{addComment.js => addComment.ts} | 8 +- .../{foldFlowLines.js => foldFlowLines.ts} | 82 +++++++++++++------ src/stringify/stringifyString.d.ts | 9 -- ...{stringifyString.js => stringifyString.ts} | 43 +++++++--- 5 files changed, 98 insertions(+), 52 deletions(-) rename src/stringify/{addComment.js => addComment.ts} (65%) rename src/stringify/{foldFlowLines.js => foldFlowLines.ts} (75%) delete mode 100644 src/stringify/stringifyString.d.ts rename src/stringify/{stringifyString.js => stringifyString.ts} (90%) diff --git a/src/doc/Schema.d.ts b/src/doc/Schema.d.ts index c60ae6e8..dad45574 100644 --- a/src/doc/Schema.d.ts +++ b/src/doc/Schema.d.ts @@ -1,4 +1,5 @@ import { Node, Pair, Scalar, YAMLMap, YAMLSeq } from '../ast' +import type { Document } from './Document' export class Schema { constructor(options: Schema.Options) @@ -53,16 +54,17 @@ export namespace Schema { interface CreateNodeContext { wrapScalars?: boolean - [key: string]: any + [key: string]: unknown } interface StringifyContext { + doc: Document.Parsed forceBlockIndent?: boolean implicitKey?: boolean - indent?: string + indent: string indentAtStart?: number inFlow?: boolean - [key: string]: any + [key: string]: unknown } type TagId = diff --git a/src/stringify/addComment.js b/src/stringify/addComment.ts similarity index 65% rename from src/stringify/addComment.js rename to src/stringify/addComment.ts index 5d221b90..ed2273de 100644 --- a/src/stringify/addComment.js +++ b/src/stringify/addComment.ts @@ -1,10 +1,14 @@ -export function addCommentBefore(str, indent, comment) { +export function addCommentBefore( + str: string, + indent: string, + comment?: string +) { if (!comment) return str const cc = comment.replace(/[\s\S]^/gm, `$&${indent}#`) return `#${cc}\n${indent}${str}` } -export function addComment(str, indent, comment) { +export function addComment(str: string, indent: string, comment?: string) { return !comment ? str : comment.includes('\n') diff --git a/src/stringify/foldFlowLines.js b/src/stringify/foldFlowLines.ts similarity index 75% rename from src/stringify/foldFlowLines.js rename to src/stringify/foldFlowLines.ts index 8d1e5613..ffa7c12b 100644 --- a/src/stringify/foldFlowLines.js +++ b/src/stringify/foldFlowLines.ts @@ -2,17 +2,35 @@ export const FOLD_FLOW = 'flow' export const FOLD_BLOCK = 'block' export const FOLD_QUOTED = 'quoted' -// presumes i+1 is at the start of a line -// returns index of last newline in more-indented block -const consumeMoreIndentedLines = (text, i) => { - let ch = text[i + 1] - while (ch === ' ' || ch === '\t') { - do { - ch = text[(i += 1)] - } while (ch && ch !== '\n') - ch = text[i + 1] - } - return i +/** + * `'block'` prevents more-indented lines from being folded; + * `'quoted'` allows for `\` escapes, including escaped newlines + */ +export type FoldMode = 'flow' | 'block' | 'quoted' + +export interface FoldOptions { + /** + * Accounts for leading contents on the first line, defaulting to + * `indent.length` + */ + indentAtStart?: number + + /** Default: `80` */ + lineWidth?: number + + /** + * Allow highly indented lines to stretch the line width or indent content + * from the start. + * + * Default: `20` + */ + minContentWidth?: number + + /** Called once if the text is folded */ + onFold?: () => void + + /** Called once if any line of text exceeds lineWidth characters */ + onOverflow?: () => void } /** @@ -22,30 +40,25 @@ const consumeMoreIndentedLines = (text, i) => { * * @param {string} text * @param {string} indent - * @param {string} [mode='flow'] `'block'` prevents more-indented lines - * from being folded; `'quoted'` allows for `\` escapes, including escaped - * newlines * @param {Object} options - * @param {number} [options.indentAtStart] Accounts for leading contents on - * the first line, defaulting to `indent.length` - * @param {number} [options.lineWidth=80] - * @param {number} [options.minContentWidth=20] Allow highly indented lines to - * stretch the line width or indent content from the start - * @param {function} options.onFold Called once if the text is folded - * @param {function} options.onFold Called once if any line of text exceeds - * lineWidth characters */ export function foldFlowLines( - text, - indent, - mode, - { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } + text: string, + indent: string, + mode: FoldMode = 'flow', + { + indentAtStart, + lineWidth = 80, + minContentWidth = 20, + onFold, + onOverflow + }: FoldOptions = {} ) { if (!lineWidth || lineWidth < 0) return text const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length) if (text.length <= endStep) return text const folds = [] - const escapedFolds = {} + const escapedFolds: Record = {} let end = lineWidth - indent.length if (typeof indentAtStart === 'number') { if (indentAtStart > lineWidth - Math.max(2, minContentWidth)) folds.push(0) @@ -137,3 +150,18 @@ export function foldFlowLines( } return res } + +/** + * Presumes `i + 1` is at the start of a line + * @returns index of last newline in more-indented block + */ +function consumeMoreIndentedLines(text: string, i: number) { + let ch = text[i + 1] + while (ch === ' ' || ch === '\t') { + do { + ch = text[(i += 1)] + } while (ch && ch !== '\n') + ch = text[i + 1] + } + return i +} diff --git a/src/stringify/stringifyString.d.ts b/src/stringify/stringifyString.d.ts deleted file mode 100644 index 4aec8bc5..00000000 --- a/src/stringify/stringifyString.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Scalar } from '../ast/index' -import { Schema } from '../doc/Schema' - -export function stringifyString( - item: Scalar, - ctx: Schema.StringifyContext, - onComment?: () => void, - onChompKeep?: () => void -): string diff --git a/src/stringify/stringifyString.js b/src/stringify/stringifyString.ts similarity index 90% rename from src/stringify/stringifyString.js rename to src/stringify/stringifyString.ts index 62b6c183..08d97734 100644 --- a/src/stringify/stringifyString.js +++ b/src/stringify/stringifyString.ts @@ -1,23 +1,29 @@ -import { addCommentBefore } from './addComment.js' +import type { Scalar } from '../ast/index.js' import { Type } from '../constants.js' +import type { Schema } from '../doc/Schema.js' +import { strOptions } from '../tags/options.js' +import { addCommentBefore } from './addComment.js' import { foldFlowLines, FOLD_BLOCK, FOLD_FLOW, FOLD_QUOTED } from './foldFlowLines.js' -import { strOptions } from '../tags/options.js' -const getFoldOptions = ({ indentAtStart }) => +interface StringifyScalar extends Scalar { + value: string +} + +const getFoldOptions = ({ indentAtStart }: Schema.StringifyContext) => indentAtStart ? Object.assign({ indentAtStart }, strOptions.fold) : strOptions.fold // Also checks for lines starting with %, as parsing the output as YAML 1.1 will // presume that's starting a new document. -const containsDocumentMarker = str => /^(%|---|\.\.\.)/m.test(str) +const containsDocumentMarker = (str: string) => /^(%|---|\.\.\.)/m.test(str) -function lineLengthOverLimit(str, limit) { +function lineLengthOverLimit(str: string, limit: number) { const strLen = str.length if (strLen <= limit) return false for (let i = 0, start = 0; i < strLen; ++i) { @@ -30,7 +36,7 @@ function lineLengthOverLimit(str, limit) { return true } -function doubleQuotedString(value, ctx) { +function doubleQuotedString(value: string, ctx: Schema.StringifyContext) { const { implicitKey } = ctx const { jsonEncoding, minMultiLineLength } = strOptions.doubleQuoted const json = JSON.stringify(value) @@ -120,7 +126,7 @@ function doubleQuotedString(value, ctx) { : foldFlowLines(str, indent, FOLD_QUOTED, getFoldOptions(ctx)) } -function singleQuotedString(value, ctx) { +function singleQuotedString(value: string, ctx: Schema.StringifyContext) { if (ctx.implicitKey) { if (/\n/.test(value)) return doubleQuotedString(value, ctx) } else { @@ -135,7 +141,12 @@ function singleQuotedString(value, ctx) { : foldFlowLines(res, indent, FOLD_FLOW, getFoldOptions(ctx)) } -function blockString({ comment, type, value }, ctx, onComment, onChompKeep) { +function blockString( + { comment, type, value }: StringifyScalar, + ctx: Schema.StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +) { // 1. Block can't end in whitespace unless the last line is non-empty. // 2. Strings consisting of only whitespace are best rendered explicitly. if (/\n[\t ]+$/.test(value) || /^\s*$/.test(value)) { @@ -203,7 +214,12 @@ function blockString({ comment, type, value }, ctx, onComment, onChompKeep) { return `${header}\n${indent}${body}` } -function plainString(item, ctx, onComment, onChompKeep) { +function plainString( + item: StringifyScalar, + ctx: Schema.StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +) { const { comment, type, value } = item const { actualString, implicitKey, indent, inFlow } = ctx if ( @@ -281,7 +297,12 @@ function plainString(item, ctx, onComment, onChompKeep) { return body } -export function stringifyString(item, ctx, onComment, onChompKeep) { +export function stringifyString( + item: Scalar, + ctx: Schema.StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +) { const { defaultKeyType, defaultType } = strOptions const { implicitKey, inFlow } = ctx let { type, value } = item @@ -295,7 +316,7 @@ export function stringifyString(item, ctx, onComment, onChompKeep) { type = Type.QUOTE_DOUBLE } - const _stringify = _type => { + const _stringify = (_type: Type | undefined) => { switch (_type) { case Type.BLOCK_FOLDED: case Type.BLOCK_LITERAL: From 1082a5f9ab0df0acbb0713bdb633377f701c90a0 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 14:12:04 +0200 Subject: [PATCH 03/24] Resolve inconsistency between Babel & tsc output for TS --- babel.config.js | 26 ++++-- package-lock.json | 201 +++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 3 files changed, 199 insertions(+), 30 deletions(-) diff --git a/babel.config.js b/babel.config.js index dacf4db7..0878688b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,13 +1,23 @@ module.exports = { - plugins: [ - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-nullish-coalescing-operator', - ['babel-plugin-trace', { strip: true }] + overrides: [ + { + test: /\.js$/, + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-nullish-coalescing-operator', + ['babel-plugin-trace', { strip: true }] + ] + }, + { + test: /\.ts$/, + plugins: [ + ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-nullish-coalescing-operator' + ] + } ] } if (process.env.NODE_ENV === 'test') - module.exports.presets = [ - ['@babel/env', { targets: { node: 'current' } }], - '@babel/preset-typescript' - ] + module.exports.presets = [['@babel/env', { targets: { node: 'current' } }]] diff --git a/package-lock.json b/package-lock.json index 32a2da98..2b11cda8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1138,12 +1138,20 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz", - "integrity": "sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + } } }, "@babel/plugin-transform-arrow-functions": { @@ -1441,14 +1449,176 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", - "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.17.tgz", + "integrity": "sha512-1bIYwnhRoetxkFonuZRtDZPFEjl1l5r+3ITkxLC3mlMaFja+GQFo94b/WHEPjqWLU9Bc+W4oFZbvCGe9eYMu1g==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-typescript": "^7.12.1" + "@babel/helper-create-class-features-plugin": "^7.12.17", + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-typescript": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.17.tgz", + "integrity": "sha512-DSA7ruZrY4WI8VxuS1jWSRezFnghEoYEFrZcw9BizQRmOZiUsiHl59+qEARGPqPikwA/GPTyRCi7isuCK/oyqg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.17", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.17.tgz", + "integrity": "sha512-I/nurmTxIxHV0M+rIpfQBF1oN342+yvl2kwZUrQuOClMamHF1w5tknfZubgNOLRoA73SzBFAdFcpb4M9HwOeWQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-member-expression-to-functions": "^7.12.17", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.17.tgz", + "integrity": "sha512-Bzv4p3ODgS/qpBE0DiJ9qf5WxSmrQ8gVTe8ClMfwwsY2x/rhykxxy3bXzG7AGTnPB2ij37zGJ/Q/6FruxHxsxg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.17" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", + "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.17.tgz", + "integrity": "sha512-r1yKkiUTYMQ8LiEI0UcQx5ETw5dpTLn9wijn9hk6KkTtOK95FndDN10M+8/s6k/Ymlbivw0Av9q4SlgF80PtHg==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.17.tgz", + "integrity": "sha512-LGkTqDqdiwC6Q7fWSwQoas/oyiEYw6Hqjve5KOSykXkmFJFqzvGMb9niaUEag3Rlve492Mkye3gLw9FTv94fdQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.17", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.12.17", + "@babel/types": "^7.12.17", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.17.tgz", + "integrity": "sha512-tNMDjcv/4DIcHxErTgwB9q2ZcYyN0sUfgGKUK/mm1FJK7Wz+KstoEekxrl/tBiNDgLK1HGi+sppj1An/1DR4fQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-unicode-escapes": { @@ -1576,17 +1746,6 @@ "esutils": "^2.0.2" } }, - "@babel/preset-typescript": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz", - "integrity": "sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", - "@babel/plugin-transform-typescript": "^7.12.1" - } - }, "@babel/runtime": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", diff --git a/package.json b/package.json index 3bcd0d3d..39a099d2 100644 --- a/package.json +++ b/package.json @@ -80,8 +80,8 @@ "@babel/core": "^7.12.10", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-transform-typescript": "^7.12.17", "@babel/preset-env": "^7.12.11", - "@babel/preset-typescript": "^7.12.7", "@rollup/plugin-babel": "^5.2.3", "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-typescript": "^8.1.1", From bae8b4bbaafb2677be21c0ecc0a4d0291adc5b6a Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 14:14:38 +0200 Subject: [PATCH 04/24] Drop babel-plugin-trace as unused --- babel.config.js | 3 +-- package-lock.json | 6 ------ package.json | 1 - tests/yaml-test-suite.js | 6 ------ 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/babel.config.js b/babel.config.js index 0878688b..7565f04c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,8 +4,7 @@ module.exports = { test: /\.js$/, plugins: [ '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-nullish-coalescing-operator', - ['babel-plugin-trace', { strip: true }] + '@babel/plugin-proposal-nullish-coalescing-operator' ] }, { diff --git a/package-lock.json b/package-lock.json index 2b11cda8..ec68b3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2869,12 +2869,6 @@ "@types/babel__traverse": "^7.0.6" } }, - "babel-plugin-trace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-trace/-/babel-plugin-trace-1.1.0.tgz", - "integrity": "sha512-joLw8IjwmBNcvQKQsJOxNTI7pXN8ipKU1HeZt7DeyIXPgZCd0EuDIxyZPv01PjKQsW8IbSLDZGZ4zafQXI+6Hw==", - "dev": true - }, "babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", diff --git a/package.json b/package.json index 39a099d2..5a661092 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "@types/jest": "^26.0.20", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", - "babel-plugin-trace": "^1.1.0", "common-tags": "^1.8.0", "cross-env": "^7.0.3", "eslint": "^7.19.0", diff --git a/tests/yaml-test-suite.js b/tests/yaml-test-suite.js index 580bdbd4..0977cf92 100644 --- a/tests/yaml-test-suite.js +++ b/tests/yaml-test-suite.js @@ -102,12 +102,6 @@ testDirs.forEach(dir => { const src2 = docs.map(doc => String(doc).replace(/\n$/, '')).join('\n...\n') + '\n' const docs2 = YAML.parseAllDocuments(src2, { resolveKnownTags: false }) - trace: name, - '\nIN\n' + yaml, - '\nJSON\n' + JSON.stringify(docs[0], null, ' '), - '\n\nOUT\n' + src2, - '\nOUT-JSON\n' + JSON.stringify(src2), - '\nRE-JSON\n' + JSON.stringify(docs2[0], null, ' ') if (json) _test('stringfy+re-parse', () => matchJson(docs2, json)) From f4a74af84d1a39fbf8ce25a82ed33874c5fd73d6 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 16:17:36 +0200 Subject: [PATCH 05/24] Refactor more internals as TS: Node, Scalar, Collection, createNode --- rollup.node-config.js | 3 +- src/ast/Collection.js | 209 ---------------- src/ast/Collection.ts | 294 +++++++++++++++++++++++ src/ast/Node.js | 1 - src/ast/Node.ts | 37 +++ src/ast/Scalar.js | 20 -- src/ast/Scalar.ts | 48 ++++ src/ast/YAMLMap.js | 2 +- src/ast/YAMLSeq.js | 2 +- src/ast/index.d.ts | 139 ++--------- src/ast/toJS.ts | 5 +- src/doc/Document.d.ts | 11 +- src/doc/Schema.d.ts | 24 +- src/doc/{createNode.js => createNode.ts} | 50 +++- src/doc/listTagNames.ts | 5 +- src/stringify/addComment.ts | 8 +- src/stringify/stringify.d.ts | 20 ++ src/stringify/stringifyString.ts | 14 +- src/visit.ts | 6 +- 19 files changed, 502 insertions(+), 396 deletions(-) delete mode 100644 src/ast/Collection.js create mode 100644 src/ast/Collection.ts delete mode 100644 src/ast/Node.js create mode 100644 src/ast/Node.ts delete mode 100644 src/ast/Scalar.js create mode 100644 src/ast/Scalar.ts rename src/doc/{createNode.js => createNode.ts} (51%) create mode 100644 src/stringify/stringify.d.ts diff --git a/rollup.node-config.js b/rollup.node-config.js index 8a2e638c..15fbe4d0 100644 --- a/rollup.node-config.js +++ b/rollup.node-config.js @@ -25,7 +25,8 @@ export default { targets: [ { src: 'src/*.d.ts', dest: 'dist' }, { src: 'src/ast/*.d.ts', dest: 'dist/ast' }, - { src: 'src/doc/*.d.ts', dest: 'dist/doc' } + { src: 'src/doc/*.d.ts', dest: 'dist/doc' }, + { src: 'src/stringify/*.d.ts', dest: 'dist/stringify' } ] }) ], diff --git a/src/ast/Collection.js b/src/ast/Collection.js deleted file mode 100644 index c7371275..00000000 --- a/src/ast/Collection.js +++ /dev/null @@ -1,209 +0,0 @@ -import { addComment } from '../stringify/addComment.js' -import { Type } from '../constants.js' -import { createNode } from '../doc/createNode.js' -import { Node } from './Node.js' -import { Scalar } from './Scalar.js' - -export function collectionFromPath(schema, path, value) { - let v = value - for (let i = path.length - 1; i >= 0; --i) { - const k = path[i] - if (Number.isInteger(k) && k >= 0) { - const a = [] - a[k] = v - v = a - } else { - const o = {} - Object.defineProperty(o, k, { - value: v, - writable: true, - enumerable: true, - configurable: true - }) - v = o - } - } - return createNode(v, null, { - onAlias() { - throw new Error('Repeated objects are not supported here') - }, - prevObjects: new Map(), - schema, - wrapScalars: false - }) -} - -// null, undefined, or an empty non-string iterable (e.g. []) -export const isEmptyPath = path => - path == null || - (typeof path === 'object' && path[Symbol.iterator]().next().done) - -export class Collection extends Node { - static maxFlowStringSingleLineLength = 60 - - items = [] - - constructor(schema) { - super() - Object.defineProperty(this, 'schema', { - value: schema, - configurable: true, - enumerable: false, - writable: true - }) - } - - addIn(path, value) { - if (isEmptyPath(path)) this.add(value) - else { - const [key, ...rest] = path - const node = this.get(key, true) - if (node instanceof Collection) node.addIn(rest, value) - else if (node === undefined && this.schema) - this.set(key, collectionFromPath(this.schema, rest, value)) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - } - - deleteIn([key, ...rest]) { - if (rest.length === 0) return this.delete(key) - const node = this.get(key, true) - if (node instanceof Collection) return node.deleteIn(rest) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - - getIn([key, ...rest], keepScalar) { - const node = this.get(key, true) - if (rest.length === 0) - return !keepScalar && node instanceof Scalar ? node.value : node - else - return node instanceof Collection - ? node.getIn(rest, keepScalar) - : undefined - } - - hasAllNullValues(allowScalar) { - return this.items.every(node => { - if (!node || node.type !== 'PAIR') return false - const n = node.value - return ( - n == null || - (allowScalar && - n instanceof Scalar && - n.value == null && - !n.commentBefore && - !n.comment && - !n.tag) - ) - }) - } - - hasIn([key, ...rest]) { - if (rest.length === 0) return this.has(key) - const node = this.get(key, true) - return node instanceof Collection ? node.hasIn(rest) : false - } - - setIn([key, ...rest], value) { - if (rest.length === 0) { - this.set(key, value) - } else { - const node = this.get(key, true) - if (node instanceof Collection) node.setIn(rest, value) - else if (node === undefined && this.schema) - this.set(key, collectionFromPath(this.schema, rest, value)) - else - throw new Error( - `Expected YAML collection at ${key}. Remaining path: ${rest}` - ) - } - } - - /* istanbul ignore next: overridden in implementations */ - toJSON() { - return null - } - - toString(ctx, { blockItem, flowChars, itemIndent }, onComment, onChompKeep) { - const { indent, indentStep, stringify } = ctx - const inFlow = - this.type === Type.FLOW_MAP || this.type === Type.FLOW_SEQ || ctx.inFlow - if (inFlow) itemIndent += indentStep - ctx = Object.assign({}, ctx, { indent: itemIndent, inFlow, type: null }) - let chompKeep = false - let hasItemWithNewLine = false - const nodes = this.items.reduce((nodes, item, i) => { - let comment - if (item) { - if (!chompKeep && item.spaceBefore) - nodes.push({ type: 'comment', str: '' }) - - if (item.commentBefore) - item.commentBefore.match(/^.*$/gm).forEach(line => { - nodes.push({ type: 'comment', str: `#${line}` }) - }) - - if (item.comment) comment = item.comment - - if ( - inFlow && - ((!chompKeep && item.spaceBefore) || - item.commentBefore || - item.comment || - (item.key && (item.key.commentBefore || item.key.comment)) || - (item.value && (item.value.commentBefore || item.value.comment))) - ) - hasItemWithNewLine = true - } - chompKeep = false - let str = stringify( - item, - ctx, - () => (comment = null), - () => (chompKeep = true) - ) - if (inFlow && !hasItemWithNewLine && str.includes('\n')) - hasItemWithNewLine = true - if (inFlow && i < this.items.length - 1) str += ',' - str = addComment(str, itemIndent, comment) - if (chompKeep && (comment || inFlow)) chompKeep = false - nodes.push({ type: 'item', str }) - return nodes - }, []) - let str - if (nodes.length === 0) { - str = flowChars.start + flowChars.end - } else if (inFlow) { - const { start, end } = flowChars - const strings = nodes.map(n => n.str) - if ( - hasItemWithNewLine || - strings.reduce((sum, str) => sum + str.length + 2, 2) > - Collection.maxFlowStringSingleLineLength - ) { - str = start - for (const s of strings) { - str += s ? `\n${indentStep}${indent}${s}` : '\n' - } - str += `\n${indent}${end}` - } else { - str = `${start} ${strings.join(' ')} ${end}` - } - } else { - const strings = nodes.map(blockItem) - str = strings.shift() - for (const s of strings) str += s ? `\n${indent}${s}` : '\n' - } - if (this.comment) { - str += '\n' + this.comment.replace(/^/gm, `${indent}#`) - if (onComment) onComment() - } else if (chompKeep && onChompKeep) onChompKeep() - return str - } -} diff --git a/src/ast/Collection.ts b/src/ast/Collection.ts new file mode 100644 index 00000000..05dffed2 --- /dev/null +++ b/src/ast/Collection.ts @@ -0,0 +1,294 @@ +import { Type } from '../constants.js' +import { createNode } from '../doc/createNode.js' +import type { Schema } from '../doc/Schema.js' +import { addComment } from '../stringify/addComment.js' +import type { StringifyContext } from '../stringify/stringify.js' + +import { Node } from './Node.js' +import { Scalar } from './Scalar.js' +import type { Pair } from './index.js' // FIXME + +export function collectionFromPath( + schema: Schema, + path: unknown[], + value: unknown +) { + let v = value + for (let i = path.length - 1; i >= 0; --i) { + const k = path[i] + if (typeof k === 'number' && Number.isInteger(k) && k >= 0) { + const a: unknown[] = [] + a[k] = v + v = a + } else { + const o = {} + Object.defineProperty(o, typeof k === 'symbol' ? k : String(k), { + value: v, + writable: true, + enumerable: true, + configurable: true + }) + v = o + } + } + return createNode(v, null, { + onAlias() { + throw new Error('Repeated objects are not supported here') + }, + prevObjects: new Map(), + schema, + wrapScalars: false + }) +} + +// null, undefined, or an empty non-string iterable (e.g. []) +export const isEmptyPath = (path: Iterable | null | undefined) => + path == null || + (typeof path === 'object' && !!path[Symbol.iterator]().next().done) + +export declare namespace Collection { + interface StringifyContext { + blockItem(node: StringifyNode): string + flowChars: { start: '{' | '['; end: '}' | ']' } + itemIndent: string + } + + interface StringifyNode { + type: 'comment' | 'item' + str: string + } +} +export abstract class Collection extends Node { + static maxFlowStringSingleLineLength = 60 + + items: unknown[] = [] + schema: Schema | undefined + + declare type?: + | Type.MAP + | Type.FLOW_MAP + | Type.SEQ + | Type.FLOW_SEQ + | Type.DOCUMENT + + constructor(schema?: Schema) { + super() + Object.defineProperty(this, 'schema', { + value: schema, + configurable: true, + enumerable: false, + writable: true + }) + } + + /** + * Adds a value to the collection. For `!!map` and `!!omap` the value must + * be a Pair instance or a `{ key, value }` object, which may not have a key + * that already exists in the map. + */ + abstract add(value: unknown): void + + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + abstract delete(key: unknown): boolean + + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + abstract get(key: unknown, keepScalar?: boolean): unknown + + /** + * Checks if the collection includes a value with the key `key`. + */ + abstract has(key: unknown): boolean + + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + abstract set(key: unknown, value: unknown): void + + /** + * Adds a value to the collection. For `!!map` and `!!omap` the value must + * be a Pair instance or a `{ key, value }` object, which may not have a key + * that already exists in the map. + */ + addIn(path: Iterable, value: unknown) { + if (isEmptyPath(path)) this.add(value) + else { + const [key, ...rest] = path + const node = this.get(key, true) + if (node instanceof Collection) node.addIn(rest, value) + else if (node === undefined && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)) + else + throw new Error( + `Expected YAML collection at ${key}. Remaining path: ${rest}` + ) + } + } + + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + deleteIn([key, ...rest]: Iterable): boolean { + if (rest.length === 0) return this.delete(key) + const node = this.get(key, true) + if (node instanceof Collection) return node.deleteIn(rest) + else + throw new Error( + `Expected YAML collection at ${key}. Remaining path: ${rest}` + ) + } + + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn([key, ...rest]: Iterable, keepScalar?: boolean): unknown { + const node = this.get(key, true) + if (rest.length === 0) + return !keepScalar && node instanceof Scalar ? node.value : node + else + return node instanceof Collection + ? node.getIn(rest, keepScalar) + : undefined + } + + hasAllNullValues(allowScalar?: boolean) { + return this.items.every(node => { + if (!node || (node as Node).type !== 'PAIR') return false + const n = (node as Pair).value + return ( + n == null || + (allowScalar && + n instanceof Scalar && + n.value == null && + !n.commentBefore && + !n.comment && + !n.tag) + ) + }) + } + + /** + * Checks if the collection includes a value with the key `key`. + */ + hasIn([key, ...rest]: Iterable): boolean { + if (rest.length === 0) return this.has(key) + const node = this.get(key, true) + return node instanceof Collection ? node.hasIn(rest) : false + } + + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn([key, ...rest]: Iterable, value: unknown) { + if (rest.length === 0) { + this.set(key, value) + } else { + const node = this.get(key, true) + if (node instanceof Collection) node.setIn(rest, value) + else if (node === undefined && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)) + else + throw new Error( + `Expected YAML collection at ${key}. Remaining path: ${rest}` + ) + } + } + + _toString( + ctx: StringifyContext, + { blockItem, flowChars, itemIndent }: Collection.StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) { + const { indent, indentStep, stringify } = ctx + const inFlow = + this.type === Type.FLOW_MAP || this.type === Type.FLOW_SEQ || ctx.inFlow + if (inFlow) itemIndent += indentStep + ctx = Object.assign({}, ctx, { indent: itemIndent, inFlow, type: null }) + let chompKeep = false + let hasItemWithNewLine = false + const nodes = this.items.reduce( + (nodes: Collection.StringifyNode[], item, i) => { + let comment: string | null = null + if (item instanceof Node) { + if (!chompKeep && item.spaceBefore) + nodes.push({ type: 'comment', str: '' }) + + if (item.commentBefore) { + // This match will always succeed on a non-empty string + for (const line of item.commentBefore.match(/^.*$/gm) as string[]) + nodes.push({ type: 'comment', str: `#${line}` }) + } + + if (item.comment) comment = item.comment + + const pair = item as Pair + if ( + inFlow && + ((!chompKeep && item.spaceBefore) || + item.commentBefore || + item.comment || + (pair.key && (pair.key.commentBefore || pair.key.comment)) || + (pair.value && (pair.value.commentBefore || pair.value.comment))) + ) + hasItemWithNewLine = true + } + chompKeep = false + let str = stringify( + item, + ctx, + () => (comment = null), + () => (chompKeep = true) + ) + if (inFlow && !hasItemWithNewLine && str.includes('\n')) + hasItemWithNewLine = true + if (inFlow && i < this.items.length - 1) str += ',' + str = addComment(str, itemIndent, comment) + if (chompKeep && (comment || inFlow)) chompKeep = false + nodes.push({ type: 'item', str }) + return nodes + }, + [] + ) + let str + if (nodes.length === 0) { + str = flowChars.start + flowChars.end + } else if (inFlow) { + const { start, end } = flowChars + const strings = nodes.map(n => n.str) + if ( + hasItemWithNewLine || + strings.reduce((sum, str) => sum + str.length + 2, 2) > + Collection.maxFlowStringSingleLineLength + ) { + str = start + for (const s of strings) { + str += s ? `\n${indentStep}${indent}${s}` : '\n' + } + str += `\n${indent}${end}` + } else { + str = `${start} ${strings.join(' ')} ${end}` + } + } else { + const strings = nodes.map(blockItem) + str = strings.shift() + for (const s of strings) str += s ? `\n${indent}${s}` : '\n' + } + if (this.comment) { + str += '\n' + this.comment.replace(/^/gm, `${indent}#`) + if (onComment) onComment() + } else if (chompKeep && onChompKeep) onChompKeep() + return str + } +} diff --git a/src/ast/Node.js b/src/ast/Node.js deleted file mode 100644 index baa5f215..00000000 --- a/src/ast/Node.js +++ /dev/null @@ -1 +0,0 @@ -export class Node {} diff --git a/src/ast/Node.ts b/src/ast/Node.ts new file mode 100644 index 00000000..842e6f09 --- /dev/null +++ b/src/ast/Node.ts @@ -0,0 +1,37 @@ +import type { Type } from '../constants' +import type { Pair } from '.' + +export declare namespace Node { + interface Parsed extends Node { + range: [number, number] + } +} + +export abstract class Node { + /** A comment on or immediately after this */ + declare comment?: string | null + + /** A comment before this */ + declare commentBefore?: string | null + + /** Only available when `keepCstNodes` is set to `true` */ + // cstNode?: CST.Node + + /** + * The [start, end] range of characters of the source parsed + * into this node (undefined for pairs or if not parsed) + */ + declare range?: [number, number] | null + + /** A blank line before this node and its commentBefore */ + declare spaceBefore?: boolean + + /** A fully qualified tag, if required */ + declare tag?: string + + /** A plain JS representation of this node */ + abstract toJSON(arg?: any): any + + /** The type of this node */ + declare type?: Type | Pair.Type +} diff --git a/src/ast/Scalar.js b/src/ast/Scalar.js deleted file mode 100644 index 66ac4f84..00000000 --- a/src/ast/Scalar.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Node } from './Node.js' -import { toJS } from './toJS.js' - -export const isScalarValue = value => - !value || (typeof value !== 'function' && typeof value !== 'object') - -export class Scalar extends Node { - constructor(value) { - super() - this.value = value - } - - toJSON(arg, ctx) { - return ctx && ctx.keep ? this.value : toJS(this.value, arg, ctx) - } - - toString() { - return String(this.value) - } -} diff --git a/src/ast/Scalar.ts b/src/ast/Scalar.ts new file mode 100644 index 00000000..1f144346 --- /dev/null +++ b/src/ast/Scalar.ts @@ -0,0 +1,48 @@ +import { Type } from '../constants.js' +import { Node } from './Node.js' +import { toJS, ToJSContext } from './toJS.js' + +export const isScalarValue = (value: unknown) => + !value || (typeof value !== 'function' && typeof value !== 'object') + +export declare namespace Scalar { + interface Parsed extends Scalar { + range: [number, number] + source: string + } + + type Type = + | Type.BLOCK_FOLDED + | Type.BLOCK_LITERAL + | Type.PLAIN + | Type.QUOTE_DOUBLE + | Type.QUOTE_SINGLE +} + +export class Scalar extends Node { + value: any + + declare type?: Scalar.Type + + /** + * By default (undefined), numbers use decimal notation. + * The YAML 1.2 core schema only supports 'HEX' and 'OCT'. + * The YAML 1.1 schema also supports 'BIN' and 'TIME' + */ + declare format?: string + + declare minFractionDigits?: number + + constructor(value: unknown) { + super() + this.value = value + } + + toJSON(arg?: any, ctx?: ToJSContext) { + return ctx && ctx.keep ? this.value : toJS(this.value, arg, ctx) + } + + toString() { + return String(this.value) + } +} diff --git a/src/ast/YAMLMap.js b/src/ast/YAMLMap.js index 4d2f2553..66f529fc 100644 --- a/src/ast/YAMLMap.js +++ b/src/ast/YAMLMap.js @@ -82,7 +82,7 @@ export class YAMLMap extends Collection { } if (!ctx.allNullValues && this.hasAllNullValues(false)) ctx = Object.assign({}, ctx, { allNullValues: true }) - return super.toString( + return super._toString( ctx, { blockItem: n => n.str, diff --git a/src/ast/YAMLSeq.js b/src/ast/YAMLSeq.js index 410d333e..6ffb1291 100644 --- a/src/ast/YAMLSeq.js +++ b/src/ast/YAMLSeq.js @@ -55,7 +55,7 @@ export class YAMLSeq extends Collection { toString(ctx, onComment, onChompKeep) { if (!ctx) return JSON.stringify(this) - return super.toString( + return super._toString( ctx, { blockItem: n => (n.type === 'comment' ? n.str : `- ${n.str}`), diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts index 62015eba..eeb71b13 100644 --- a/src/ast/index.d.ts +++ b/src/ast/index.d.ts @@ -1,72 +1,23 @@ import { Type } from '../constants' -import { Document } from '../doc/Document' -import { Schema } from '../doc/Schema' +import type { StringifyContext } from '../stringify/stringify' +import { Collection } from './Collection' +import { Node } from './Node' +import { Scalar } from './Scalar' +import { ToJSContext } from './toJS' export { ToJSContext, toJS } from './toJS' -export function findPair(items: any[], key: Scalar | any): Pair | undefined - -export class Node { - /** A comment on or immediately after this */ - comment?: string | null - /** A comment before this */ - commentBefore?: string | null - /** Only available when `keepCstNodes` is set to `true` */ - // cstNode?: CST.Node - /** - * The [start, end] range of characters of the source parsed - * into this node (undefined for pairs or if not parsed) - */ - range?: [number, number] | null - /** A blank line before this node and its commentBefore */ - spaceBefore?: boolean - /** A fully qualified tag, if required */ - tag?: string - /** A plain JS representation of this node */ - toJSON(arg?: any): any - /** The type of this node */ - type?: Type | Pair.Type -} +export { Collection, Node, Scalar } -export namespace Node { - interface Parsed extends Node { - range: [number, number] - } -} - -export class Scalar extends Node { - constructor(value: any) - type?: Scalar.Type - /** - * By default (undefined), numbers use decimal notation. - * The YAML 1.2 core schema only supports 'HEX' and 'OCT'. - * The YAML 1.1 schema also supports 'BIN' and 'TIME' - */ - format?: string - minFractionDigits?: number - value: any - toJSON(arg?: any, ctx?: AST.NodeToJsonContext): any - toString(): string -} -export namespace Scalar { - interface Parsed extends Scalar { - range: [number, number] - source: string - } - type Type = - | Type.BLOCK_FOLDED - | Type.BLOCK_LITERAL - | Type.PLAIN - | Type.QUOTE_DOUBLE - | Type.QUOTE_SINGLE -} +export function findPair(items: any[], key: Scalar | any): Pair | undefined export class Alias extends Node { constructor(source: Node) type: Type.ALIAS source: Node // cstNode?: CST.Alias - toString(ctx: Schema.StringifyContext): string + toJSON(arg?: any, ctx?: ToJSContext): any + toString(ctx: StringifyContext): string } export namespace Alias { @@ -83,9 +34,9 @@ export class Pair extends Node { /** Always Node or null when parsed, but can be set to anything. */ value: any cstNode?: never // no corresponding cstNode - toJSON(arg?: any, ctx?: AST.NodeToJsonContext): object | Map + toJSON(arg?: any, ctx?: ToJSContext): object | Map toString( - ctx?: Schema.StringifyContext, + ctx?: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string @@ -105,58 +56,21 @@ export class Merge extends Pair { key: AST.PlainValue /** Always YAMLSeq, stringified as *A if length = 1 */ value: YAMLSeq - toString(ctx?: Schema.StringifyContext, onComment?: () => void): string -} - -export class Collection extends Node { - type?: Type.MAP | Type.FLOW_MAP | Type.SEQ | Type.FLOW_SEQ | Type.DOCUMENT - items: any[] - schema?: Schema - - constructor(schema?: Schema) - - /** - * Adds a value to the collection. For `!!map` and `!!omap` the value must - * be a Pair instance or a `{ key, value }` object, which may not have a key - * that already exists in the map. - */ - add(value: any): void - addIn(path: Iterable, value: any): void - /** - * Removes a value from the collection. - * @returns `true` if the item was found and removed. - */ - delete(key: any): boolean - deleteIn(path: Iterable): boolean - /** - * Returns item at `key`, or `undefined` if not found. By default unwraps - * scalar values from their surrounding node; to disable set `keepScalar` to - * `true` (collections are always returned intact). - */ - get(key: any, keepScalar?: boolean): any - getIn(path: Iterable, keepScalar?: boolean): any - /** - * Checks if the collection includes a value with the key `key`. - */ - has(key: any): boolean - hasIn(path: Iterable): boolean - /** - * Sets a value in this collection. For `!!set`, `value` needs to be a - * boolean to add/remove the item from the set. - */ - set(key: any, value: any): void - setIn(path: Iterable, value: any): void - - hasAllNullValues(allowScalar?: boolean): boolean + toString(ctx?: StringifyContext, onComment?: () => void): string } export class YAMLMap extends Collection { static readonly tagName: 'tag:yaml.org,2002:map' type?: Type.FLOW_MAP | Type.MAP items: Array - toJSON(arg?: any, ctx?: AST.NodeToJsonContext): object | Map + add(value: unknown): void + delete(key: unknown): boolean + get(key: unknown, keepScalar?: boolean): unknown + has(key: unknown): boolean + set(key: unknown, value: unknown): void + toJSON(arg?: any, ctx?: ToJSContext): object | Map toString( - ctx?: Schema.StringifyContext, + ctx?: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string @@ -171,13 +85,14 @@ export namespace YAMLMap { export class YAMLSeq extends Collection { static readonly tagName: 'tag:yaml.org,2002:seq' type?: Type.FLOW_SEQ | Type.SEQ + add(value: unknown): void delete(key: number | string | Scalar): boolean get(key: number | string | Scalar, keepScalar?: boolean): any has(key: number | string | Scalar): boolean set(key: number | string | Scalar, value: any): void - toJSON(arg?: any, ctx?: AST.NodeToJsonContext): any[] + toJSON(arg?: any, ctx?: ToJSContext): any[] toString( - ctx?: Schema.StringifyContext, + ctx?: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string @@ -191,16 +106,6 @@ export namespace YAMLSeq { } export namespace AST { - interface NodeToJsonContext { - anchors?: any[] - doc: Document - keep?: boolean - mapAsMap?: boolean - maxAliasCount?: number - onCreate?: (node: Node) => void - [key: string]: any - } - interface BlockFolded extends Scalar { type: Type.BLOCK_FOLDED // cstNode?: CST.BlockFolded diff --git a/src/ast/toJS.ts b/src/ast/toJS.ts index 951c788a..0508effc 100644 --- a/src/ast/toJS.ts +++ b/src/ast/toJS.ts @@ -28,10 +28,11 @@ export interface ToJSContext { * `{ keep: true }` is not set, output should be suitable for JSON * stringification. */ -export function toJS(value: any, arg: string | null, ctx: ToJSContext): any { +export function toJS(value: any, arg: string | null, ctx?: ToJSContext): any { if (Array.isArray(value)) return value.map((v, i) => toJS(v, String(i), ctx)) if (value && typeof value.toJSON === 'function') { - const anchor = ctx && ctx.anchors && ctx.anchors.get(value) + if (!ctx) return value.toJSON(arg) + const anchor = ctx.anchors && ctx.anchors.get(value) if (anchor) ctx.onCreate = res => { anchor.res = res diff --git a/src/doc/Document.d.ts b/src/doc/Document.d.ts index 91305178..f520fbc6 100644 --- a/src/doc/Document.d.ts +++ b/src/doc/Document.d.ts @@ -56,7 +56,10 @@ export class Document extends Collection { * initialise. */ options: Required - schema?: Schema + + // FIXME required by Collection, currently optional in Document + declare schema: Schema + /** * Array of prefixes; each will have a string `handle` that * starts and ends with `!` and a string `prefix` that the handle will be replaced by. @@ -70,6 +73,12 @@ export class Document extends Collection { /** Warnings encountered during parsing. */ warnings: YAMLWarning[] + add(value: unknown): void + delete(key: unknown): boolean + get(key: unknown, keepScalar?: boolean): unknown + has(key: unknown): boolean + set(key: unknown, value: unknown): void + /** * Convert any value into a `Node` using the current schema, recursively * turning objects into collections. diff --git a/src/doc/Schema.d.ts b/src/doc/Schema.d.ts index dad45574..b2522813 100644 --- a/src/doc/Schema.d.ts +++ b/src/doc/Schema.d.ts @@ -1,11 +1,14 @@ import { Node, Pair, Scalar, YAMLMap, YAMLSeq } from '../ast' -import type { Document } from './Document' +import { StringifyContext } from '../stringify/stringify' +import { CreateNodeContext } from './createNode' export class Schema { constructor(options: Schema.Options) knownTags: { [key: string]: Schema.Tag } merge: boolean + map: Schema.Tag name: Schema.Name + seq: Schema.Tag sortMapEntries: ((a: Pair, b: Pair) => number) | null tags: Schema.Tag[] } @@ -52,21 +55,6 @@ export namespace Schema { tags?: Options['customTags'] } - interface CreateNodeContext { - wrapScalars?: boolean - [key: string]: unknown - } - - interface StringifyContext { - doc: Document.Parsed - forceBlockIndent?: boolean - implicitKey?: boolean - indent: string - indentAtStart?: number - inFlow?: boolean - [key: string]: unknown - } - type TagId = | 'binary' | 'bool' @@ -91,7 +79,7 @@ export namespace Schema { createNode?: ( schema: Schema, value: any, - ctx: Schema.CreateNodeContext + ctx: CreateNodeContext ) => YAMLMap | YAMLSeq | Scalar /** * If `true`, together with `test` allows for values to be stringified without @@ -138,7 +126,7 @@ export namespace Schema { */ stringify?: ( item: Node, - ctx: Schema.StringifyContext, + ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ) => string diff --git a/src/doc/createNode.js b/src/doc/createNode.ts similarity index 51% rename from src/doc/createNode.js rename to src/doc/createNode.ts index 3daec963..1efe4be5 100644 --- a/src/doc/createNode.js +++ b/src/doc/createNode.ts @@ -1,8 +1,30 @@ +import { Alias } from '../ast/index.js' import { Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' import { defaultTagPrefix } from '../constants.js' +import type { Replacer } from './Document.js' +import type { Schema } from './Schema.js' -function findTagObject(value, tagName, tags) { +export interface CreateNodeAliasRef { + node: unknown + value: unknown +} + +export interface CreateNodeContext { + keepUndefined?: boolean + onAlias(source: CreateNodeAliasRef): Alias + onTagObj?: (tagObj: unknown) => void + prevObjects: Map + replacer?: Replacer + schema: Schema + wrapScalars: boolean +} + +function findTagObject( + value: unknown, + tagName: string | null, + tags: Schema.Tag[] +) { if (tagName) { const match = tags.filter(t => t.tag === tagName) const tagObj = match.find(t => !t.format) || match[0] @@ -12,7 +34,11 @@ function findTagObject(value, tagName, tags) { return tags.find(t => t.identify && t.identify(value) && !t.format) } -export function createNode(value, tagName, ctx) { +export function createNode( + value: unknown, + tagName: string | null, + ctx: CreateNodeContext +) { if (value instanceof Node) return value const { onAlias, onTagObj, prevObjects, wrapScalars } = ctx const { map, seq, tags } = ctx.schema @@ -21,10 +47,12 @@ export function createNode(value, tagName, ctx) { let tagObj = findTagObject(value, tagName, tags) if (!tagObj) { - if (typeof value.toJSON === 'function') value = value.toJSON() + if (value && typeof (value as any).toJSON === 'function') + value = (value as any).toJSON() if (!value || typeof value !== 'object') return wrapScalars ? new Scalar(value) : value - tagObj = value instanceof Map ? map : value[Symbol.iterator] ? seq : map + tagObj = + value instanceof Map ? map : Symbol.iterator in Object(value) ? seq : map } if (onTagObj) { onTagObj(tagObj) @@ -32,21 +60,21 @@ export function createNode(value, tagName, ctx) { } // Detect duplicate references to the same object & use Alias nodes for all - // after first. The `obj` wrapper allows for circular references to resolve. - const obj = { value: undefined, node: undefined } + // after first. The `ref` wrapper allows for circular references to resolve. + const ref: CreateNodeAliasRef = { value: undefined, node: undefined } if (value && typeof value === 'object') { const prev = prevObjects.get(value) if (prev) return onAlias(prev) - obj.value = value - prevObjects.set(value, obj) + ref.value = value + prevObjects.set(value, ref) } - obj.node = tagObj.createNode + ref.node = tagObj.createNode ? tagObj.createNode(ctx.schema, value, ctx) : wrapScalars ? new Scalar(value) : value - if (tagName && obj.node instanceof Node) obj.node.tag = tagName + if (tagName && ref.node instanceof Node) ref.node.tag = tagName - return obj.node + return ref.node } diff --git a/src/doc/listTagNames.ts b/src/doc/listTagNames.ts index d64d062b..41b0b02f 100644 --- a/src/doc/listTagNames.ts +++ b/src/doc/listTagNames.ts @@ -1,15 +1,16 @@ import { Collection, Node, Pair, Scalar } from '../ast/index.js' -function visit(node: Node, tags: Record) { +function visit(node: unknown, tags: Record) { if (node && typeof node === 'object') { - const { tag } = node if (node instanceof Collection) { + const { tag } = node if (tag) tags[tag] = true node.items.forEach(n => visit(n, tags)) } else if (node instanceof Pair) { visit(node.key, tags) visit(node.value, tags) } else if (node instanceof Scalar) { + const { tag } = node if (tag) tags[tag] = true } } diff --git a/src/stringify/addComment.ts b/src/stringify/addComment.ts index ed2273de..60b47450 100644 --- a/src/stringify/addComment.ts +++ b/src/stringify/addComment.ts @@ -1,14 +1,18 @@ export function addCommentBefore( str: string, indent: string, - comment?: string + comment?: string | null ) { if (!comment) return str const cc = comment.replace(/[\s\S]^/gm, `$&${indent}#`) return `#${cc}\n${indent}${str}` } -export function addComment(str: string, indent: string, comment?: string) { +export function addComment( + str: string, + indent: string, + comment?: string | null +) { return !comment ? str : comment.includes('\n') diff --git a/src/stringify/stringify.d.ts b/src/stringify/stringify.d.ts new file mode 100644 index 00000000..dad71656 --- /dev/null +++ b/src/stringify/stringify.d.ts @@ -0,0 +1,20 @@ +import type { Document } from '../doc/Document' + +export interface StringifyContext { + doc: Document.Parsed + forceBlockIndent?: boolean + implicitKey?: boolean + indent: string + indentStep: string + indentAtStart?: number + inFlow?: boolean + stringify: typeof stringify + [key: string]: unknown +} + +export function stringify( + item: unknown, + ctx: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +): string diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index 08d97734..a6332673 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -1,6 +1,5 @@ import type { Scalar } from '../ast/index.js' import { Type } from '../constants.js' -import type { Schema } from '../doc/Schema.js' import { strOptions } from '../tags/options.js' import { addCommentBefore } from './addComment.js' import { @@ -9,12 +8,13 @@ import { FOLD_FLOW, FOLD_QUOTED } from './foldFlowLines.js' +import type { StringifyContext } from './stringify.js' interface StringifyScalar extends Scalar { value: string } -const getFoldOptions = ({ indentAtStart }: Schema.StringifyContext) => +const getFoldOptions = ({ indentAtStart }: StringifyContext) => indentAtStart ? Object.assign({ indentAtStart }, strOptions.fold) : strOptions.fold @@ -36,7 +36,7 @@ function lineLengthOverLimit(str: string, limit: number) { return true } -function doubleQuotedString(value: string, ctx: Schema.StringifyContext) { +function doubleQuotedString(value: string, ctx: StringifyContext) { const { implicitKey } = ctx const { jsonEncoding, minMultiLineLength } = strOptions.doubleQuoted const json = JSON.stringify(value) @@ -126,7 +126,7 @@ function doubleQuotedString(value: string, ctx: Schema.StringifyContext) { : foldFlowLines(str, indent, FOLD_QUOTED, getFoldOptions(ctx)) } -function singleQuotedString(value: string, ctx: Schema.StringifyContext) { +function singleQuotedString(value: string, ctx: StringifyContext) { if (ctx.implicitKey) { if (/\n/.test(value)) return doubleQuotedString(value, ctx) } else { @@ -143,7 +143,7 @@ function singleQuotedString(value: string, ctx: Schema.StringifyContext) { function blockString( { comment, type, value }: StringifyScalar, - ctx: Schema.StringifyContext, + ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ) { @@ -216,7 +216,7 @@ function blockString( function plainString( item: StringifyScalar, - ctx: Schema.StringifyContext, + ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ) { @@ -299,7 +299,7 @@ function plainString( export function stringifyString( item: Scalar, - ctx: Schema.StringifyContext, + ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ) { diff --git a/src/visit.ts b/src/visit.ts index 3f7e1001..3870366c 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -12,7 +12,7 @@ export type visitorFn = ( ) => void | symbol | number | Node export type visitor = - | visitorFn + | visitorFn | { Alias?: visitorFn Map?: visitorFn @@ -51,7 +51,7 @@ export type visitor = export function visit( node: Node | Document, visitor: - | visitorFn + | visitorFn | { Alias?: visitorFn Map?: visitorFn @@ -81,7 +81,7 @@ visit.REMOVE = REMOVE as symbol function _visit( key: number | 'key' | 'value' | null, - node: Node, + node: unknown, visitor: visitor, path: readonly Node[] ): void | symbol | number | Node { From 0c2d254e7ae7ea75825b90e239031d0545280ced Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 18:43:27 +0200 Subject: [PATCH 06/24] Refactor collections as TS, adding generics --- src/ast/Collection.ts | 14 ++-- src/ast/{Merge.js => Merge.ts} | 46 +++++++---- src/ast/Node.ts | 13 ++- src/ast/{Pair.js => Pair.ts} | 125 +++++++++++++++++++---------- src/ast/{YAMLMap.js => YAMLMap.ts} | 75 +++++++++++------ src/ast/{YAMLSeq.js => YAMLSeq.ts} | 50 ++++++++---- src/ast/index.d.ts | 88 ++------------------ src/ast/index.js | 2 +- src/ast/toJS.ts | 4 +- src/compose/resolve-merge-pair.ts | 2 +- src/stringify/stringify.d.ts | 2 +- tests/doc/createNode.js | 8 +- tests/typings.ts | 7 +- 13 files changed, 229 insertions(+), 207 deletions(-) rename src/ast/{Merge.js => Merge.ts} (61%) rename src/ast/{Pair.js => Pair.ts} (79%) rename src/ast/{YAMLMap.js => YAMLMap.ts} (50%) rename src/ast/{YAMLSeq.js => YAMLSeq.ts} (61%) diff --git a/src/ast/Collection.ts b/src/ast/Collection.ts index 05dffed2..a551a14b 100644 --- a/src/ast/Collection.ts +++ b/src/ast/Collection.ts @@ -5,8 +5,8 @@ import { addComment } from '../stringify/addComment.js' import type { StringifyContext } from '../stringify/stringify.js' import { Node } from './Node.js' +import type { Pair } from './Pair.js' import { Scalar } from './Scalar.js' -import type { Pair } from './index.js' // FIXME export function collectionFromPath( schema: Schema, @@ -81,11 +81,7 @@ export abstract class Collection extends Node { }) } - /** - * Adds a value to the collection. For `!!map` and `!!omap` the value must - * be a Pair instance or a `{ key, value }` object, which may not have a key - * that already exists in the map. - */ + /** Adds a value to the collection. */ abstract add(value: unknown): void /** @@ -233,7 +229,7 @@ export abstract class Collection extends Node { if (item.comment) comment = item.comment - const pair = item as Pair + const pair = item as any // Apply guards manually in the following if ( inFlow && ((!chompKeep && item.spaceBefore) || @@ -261,7 +257,7 @@ export abstract class Collection extends Node { }, [] ) - let str + let str: string if (nodes.length === 0) { str = flowChars.start + flowChars.end } else if (inFlow) { @@ -282,7 +278,7 @@ export abstract class Collection extends Node { } } else { const strings = nodes.map(blockItem) - str = strings.shift() + str = strings.shift() || '' for (const s of strings) str += s ? `\n${indent}${s}` : '\n' } if (this.comment) { diff --git a/src/ast/Merge.js b/src/ast/Merge.ts similarity index 61% rename from src/ast/Merge.js rename to src/ast/Merge.ts index 7b2279bf..bad7e210 100644 --- a/src/ast/Merge.js +++ b/src/ast/Merge.ts @@ -1,25 +1,33 @@ -import { Pair } from './Pair.js' +import { StringifyContext } from '../stringify/stringify.js' +import type { Alias } from './index.js' +import { Node } from './Node.js' +import { Pair, PairType } from './Pair.js' import { Scalar } from './Scalar.js' +import type { ToJSContext } from './toJS.js' import { YAMLMap } from './YAMLMap.js' import { YAMLSeq } from './YAMLSeq.js' -export class Merge extends Pair { +export class Merge extends Pair> { static KEY = '<<' - constructor(pair) { - if (pair instanceof Pair) { - let seq = pair.value - if (!(seq instanceof YAMLSeq)) { - seq = new YAMLSeq() + type: PairType.MERGE_PAIR + + declare value: YAMLSeq + + constructor(pair?: Pair>) { + if (pair instanceof Pair && pair.value instanceof Node) { + if (pair.value instanceof YAMLSeq) super(pair.key, pair.value) + else { + const seq = new YAMLSeq() seq.items.push(pair.value) seq.range = pair.value.range + super(pair.key, seq) } - super(pair.key, seq) this.range = pair.range } else { super(new Scalar(Merge.KEY), new YAMLSeq()) } - this.type = Pair.Type.MERGE_PAIR + this.type = PairType.MERGE_PAIR } // If the value associated with a merge key is a single mapping node, each of @@ -29,7 +37,13 @@ export class Merge extends Pair { // of these nodes is merged in turn according to its order in the sequence. // Keys in mapping nodes earlier in the sequence override keys specified in // later mapping nodes. -- http://yaml.org/type/merge.html - addToJSMap(ctx, map) { + addToJSMap( + ctx: ToJSContext | undefined, + map: + | Map + | Set + | Record + ) { for (const { source } of this.value.items) { if (!(source instanceof YAMLMap)) throw new Error('Merge sources must be maps') @@ -52,12 +66,14 @@ export class Merge extends Pair { return map } - toString(ctx, onComment) { + toString(ctx?: StringifyContext, onComment?: () => void) { const seq = this.value if (seq.items.length > 1) return super.toString(ctx, onComment) - this.value = seq.items[0] - const str = super.toString(ctx, onComment) - this.value = seq - return str + try { + this.value = seq.items[0] as any + return super.toString(ctx, onComment) + } finally { + this.value = seq + } } } diff --git a/src/ast/Node.ts b/src/ast/Node.ts index 842e6f09..4b8bc5bb 100644 --- a/src/ast/Node.ts +++ b/src/ast/Node.ts @@ -1,5 +1,6 @@ import type { Type } from '../constants' -import type { Pair } from '.' +import { StringifyContext } from '../stringify/stringify' +import type { PairType } from './Pair' export declare namespace Node { interface Parsed extends Node { @@ -30,8 +31,14 @@ export abstract class Node { declare tag?: string /** A plain JS representation of this node */ - abstract toJSON(arg?: any): any + abstract toJSON(): any + + abstract toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ): string /** The type of this node */ - declare type?: Type | Pair.Type + declare type?: Type | PairType } diff --git a/src/ast/Pair.js b/src/ast/Pair.ts similarity index 79% rename from src/ast/Pair.js rename to src/ast/Pair.ts index 03f4e62e..3d755c98 100644 --- a/src/ast/Pair.js +++ b/src/ast/Pair.ts @@ -1,66 +1,61 @@ import { Type } from '../constants.js' -import { createNode } from '../doc/createNode.js' +import { createNode, CreateNodeContext } from '../doc/createNode.js' import { warn } from '../log.js' import { addComment } from '../stringify/addComment.js' +import { StringifyContext } from '../stringify/stringify.js' + import { Collection } from './Collection.js' import { Node } from './Node.js' import { Scalar } from './Scalar.js' import { YAMLSeq } from './YAMLSeq.js' -import { toJS } from './toJS.js' +import { toJS, ToJSContext } from './toJS.js' +import type { YAMLMap } from './index.js' -function stringifyKey(key, jsKey, ctx) { - if (jsKey === null) return '' - if (typeof jsKey !== 'object') return String(jsKey) - if (key instanceof Node && ctx && ctx.doc) { - const strKey = key.toString({ - anchors: Object.create(null), - doc: ctx.doc, - indent: '', - indentStep: ctx.indentStep, - inFlow: true, - inStringifyKey: true, - stringify: ctx.stringify - }) - if (!ctx.mapKeyWarned) { - let jsonStr = JSON.stringify(strKey) - if (jsonStr.length > 40) - jsonStr = jsonStr.split('').splice(36, '..."').join('') - warn( - ctx.doc.options.logLevel, - `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.` - ) - ctx.mapKeyWarned = true - } - return strKey - } - return JSON.stringify(jsKey) -} - -export function createPair(key, value, ctx) { +export function createPair( + key: unknown, + value: unknown, + ctx: CreateNodeContext +) { const k = createNode(key, null, ctx) const v = createNode(value, null, ctx) return new Pair(k, v) } -export class Pair extends Node { - static Type = { - PAIR: 'PAIR', - MERGE_PAIR: 'MERGE_PAIR' +export enum PairType { + PAIR = 'PAIR', + MERGE_PAIR = 'MERGE_PAIR' +} + +export declare namespace Pair { + interface Parsed extends Pair { + key: Scalar | YAMLMap | YAMLSeq + value: Scalar | YAMLMap | YAMLSeq | null } +} - constructor(key, value = null) { +export class Pair extends Node { + /** Always Node or null when parsed, but can be set to anything. */ + key: K + + /** Always Node or null when parsed, but can be set to anything. */ + value: V | null + + type: PairType + + constructor(key: K, value: V | null = null) { super() this.key = key this.value = value - this.type = Pair.Type.PAIR + this.type = PairType.PAIR } + // @ts-ignore This is fine. get commentBefore() { return this.key instanceof Node ? this.key.commentBefore : undefined } set commentBefore(cb) { - if (this.key == null) this.key = new Scalar(null) + if (this.key == null) this.key = new Scalar(null) as any // FIXME if (this.key instanceof Node) this.key.commentBefore = cb else { const msg = @@ -69,12 +64,13 @@ export class Pair extends Node { } } + // @ts-ignore This is fine. get spaceBefore() { return this.key instanceof Node ? this.key.spaceBefore : undefined } set spaceBefore(sb) { - if (this.key == null) this.key = new Scalar(null) + if (this.key == null) this.key = new Scalar(null) as any // FIXME if (this.key instanceof Node) this.key.spaceBefore = sb else { const msg = @@ -83,7 +79,13 @@ export class Pair extends Node { } } - addToJSMap(ctx, map) { + addToJSMap( + ctx: ToJSContext | undefined, + map: + | Map + | Set + | Record + ) { const key = toJS(this.key, '', ctx) if (map instanceof Map) { const value = toJS(this.value, key, ctx) @@ -105,16 +107,20 @@ export class Pair extends Node { return map } - toJSON(_, ctx) { + toJSON(_?: unknown, ctx?: ToJSContext) { const pair = ctx && ctx.mapAsMap ? new Map() : {} return this.addToJSMap(ctx, pair) } - toString(ctx, onComment, onChompKeep) { + toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) { if (!ctx || !ctx.doc) return JSON.stringify(this) const { indent: indentSize, indentSeq, simpleKeys } = ctx.doc.options - let { key, value } = this - let keyComment = key instanceof Node && key.comment + let { key, value }: { key: K; value: V | Node | null } = this + let keyComment = (key instanceof Node && key.comment) || null if (simpleKeys) { if (keyComment) { throw new Error('With simple keys, key nodes cannot have comments') @@ -224,3 +230,34 @@ export class Pair extends Node { return addComment(str + ws + valueStr, ctx.indent, valueComment) } } + +function stringifyKey( + key: unknown, + jsKey: unknown, + ctx: ToJSContext | undefined +) { + if (jsKey === null) return '' + if (typeof jsKey !== 'object') return String(jsKey) + if (key instanceof Node && ctx && ctx.doc) { + const strKey = key.toString({ + anchors: Object.create(null), + doc: ctx.doc, + indent: '', + indentStep: ctx.indentStep, + inFlow: true, + inStringifyKey: true, + stringify: ctx.stringify + }) + if (!ctx.mapKeyWarned) { + let jsonStr = JSON.stringify(strKey) + if (jsonStr.length > 40) jsonStr = jsonStr.substring(0, 36) + '..."' + warn( + ctx.doc.options.logLevel, + `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.` + ) + ctx.mapKeyWarned = true + } + return strKey + } + return JSON.stringify(jsKey) +} diff --git a/src/ast/YAMLMap.js b/src/ast/YAMLMap.ts similarity index 50% rename from src/ast/YAMLMap.js rename to src/ast/YAMLMap.ts index 66f529fc..a1344c52 100644 --- a/src/ast/YAMLMap.js +++ b/src/ast/YAMLMap.ts @@ -1,62 +1,86 @@ +import { Type } from '../constants.js' +import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' import { Pair } from './Pair.js' import { Scalar, isScalarValue } from './Scalar.js' +import { ToJSContext } from './toJS.js' -export function findPair(items, key) { +export function findPair(items: Iterable, key: unknown) { const k = key instanceof Scalar ? key.value : key for (const it of items) { if (it instanceof Pair) { if (it.key === key || it.key === k) return it - if (it.key && it.key.value === k) return it + if (it.key instanceof Scalar && it.key.value === k) return it } } return undefined } -export class YAMLMap extends Collection { - static get tagName() { +export declare namespace YAMLMap { + interface Parsed extends YAMLMap { + items: Pair.Parsed[] + range: [number, number] + } +} + +export class YAMLMap extends Collection { + static get tagName(): 'tag:yaml.org,2002:map' { return 'tag:yaml.org,2002:map' } - add(pair, overwrite) { - if (!pair) pair = new Pair(pair) - else if (!(pair instanceof Pair)) - pair = new Pair(pair.key || pair, pair.value) - const prev = findPair(this.items, pair.key) + type?: Type.FLOW_MAP | Type.MAP + + items: Pair[] = [] + + /** + * Adds a value to the collection. + * + * @param overwrite - If not set `true`, using a key that is already in the + * collection will throw. Otherwise, overwrites the previous value. + */ + add(pair: Pair | { key: K; value: V }, overwrite?: boolean) { + let _pair: Pair + if (pair instanceof Pair) _pair = pair + else if (!pair || typeof pair !== 'object' || !('key' in pair)) { + // In TypeScript, this never happens. + _pair = new Pair(pair as any, (pair as any).value) + } else _pair = new Pair(pair.key, pair.value) + + const prev = findPair(this.items, _pair.key) const sortEntries = this.schema && this.schema.sortMapEntries if (prev) { - if (!overwrite) throw new Error(`Key ${pair.key} already set`) + if (!overwrite) throw new Error(`Key ${_pair.key} already set`) // For scalars, keep the old node & its comments and anchors - if (prev.value instanceof Scalar && isScalarValue(pair.value)) - prev.value.value = pair.value - else prev.value = pair.value + if (prev.value instanceof Scalar && isScalarValue(_pair.value)) + prev.value.value = _pair.value + else prev.value = _pair.value } else if (sortEntries) { - const i = this.items.findIndex(item => sortEntries(pair, item) < 0) - if (i === -1) this.items.push(pair) - else this.items.splice(i, 0, pair) + const i = this.items.findIndex(item => sortEntries(_pair, item) < 0) + if (i === -1) this.items.push(_pair) + else this.items.splice(i, 0, _pair) } else { - this.items.push(pair) + this.items.push(_pair) } } - delete(key) { + delete(key: K) { const it = findPair(this.items, key) if (!it) return false const del = this.items.splice(this.items.indexOf(it), 1) return del.length > 0 } - get(key, keepScalar) { + get(key: K, keepScalar?: boolean) { const it = findPair(this.items, key) const node = it && it.value return !keepScalar && node instanceof Scalar ? node.value : node } - has(key) { + has(key: K) { return !!findPair(this.items, key) } - set(key, value) { + set(key: K, value: V) { this.add(new Pair(key, value), true) } @@ -65,14 +89,18 @@ export class YAMLMap extends Collection { * @param {Class} Type - If set, forces the returned collection type * @returns Instance of Type, Map, or Object */ - toJSON(_, ctx, Type) { + toJSON(_?: unknown, ctx?: ToJSContext, Type?: any) { const map = Type ? new Type() : ctx && ctx.mapAsMap ? new Map() : {} if (ctx && ctx.onCreate) ctx.onCreate(map) for (const item of this.items) item.addToJSMap(ctx, map) return map } - toString(ctx, onComment, onChompKeep) { + toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) { if (!ctx) return JSON.stringify(this) for (const item of this.items) { if (!(item instanceof Pair)) @@ -87,7 +115,6 @@ export class YAMLMap extends Collection { { blockItem: n => n.str, flowChars: { start: '{', end: '}' }, - isMap: true, itemIndent: ctx.indent || '' }, onComment, diff --git a/src/ast/YAMLSeq.js b/src/ast/YAMLSeq.ts similarity index 61% rename from src/ast/YAMLSeq.js rename to src/ast/YAMLSeq.ts index 6ffb1291..1c59ba82 100644 --- a/src/ast/YAMLSeq.js +++ b/src/ast/YAMLSeq.ts @@ -1,42 +1,49 @@ +import { Type } from '../constants.js' +import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' import { Scalar, isScalarValue } from './Scalar.js' -import { toJS } from './toJS.js' +import { toJS, ToJSContext } from './toJS.js' -function asItemIndex(key) { - let idx = key instanceof Scalar ? key.value : key - if (idx && typeof idx === 'string') idx = Number(idx) - return Number.isInteger(idx) && idx >= 0 ? idx : null +export declare namespace YAMLSeq { + interface Parsed extends YAMLSeq { + items: Node[] + range: [number, number] + } } -export class YAMLSeq extends Collection { - static get tagName() { - return 'tag:yaml.org,2002:map' +export class YAMLSeq extends Collection { + static get tagName(): 'tag:yaml.org,2002:seq' { + return 'tag:yaml.org,2002:seq' } - add(value) { + declare items: T[] + + type?: Type.FLOW_SEQ | Type.SEQ + + add(value: T) { this.items.push(value) } - delete(key) { + delete(key: number | string | Scalar) { const idx = asItemIndex(key) if (typeof idx !== 'number') return false const del = this.items.splice(idx, 1) return del.length > 0 } - get(key, keepScalar) { + get(key: number | string | Scalar, keepScalar?: boolean) { const idx = asItemIndex(key) if (typeof idx !== 'number') return undefined const it = this.items[idx] return !keepScalar && it instanceof Scalar ? it.value : it } - has(key) { + has(key: number | string | Scalar) { const idx = asItemIndex(key) return typeof idx === 'number' && idx < this.items.length } - set(key, value) { + set(key: number | string | Scalar, value: T) { const idx = asItemIndex(key) if (typeof idx !== 'number') throw new Error(`Expected a valid index, not ${key}.`) @@ -45,22 +52,25 @@ export class YAMLSeq extends Collection { else this.items[idx] = value } - toJSON(_, ctx) { - const seq = [] + toJSON(_?: unknown, ctx?: ToJSContext) { + const seq: unknown[] = [] if (ctx && ctx.onCreate) ctx.onCreate(seq) let i = 0 for (const item of this.items) seq.push(toJS(item, String(i++), ctx)) return seq } - toString(ctx, onComment, onChompKeep) { + toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) { if (!ctx) return JSON.stringify(this) return super._toString( ctx, { blockItem: n => (n.type === 'comment' ? n.str : `- ${n.str}`), flowChars: { start: '[', end: ']' }, - isMap: false, itemIndent: (ctx.indent || '') + ' ' }, onComment, @@ -68,3 +78,9 @@ export class YAMLSeq extends Collection { ) } } + +function asItemIndex(key: unknown): number | null { + let idx = key instanceof Scalar ? key.value : key + if (idx && typeof idx === 'string') idx = Number(idx) + return Number.isInteger(idx) && idx >= 0 ? idx : null +} diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts index eeb71b13..b8364fd9 100644 --- a/src/ast/index.d.ts +++ b/src/ast/index.d.ts @@ -1,15 +1,16 @@ import { Type } from '../constants' import type { StringifyContext } from '../stringify/stringify' import { Collection } from './Collection' +export { Merge } from './Merge' import { Node } from './Node' +export { Pair, PairType } from './Pair' import { Scalar } from './Scalar' import { ToJSContext } from './toJS' +import { YAMLMap, findPair } from './YAMLMap' +import { YAMLSeq } from './YAMLSeq' export { ToJSContext, toJS } from './toJS' - -export { Collection, Node, Scalar } - -export function findPair(items: any[], key: Scalar | any): Pair | undefined +export { Collection, Node, Scalar, YAMLMap, YAMLSeq, findPair } export class Alias extends Node { constructor(source: Node) @@ -26,85 +27,6 @@ export namespace Alias { } } -export class Pair extends Node { - constructor(key: any, value?: any) - type: Pair.Type.PAIR | Pair.Type.MERGE_PAIR - /** Always Node or null when parsed, but can be set to anything. */ - key: any - /** Always Node or null when parsed, but can be set to anything. */ - value: any - cstNode?: never // no corresponding cstNode - toJSON(arg?: any, ctx?: ToJSContext): object | Map - toString( - ctx?: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void - ): string -} -export namespace Pair { - enum Type { - PAIR = 'PAIR', - MERGE_PAIR = 'MERGE_PAIR' - } -} - -export class Merge extends Pair { - static KEY: '<<' - constructor(pair?: Pair) - type: Pair.Type.MERGE_PAIR - /** Always Scalar('<<'), defined by the type specification */ - key: AST.PlainValue - /** Always YAMLSeq, stringified as *A if length = 1 */ - value: YAMLSeq - toString(ctx?: StringifyContext, onComment?: () => void): string -} - -export class YAMLMap extends Collection { - static readonly tagName: 'tag:yaml.org,2002:map' - type?: Type.FLOW_MAP | Type.MAP - items: Array - add(value: unknown): void - delete(key: unknown): boolean - get(key: unknown, keepScalar?: boolean): unknown - has(key: unknown): boolean - set(key: unknown, value: unknown): void - toJSON(arg?: any, ctx?: ToJSContext): object | Map - toString( - ctx?: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void - ): string -} - -export namespace YAMLMap { - interface Parsed extends YAMLMap { - range: [number, number] - } -} - -export class YAMLSeq extends Collection { - static readonly tagName: 'tag:yaml.org,2002:seq' - type?: Type.FLOW_SEQ | Type.SEQ - add(value: unknown): void - delete(key: number | string | Scalar): boolean - get(key: number | string | Scalar, keepScalar?: boolean): any - has(key: number | string | Scalar): boolean - set(key: number | string | Scalar, value: any): void - toJSON(arg?: any, ctx?: ToJSContext): any[] - toString( - ctx?: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void - ): string -} - -export namespace YAMLSeq { - interface Parsed extends YAMLSeq { - items: Node[] - range: [number, number] - } -} - export namespace AST { interface BlockFolded extends Scalar { type: Type.BLOCK_FOLDED diff --git a/src/ast/index.js b/src/ast/index.js index c173eba6..7d83d08b 100644 --- a/src/ast/index.js +++ b/src/ast/index.js @@ -2,7 +2,7 @@ export { Alias } from './Alias.js' export { Collection, collectionFromPath, isEmptyPath } from './Collection.js' export { Merge } from './Merge.js' export { Node } from './Node.js' -export { Pair } from './Pair.js' +export { Pair, PairType } from './Pair.js' export { Scalar } from './Scalar.js' export { YAMLMap, findPair } from './YAMLMap.js' export { YAMLSeq } from './YAMLSeq.js' diff --git a/src/ast/toJS.ts b/src/ast/toJS.ts index 0508effc..a3833028 100644 --- a/src/ast/toJS.ts +++ b/src/ast/toJS.ts @@ -4,7 +4,7 @@ import { Node } from './index.js' export interface ToJSContext { anchors: Map< Node, - { alias: string[]; aliasCount: number; count: number; res?: Node } + { alias: string[]; aliasCount: number; count: number; res?: unknown } > | null doc: Document indentStep: string @@ -12,7 +12,7 @@ export interface ToJSContext { mapAsMap: boolean mapKeyWarned: boolean maxAliasCount: number - onCreate?: (res: Node) => void + onCreate?: (res: unknown) => void /** Requiring directly in Pair would create circular dependencies */ stringify: () => string diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index cfd014fa..2d99f599 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -6,7 +6,7 @@ export function resolveMergePair( ) { if (!(pair.key instanceof Scalar) || pair.key.value !== Merge.KEY) return pair - const merge = new Merge(pair) + const merge = new Merge(pair as Pair) for (const node of merge.value.items as Node.Parsed[]) { if (node instanceof Alias) { if (node.source instanceof YAMLMap) { diff --git a/src/stringify/stringify.d.ts b/src/stringify/stringify.d.ts index dad71656..1a4dbd01 100644 --- a/src/stringify/stringify.d.ts +++ b/src/stringify/stringify.d.ts @@ -1,7 +1,7 @@ import type { Document } from '../doc/Document' export interface StringifyContext { - doc: Document.Parsed + doc: Document forceBlockIndent?: boolean implicitKey?: boolean indent: string diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js index d88cff90..842aee4c 100644 --- a/tests/doc/createNode.js +++ b/tests/doc/createNode.js @@ -1,5 +1,5 @@ import * as YAML from '../../src/index.js' -import { Pair, Scalar, YAMLMap, YAMLSeq } from '../../src/ast/index.js' +import { Pair, PairType, Scalar, YAMLMap, YAMLSeq } from '../../src/ast/index.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' let doc @@ -144,15 +144,15 @@ describe('objects', () => { const s = doc.createNode({ x: true, y: undefined }) expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toMatchObject([ - { type: Pair.Type.PAIR, key: { value: 'x' }, value: { value: true } } + { type: PairType.PAIR, key: { value: 'x' }, value: { value: true } } ]) }) test('createNode({ x: true, y: undefined }, { keepUndefined: true })', () => { const s = doc.createNode({ x: true, y: undefined }, { keepUndefined: true }) expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toMatchObject([ - { type: Pair.Type.PAIR, key: { value: 'x' }, value: { value: true } }, - { type: Pair.Type.PAIR, key: { value: 'y' }, value: { value: null } } + { type: PairType.PAIR, key: { value: 'x' }, value: { value: true } }, + { type: PairType.PAIR, key: { value: 'y' }, value: { value: null } } ]) }) describe('{ x: 3, y: [4], z: { w: "five", v: 6 } }', () => { diff --git a/tests/typings.ts b/tests/typings.ts index 686f27e5..5e3d03d7 100644 --- a/tests/typings.ts +++ b/tests/typings.ts @@ -1,7 +1,7 @@ // To test types, compile this file with tsc import { Document, parse, parseDocument, stringify, visit } from '../src/index' -import { YAMLMap, YAMLSeq, Pair } from '../src/types' +import { YAMLMap, YAMLSeq, Pair, Scalar } from '../src/types' parse('3.14159') // 3.14159 @@ -50,7 +50,7 @@ const src = '[{ a: A }, { b: B }]' const doc = parseDocument(src) const seq = doc.contents as YAMLSeq const { anchors } = doc -const [a, b] = seq.items as YAMLMap[] +const [a, b] = seq.items as YAMLMap.Parsed[] anchors.setAnchor(a.items[0].value) // 'a1' anchors.setAnchor(b.items[0].value) // 'a2' anchors.setAnchor(null, 'a1') // 'a1' @@ -109,7 +109,8 @@ visit(doc, { if (map.items.length > 3) return visit.SKIP }, Pair(_, pair) { - if (pair.key.value === 'foo') return visit.REMOVE + if (pair.key instanceof Scalar && pair.key.value === 'foo') + return visit.REMOVE }, Seq(_, seq) { if (seq.items.length > 3) return visit.BREAK From b89b20f1578ae887360712bd0d2efdffec2b7668 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 19:23:15 +0200 Subject: [PATCH 07/24] Refactor Alias as TS --- src/ast/{Alias.js => Alias.ts} | 86 ++++++++++++++++--------------- src/ast/index.d.ts | 22 ++------ src/ast/toJS.ts | 2 +- src/compose/resolve-merge-pair.ts | 7 ++- src/doc/Document.d.ts | 2 +- src/stringify/stringify.d.ts | 2 + src/stringify/stringify.js | 2 +- 7 files changed, 57 insertions(+), 66 deletions(-) rename src/ast/{Alias.js => Alias.ts} (62%) diff --git a/src/ast/Alias.js b/src/ast/Alias.ts similarity index 62% rename from src/ast/Alias.js rename to src/ast/Alias.ts index 1d6958d0..0e1c374e 100644 --- a/src/ast/Alias.js +++ b/src/ast/Alias.ts @@ -1,57 +1,33 @@ import { Type } from '../constants.js' +import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' import { Node } from './Node.js' import { Pair } from './Pair.js' -import { toJS } from './toJS.js' +import { toJS, ToJSContext } from './toJS.js' -const getAliasCount = (node, anchors) => { - if (node instanceof Alias) { - const anchor = anchors.get(node.source) - return anchor.count * anchor.aliasCount - } else if (node instanceof Collection) { - let count = 0 - for (const item of node.items) { - const c = getAliasCount(item, anchors) - if (c > count) count = c - } - return count - } else if (node instanceof Pair) { - const kc = getAliasCount(node.key, anchors) - const vc = getAliasCount(node.value, anchors) - return Math.max(kc, vc) +export declare namespace Alias { + interface Parsed extends Alias { + range: [number, number] } - return 1 } export class Alias extends Node { - static default = true + source: Node + type: Type.ALIAS = Type.ALIAS - static stringify( - { range, source }, - { anchors, doc, implicitKey, inStringifyKey } - ) { - let anchor = Object.keys(anchors).find(a => anchors[a] === source) - if (!anchor && inStringifyKey) - anchor = doc.anchors.getName(source) || doc.anchors.newName() - if (anchor) return `*${anchor}${implicitKey ? ' ' : ''}` - const msg = doc.anchors.getName(source) - ? 'Alias node must be after source node' - : 'Source node not found for alias node' - throw new Error(`${msg} [${range}]`) - } - - constructor(source) { + constructor(source: Node) { super() this.source = source - this.type = Type.ALIAS - } - - set tag(t) { - throw new Error('Alias nodes cannot have tags') + Object.defineProperty(this, 'tag', { + set() { + throw new Error('Alias nodes cannot have tags') + } + }) } - toJSON(arg, ctx) { - if (!ctx) return toJS(this.source, arg, ctx) + toJSON(arg?: unknown, ctx?: ToJSContext) { + if (!ctx) + return toJS(this.source, typeof arg === 'string' ? arg : null, ctx) const { anchors, maxAliasCount } = ctx const anchor = anchors.get(this.source) /* istanbul ignore if */ @@ -74,7 +50,33 @@ export class Alias extends Node { // Only called when stringifying an alias mapping key while constructing // Object output. - toString(ctx) { - return Alias.stringify(this, ctx) + toString({ anchors, doc, implicitKey, inStringifyKey }: StringifyContext) { + let anchor = Object.keys(anchors).find(a => anchors[a] === this.source) + if (!anchor && inStringifyKey) + anchor = doc.anchors.getName(this.source) || doc.anchors.newName() + if (anchor) return `*${anchor}${implicitKey ? ' ' : ''}` + const msg = doc.anchors.getName(this.source) + ? 'Alias node must be after source node' + : 'Source node not found for alias node' + throw new Error(`${msg} [${this.range}]`) + } +} + +function getAliasCount(node: unknown, anchors: ToJSContext['anchors']): number { + if (node instanceof Alias) { + const anchor = anchors.get(node.source) + return anchor ? anchor.count * anchor.aliasCount : 0 + } else if (node instanceof Collection) { + let count = 0 + for (const item of node.items) { + const c = getAliasCount(item, anchors) + if (c > count) count = c + } + return count + } else if (node instanceof Pair) { + const kc = getAliasCount(node.key, anchors) + const vc = getAliasCount(node.value, anchors) + return Math.max(kc, vc) } + return 1 } diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts index b8364fd9..4f770ede 100644 --- a/src/ast/index.d.ts +++ b/src/ast/index.d.ts @@ -1,31 +1,15 @@ import { Type } from '../constants' -import type { StringifyContext } from '../stringify/stringify' -import { Collection } from './Collection' +export { Alias } from './Alias' +export { Collection } from './Collection' export { Merge } from './Merge' import { Node } from './Node' export { Pair, PairType } from './Pair' import { Scalar } from './Scalar' -import { ToJSContext } from './toJS' import { YAMLMap, findPair } from './YAMLMap' import { YAMLSeq } from './YAMLSeq' - export { ToJSContext, toJS } from './toJS' -export { Collection, Node, Scalar, YAMLMap, YAMLSeq, findPair } - -export class Alias extends Node { - constructor(source: Node) - type: Type.ALIAS - source: Node - // cstNode?: CST.Alias - toJSON(arg?: any, ctx?: ToJSContext): any - toString(ctx: StringifyContext): string -} -export namespace Alias { - interface Parsed extends Alias { - range: [number, number] - } -} +export { Node, Scalar, YAMLMap, YAMLSeq, findPair } export namespace AST { interface BlockFolded extends Scalar { diff --git a/src/ast/toJS.ts b/src/ast/toJS.ts index a3833028..9846e2bb 100644 --- a/src/ast/toJS.ts +++ b/src/ast/toJS.ts @@ -5,7 +5,7 @@ export interface ToJSContext { anchors: Map< Node, { alias: string[]; aliasCount: number; count: number; res?: unknown } - > | null + > doc: Document indentStep: string keep: boolean diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index 2d99f599..4b01b872 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -7,7 +7,7 @@ export function resolveMergePair( if (!(pair.key instanceof Scalar) || pair.key.value !== Merge.KEY) return pair const merge = new Merge(pair as Pair) - for (const node of merge.value.items as Node.Parsed[]) { + for (const node of merge.value.items as Alias.Parsed[]) { if (node instanceof Alias) { if (node.source instanceof YAMLMap) { // ok @@ -15,7 +15,10 @@ export function resolveMergePair( onError(node.range[0], 'Merge nodes aliases can only point to maps') } } else { - onError(node.range[0], 'Merge nodes can only have Alias nodes as values') + onError( + (node as Node.Parsed).range[0], + 'Merge nodes can only have Alias nodes as values' + ) } } return merge diff --git a/src/doc/Document.d.ts b/src/doc/Document.d.ts index f520fbc6..e2a5c983 100644 --- a/src/doc/Document.d.ts +++ b/src/doc/Document.d.ts @@ -165,7 +165,7 @@ export namespace Document { * Find an available anchor name with the given `prefix` and a * numerical suffix. */ - newName(prefix: string): string + newName(prefix?: string): string /** * Associate an anchor with `node`. If `name` is empty, a new name will be generated. * To remove an anchor, use `setAnchor(null, name)`. diff --git a/src/stringify/stringify.d.ts b/src/stringify/stringify.d.ts index 1a4dbd01..7fa4dd1e 100644 --- a/src/stringify/stringify.d.ts +++ b/src/stringify/stringify.d.ts @@ -1,6 +1,8 @@ +import { Node } from '../ast'; import type { Document } from '../doc/Document' export interface StringifyContext { + anchors: Record doc: Document forceBlockIndent?: boolean implicitKey?: boolean diff --git a/src/stringify/stringify.js b/src/stringify/stringify.js index 8e463e85..d5c0d722 100644 --- a/src/stringify/stringify.js +++ b/src/stringify/stringify.js @@ -5,7 +5,6 @@ import { Scalar } from '../ast/Scalar.js' import { stringifyString } from './stringifyString.js' function getTagObject(tags, item) { - if (item instanceof Alias) return Alias if (item.tag) { const match = tags.filter(t => t.tag === item.tag) if (match.length > 0) @@ -58,6 +57,7 @@ export function stringify(item, ctx, onComment, onChompKeep) { } if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) + if (item instanceof Alias) return item.toString(ctx) if (!tagObj) tagObj = getTagObject(schema.tags, item) const props = stringifyProps(item, tagObj, ctx) From d65a0e8295988e95a471417d91c7690ee0b06e45 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 19:25:12 +0200 Subject: [PATCH 08/24] Drop AST namespace from typings --- index.d.ts | 1 - src/ast/index.d.ts | 60 ++++------------------------------------------ 2 files changed, 4 insertions(+), 57 deletions(-) diff --git a/index.d.ts b/index.d.ts index ed3f09d9..9247c2a8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,2 +1 @@ export * from './dist/index' -export { AST } from './dist/ast/index' diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts index 4f770ede..681bdfd5 100644 --- a/src/ast/index.d.ts +++ b/src/ast/index.d.ts @@ -1,61 +1,9 @@ -import { Type } from '../constants' export { Alias } from './Alias' export { Collection } from './Collection' export { Merge } from './Merge' -import { Node } from './Node' +export { Node } from './Node' export { Pair, PairType } from './Pair' -import { Scalar } from './Scalar' -import { YAMLMap, findPair } from './YAMLMap' -import { YAMLSeq } from './YAMLSeq' +export { Scalar } from './Scalar' +export { YAMLMap, findPair } from './YAMLMap' +export { YAMLSeq } from './YAMLSeq' export { ToJSContext, toJS } from './toJS' - -export { Node, Scalar, YAMLMap, YAMLSeq, findPair } - -export namespace AST { - interface BlockFolded extends Scalar { - type: Type.BLOCK_FOLDED - // cstNode?: CST.BlockFolded - } - - interface BlockLiteral extends Scalar { - type: Type.BLOCK_LITERAL - // cstNode?: CST.BlockLiteral - } - - interface PlainValue extends Scalar { - type: Type.PLAIN - // cstNode?: CST.PlainValue - } - - interface QuoteDouble extends Scalar { - type: Type.QUOTE_DOUBLE - // cstNode?: CST.QuoteDouble - } - - interface QuoteSingle extends Scalar { - type: Type.QUOTE_SINGLE - // cstNode?: CST.QuoteSingle - } - - interface FlowMap extends YAMLMap { - type: Type.FLOW_MAP - // cstNode?: CST.FlowMap - } - - interface BlockMap extends YAMLMap { - type: Type.MAP - // cstNode?: CST.Map - } - - interface FlowSeq extends YAMLSeq { - type: Type.FLOW_SEQ - items: Array - // cstNode?: CST.FlowSeq - } - - interface BlockSeq extends YAMLSeq { - type: Type.SEQ - items: Array - // cstNode?: CST.Seq - } -} From 951e1f49ea503f1cf2f03bd9ab0fe0b1975e9fdc Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 20:05:39 +0200 Subject: [PATCH 09/24] Add more generics; refactor last of src/ast/ as TS --- src/ast/Alias.ts | 6 +++--- src/ast/Scalar.ts | 6 +++--- src/ast/YAMLMap.ts | 2 +- src/ast/YAMLSeq.ts | 4 +++- src/ast/index.d.ts | 9 --------- src/ast/{index.js => index.ts} | 3 +-- src/stringify/stringifyNumber.ts | 4 ++-- src/stringify/stringifyString.ts | 23 ++++++++++++----------- 8 files changed, 25 insertions(+), 32 deletions(-) delete mode 100644 src/ast/index.d.ts rename src/ast/{index.js => index.ts} (88%) diff --git a/src/ast/Alias.ts b/src/ast/Alias.ts index 0e1c374e..5d7966a2 100644 --- a/src/ast/Alias.ts +++ b/src/ast/Alias.ts @@ -11,11 +11,11 @@ export declare namespace Alias { } } -export class Alias extends Node { - source: Node +export class Alias extends Node { + source: T type: Type.ALIAS = Type.ALIAS - constructor(source: Node) { + constructor(source: T) { super() this.source = source Object.defineProperty(this, 'tag', { diff --git a/src/ast/Scalar.ts b/src/ast/Scalar.ts index 1f144346..280365b9 100644 --- a/src/ast/Scalar.ts +++ b/src/ast/Scalar.ts @@ -19,8 +19,8 @@ export declare namespace Scalar { | Type.QUOTE_SINGLE } -export class Scalar extends Node { - value: any +export class Scalar extends Node { + value: T declare type?: Scalar.Type @@ -33,7 +33,7 @@ export class Scalar extends Node { declare minFractionDigits?: number - constructor(value: unknown) { + constructor(value: T) { super() this.value = value } diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index a1344c52..b563e300 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -30,7 +30,7 @@ export class YAMLMap extends Collection { type?: Type.FLOW_MAP | Type.MAP - items: Pair[] = [] + declare items: Pair[] /** * Adds a value to the collection. diff --git a/src/ast/YAMLSeq.ts b/src/ast/YAMLSeq.ts index 1c59ba82..497b4e98 100644 --- a/src/ast/YAMLSeq.ts +++ b/src/ast/YAMLSeq.ts @@ -82,5 +82,7 @@ export class YAMLSeq extends Collection { function asItemIndex(key: unknown): number | null { let idx = key instanceof Scalar ? key.value : key if (idx && typeof idx === 'string') idx = Number(idx) - return Number.isInteger(idx) && idx >= 0 ? idx : null + return typeof idx === 'number' && Number.isInteger(idx) && idx >= 0 + ? idx + : null } diff --git a/src/ast/index.d.ts b/src/ast/index.d.ts deleted file mode 100644 index 681bdfd5..00000000 --- a/src/ast/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { Alias } from './Alias' -export { Collection } from './Collection' -export { Merge } from './Merge' -export { Node } from './Node' -export { Pair, PairType } from './Pair' -export { Scalar } from './Scalar' -export { YAMLMap, findPair } from './YAMLMap' -export { YAMLSeq } from './YAMLSeq' -export { ToJSContext, toJS } from './toJS' diff --git a/src/ast/index.js b/src/ast/index.ts similarity index 88% rename from src/ast/index.js rename to src/ast/index.ts index 7d83d08b..c51c8810 100644 --- a/src/ast/index.js +++ b/src/ast/index.ts @@ -6,5 +6,4 @@ export { Pair, PairType } from './Pair.js' export { Scalar } from './Scalar.js' export { YAMLMap, findPair } from './YAMLMap.js' export { YAMLSeq } from './YAMLSeq.js' - -export { toJS } from './toJS.js' +export { ToJSContext, toJS } from './toJS.js' diff --git a/src/stringify/stringifyNumber.ts b/src/stringify/stringifyNumber.ts index 2aac473a..4cc9fb13 100644 --- a/src/stringify/stringifyNumber.ts +++ b/src/stringify/stringifyNumber.ts @@ -7,8 +7,8 @@ export function stringifyNumber({ value }: Scalar) { if (typeof value === 'bigint') return String(value) - if (!isFinite(value)) - return isNaN(value) ? '.nan' : value < 0 ? '-.inf' : '.inf' + const num = typeof value === 'number' ? value : Number(value) + if (!isFinite(num)) return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf' let n = JSON.stringify(value) if ( !format && diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index a6332673..25239f2e 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -305,14 +305,15 @@ export function stringifyString( ) { const { defaultKeyType, defaultType } = strOptions const { implicitKey, inFlow } = ctx - let { type, value } = item - if (typeof value !== 'string') { - value = String(value) - item = Object.assign({}, item, { value }) - } + const ss: Scalar = + typeof item.value === 'string' + ? (item as Scalar) + : Object.assign({}, item, { value: String(item.value) }) + + let { type } = item if (type !== Type.QUOTE_DOUBLE) { // force double quotes on control characters & unpaired surrogates - if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(value)) + if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value)) type = Type.QUOTE_DOUBLE } @@ -321,14 +322,14 @@ export function stringifyString( case Type.BLOCK_FOLDED: case Type.BLOCK_LITERAL: return implicitKey || inFlow - ? doubleQuotedString(value, ctx) // blocks are not valid inside flow containers - : blockString(item, ctx, onComment, onChompKeep) + ? doubleQuotedString(ss.value, ctx) // blocks are not valid inside flow containers + : blockString(ss, ctx, onComment, onChompKeep) case Type.QUOTE_DOUBLE: - return doubleQuotedString(value, ctx) + return doubleQuotedString(ss.value, ctx) case Type.QUOTE_SINGLE: - return singleQuotedString(value, ctx) + return singleQuotedString(ss.value, ctx) case Type.PLAIN: - return plainString(item, ctx, onComment, onChompKeep) + return plainString(ss, ctx, onComment, onChompKeep) default: return null } From 904bbe735b0dbdcf032e15aa61328c955312048a Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 20:23:24 +0200 Subject: [PATCH 10/24] Refactor stringify() as TypeScript --- rollup.node-config.js | 9 +-- src/doc/Document.d.ts | 2 + src/doc/createNode.ts | 2 +- src/stringify/stringify.d.ts | 22 ------ src/stringify/{stringify.js => stringify.ts} | 70 ++++++++++++++------ 5 files changed, 52 insertions(+), 53 deletions(-) delete mode 100644 src/stringify/stringify.d.ts rename src/stringify/{stringify.js => stringify.ts} (52%) diff --git a/rollup.node-config.js b/rollup.node-config.js index 15fbe4d0..bf55cadc 100644 --- a/rollup.node-config.js +++ b/rollup.node-config.js @@ -21,14 +21,7 @@ export default { presets: [['@babel/env', { modules: false, targets: { node: '10.0' } }]] }), typescript(), - copy({ - targets: [ - { src: 'src/*.d.ts', dest: 'dist' }, - { src: 'src/ast/*.d.ts', dest: 'dist/ast' }, - { src: 'src/doc/*.d.ts', dest: 'dist/doc' }, - { src: 'src/stringify/*.d.ts', dest: 'dist/stringify' } - ] - }) + copy({ targets: [{ src: 'src/doc/*.d.ts', dest: 'dist/doc' }] }) ], treeshake: { moduleSideEffects: false, propertyReadSideEffects: false } } diff --git a/src/doc/Document.d.ts b/src/doc/Document.d.ts index e2a5c983..526ee63e 100644 --- a/src/doc/Document.d.ts +++ b/src/doc/Document.d.ts @@ -9,6 +9,8 @@ export type Replacer = any[] | ((key: any, value: any) => boolean) export type Reviver = (key: any, value: any) => any export interface CreateNodeOptions { + onTagObj?: (tagObj: Schema.Tag) => void + /** * Filter or modify values while creating a node. * diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index 1efe4be5..65ab23eb 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -13,7 +13,7 @@ export interface CreateNodeAliasRef { export interface CreateNodeContext { keepUndefined?: boolean onAlias(source: CreateNodeAliasRef): Alias - onTagObj?: (tagObj: unknown) => void + onTagObj?: (tagObj: Schema.Tag) => void prevObjects: Map replacer?: Replacer schema: Schema diff --git a/src/stringify/stringify.d.ts b/src/stringify/stringify.d.ts deleted file mode 100644 index 7fa4dd1e..00000000 --- a/src/stringify/stringify.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Node } from '../ast'; -import type { Document } from '../doc/Document' - -export interface StringifyContext { - anchors: Record - doc: Document - forceBlockIndent?: boolean - implicitKey?: boolean - indent: string - indentStep: string - indentAtStart?: number - inFlow?: boolean - stringify: typeof stringify - [key: string]: unknown -} - -export function stringify( - item: unknown, - ctx: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void -): string diff --git a/src/stringify/stringify.js b/src/stringify/stringify.ts similarity index 52% rename from src/stringify/stringify.js rename to src/stringify/stringify.ts index d5c0d722..f239b6de 100644 --- a/src/stringify/stringify.js +++ b/src/stringify/stringify.ts @@ -2,16 +2,32 @@ import { Alias } from '../ast/Alias.js' import { Node } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' import { Scalar } from '../ast/Scalar.js' +import type { Document } from '../doc/Document' +import { Schema } from '../doc/Schema.js' import { stringifyString } from './stringifyString.js' -function getTagObject(tags, item) { +export interface StringifyContext { + anchors: Record + doc: Document + forceBlockIndent?: boolean + implicitKey?: boolean + indent: string + indentStep: string + indentAtStart?: number + inFlow?: boolean + stringify: typeof stringify + [key: string]: unknown +} + +function getTagObject(tags: Schema.Tag[], item: Node) { if (item.tag) { const match = tags.filter(t => t.tag === item.tag) if (match.length > 0) - return match.find(t => t.format === item.format) || match[0] + return match.find(t => t.format === (item as Scalar).format) || match[0] } - let tagObj, obj + let tagObj: Schema.Tag | undefined = undefined + let obj: unknown if (item instanceof Scalar) { obj = item.value const match = tags.filter(t => t.identify && t.identify(obj)) @@ -23,6 +39,7 @@ function getTagObject(tags, item) { } if (!tagObj) { + // @ts-ignore const name = obj && obj.constructor ? obj.constructor.name : typeof obj throw new Error(`Tag not resolved for ${name} value`) } @@ -30,7 +47,11 @@ function getTagObject(tags, item) { } // needs to be called before value stringifier to allow for circular anchor refs -function stringifyProps(node, tagObj, { anchors, doc }) { +function stringifyProps( + node: Node, + tagObj: Schema.Tag, + { anchors, doc }: StringifyContext +) { const props = [] const anchor = doc.anchors.getName(node) if (anchor) { @@ -45,33 +66,38 @@ function stringifyProps(node, tagObj, { anchors, doc }) { return props.join(' ') } -export function stringify(item, ctx, onComment, onChompKeep) { - const { schema } = ctx.doc - - let tagObj - if (!(item instanceof Node)) { - item = ctx.doc.createNode(item, { - onTagObj: o => (tagObj = o), - wrapScalars: true - }) - } - +export function stringify( + item: unknown, + ctx: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void +): string { if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) if (item instanceof Alias) return item.toString(ctx) - if (!tagObj) tagObj = getTagObject(schema.tags, item) - const props = stringifyProps(item, tagObj, ctx) + let tagObj: Schema.Tag | undefined = undefined + const node: Node = + item instanceof Node + ? item + : ctx.doc.createNode(item, { + onTagObj: o => (tagObj = o), + wrapScalars: true + }) + + if (!tagObj) tagObj = getTagObject(ctx.doc.schema.tags, node) + + const props = stringifyProps(node, tagObj, ctx) if (props.length > 0) ctx.indentAtStart = (ctx.indentAtStart || 0) + props.length + 1 const str = typeof tagObj.stringify === 'function' - ? tagObj.stringify(item, ctx, onComment, onChompKeep) - : item instanceof Scalar - ? stringifyString(item, ctx, onComment, onChompKeep) - : item.toString(ctx, onComment, onChompKeep) + ? tagObj.stringify(node, ctx, onComment, onChompKeep) + : node instanceof Scalar + ? stringifyString(node, ctx, onComment, onChompKeep) + : node.toString(ctx, onComment, onChompKeep) if (!props) return str - return item instanceof Scalar || str[0] === '{' || str[0] === '[' + return node instanceof Scalar || str[0] === '{' || str[0] === '[' ? `${props} ${str}` : `${props}\n${ctx.indent}${str}` } From 7fa4bb7f8df9734382478a1c93ab72de1717e67e Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 21 Feb 2021 22:27:18 +0200 Subject: [PATCH 11/24] Refactor src/doc/ internals as TypeScript --- rollup.node-config.js | 4 +- src/compose/compose-scalar.ts | 3 +- src/doc/{Anchors.js => Anchors.ts} | 55 ++++--- src/doc/Document.d.ts | 49 ++----- src/doc/Schema.d.ts | 147 ------------------- src/doc/Schema.js | 30 ---- src/doc/Schema.ts | 88 +++++++++++ src/doc/{applyReviver.js => applyReviver.ts} | 9 +- src/doc/createNode.ts | 5 +- src/doc/directives.ts | 20 ++- src/doc/getSchemaTags.js | 31 ---- src/doc/getSchemaTags.ts | 38 +++++ src/doc/listTagNames.ts | 20 --- src/options.ts | 4 +- src/public-api.ts | 6 +- src/stringify/stringify.ts | 10 +- src/tags/index.d.ts | 4 + src/tags/types.ts | 105 +++++++++++++ 18 files changed, 320 insertions(+), 308 deletions(-) rename src/doc/{Anchors.js => Anchors.ts} (57%) delete mode 100644 src/doc/Schema.d.ts delete mode 100644 src/doc/Schema.js create mode 100644 src/doc/Schema.ts rename src/doc/{applyReviver.js => applyReviver.ts} (89%) delete mode 100644 src/doc/getSchemaTags.js create mode 100644 src/doc/getSchemaTags.ts delete mode 100644 src/doc/listTagNames.ts create mode 100644 src/tags/index.d.ts create mode 100644 src/tags/types.ts diff --git a/rollup.node-config.js b/rollup.node-config.js index bf55cadc..f71139ee 100644 --- a/rollup.node-config.js +++ b/rollup.node-config.js @@ -21,7 +21,9 @@ export default { presets: [['@babel/env', { modules: false, targets: { node: '10.0' } }]] }), typescript(), - copy({ targets: [{ src: 'src/doc/*.d.ts', dest: 'dist/doc' }] }) + copy({ + targets: [{ src: 'src/tags/*.d.ts', dest: 'dist/tags' }] + }) ], treeshake: { moduleSideEffects: false, propertyReadSideEffects: false } } diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index 4e9fa2ee..75ba3bae 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -2,6 +2,7 @@ import { Scalar } from '../ast/index.js' import { Document } from '../doc/Document.js' import type { Schema } from '../doc/Schema.js' import type { BlockScalar, FlowScalar } from '../parse/tokens.js' +import type { Tag } from '../tags/types.js' import { resolveBlockScalar } from './resolve-block-scalar.js' import { resolveFlowScalar } from './resolve-flow-scalar.js' @@ -51,7 +52,7 @@ function findScalarTagByName( onError: (offset: number, message: string, warning?: boolean) => void ) { if (tagName === '!') return defaultScalarTag(schema) // non-specific tag - const matchWithTest: Schema.Tag[] = [] + const matchWithTest: Tag[] = [] for (const tag of schema.tags) { if (tag.tag === tagName) { if (tag.default && tag.test) matchWithTest.push(tag) diff --git a/src/doc/Anchors.js b/src/doc/Anchors.ts similarity index 57% rename from src/doc/Anchors.js rename to src/doc/Anchors.ts index 01978280..f380c6e8 100644 --- a/src/doc/Anchors.js +++ b/src/doc/Anchors.ts @@ -1,26 +1,27 @@ -import { Alias, Merge, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' +import { Alias, Merge, Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' export class Anchors { - static validAnchorNode(node) { - return ( - node instanceof Scalar || - node instanceof YAMLSeq || - node instanceof YAMLMap - ) - } - - map = Object.create(null) + private map: Record = Object.create(null) + private prefix: string - constructor(prefix) { + constructor(prefix: string) { this.prefix = prefix } - createAlias(node, name) { + /** + * Create a new `Alias` node, adding the required anchor for `node`. + * If `name` is empty, a new anchor name will be generated. + */ + createAlias(node: Node, name?: string) { this.setAnchor(node, name) return new Alias(node) } - createMergePair(...sources) { + /** + * Create a new `Merge` node with the given source nodes. + * Non-`Alias` sources will be automatically wrapped. + */ + createMergePair(...sources: Node[]) { const merge = new Merge() merge.value.items = sources.map(s => { if (s instanceof Alias) { @@ -33,20 +34,26 @@ export class Anchors { return merge } - getName(node) { - const { map } = this - return Object.keys(map).find(a => map[a] === node) + /** The anchor name associated with `node`, if set. */ + getName(node: Node) { + return Object.keys(this.map).find(a => this.map[a] === node) } + /** List of all defined anchor names. */ getNames() { return Object.keys(this.map) } - getNode(name) { + /** The node associated with the anchor `name`, if set. */ + getNode(name: string) { return this.map[name] } - newName(prefix) { + /** + * Find an available anchor name with the given `prefix` and a + * numerical suffix. + */ + newName(prefix?: string) { if (!prefix) prefix = this.prefix const names = Object.keys(this.map) for (let i = 1; true; ++i) { @@ -55,7 +62,11 @@ export class Anchors { } } - setAnchor(node, name) { + /** + * Associate an anchor with `node`. If `name` is empty, a new name will be generated. + * To remove an anchor, use `setAnchor(null, name)`. + */ + setAnchor(node: Node | null, name?: string) { const { map } = this if (!node) { if (!name) return null @@ -63,7 +74,11 @@ export class Anchors { return name } - if (!Anchors.validAnchorNode(node)) + const valid = + node instanceof Scalar || + node instanceof YAMLSeq || + node instanceof YAMLMap + if (!valid) throw new Error('Anchors may only be set for Scalar, Seq and Map nodes') if (name) { if (/[\x00-\x19\s,[\]{}]/.test(name)) diff --git a/src/doc/Document.d.ts b/src/doc/Document.d.ts index 526ee63e..b577848c 100644 --- a/src/doc/Document.d.ts +++ b/src/doc/Document.d.ts @@ -1,15 +1,18 @@ -import { Alias, Collection, Merge, Node, Pair } from '../ast' +import { Collection, Node, Pair } from '../ast' import { Type } from '../constants' import { YAMLError, YAMLWarning } from '../errors' import { Options } from '../options' +import type { Tag, TagId } from '../tags/types' +import { Anchors } from './Anchors' +import { Reviver } from './applyReviver' import { Directives } from './directives' -import { Schema } from './Schema' +import { Schema, SchemaName } from './Schema' export type Replacer = any[] | ((key: any, value: any) => boolean) -export type Reviver = (key: any, value: any) => any +export type { Anchors, Reviver } export interface CreateNodeOptions { - onTagObj?: (tagObj: Schema.Tag) => void + onTagObj?: (tagObj: Tag) => void /** * Filter or modify values while creating a node. @@ -48,7 +51,7 @@ export class Document extends Collection { * Anchors associated with the document's nodes; * also provides alias & merge node creators. */ - anchors: Document.Anchors + anchors: Anchors /** The document contents. */ contents: any /** Errors encountered during parsing. */ @@ -105,8 +108,8 @@ export class Document extends Collection { * by the document. */ setSchema( - id?: Options['version'] | Schema.Name, - customTags?: (Schema.TagId | Schema.Tag)[] + id?: Options['version'] | SchemaName, + customTags?: (TagId | Tag)[] ): void /** Set `handle` as a shorthand string for the `prefix` tag namespace. */ setTagPrefix(handle: string, prefix: string): void @@ -143,38 +146,6 @@ export namespace Document { schema: Schema } - interface Anchors { - /** @private */ - map: Record - - /** - * Create a new `Alias` node, adding the required anchor for `node`. - * If `name` is empty, a new anchor name will be generated. - */ - createAlias(node: Node, name?: string): Alias - /** - * Create a new `Merge` node with the given source nodes. - * Non-`Alias` sources will be automatically wrapped. - */ - createMergePair(...nodes: Node[]): Merge - /** The anchor name associated with `node`, if set. */ - getName(node: Node): undefined | string - /** List of all defined anchor names. */ - getNames(): string[] - /** The node associated with the anchor `name`, if set. */ - getNode(name: string): undefined | Node - /** - * Find an available anchor name with the given `prefix` and a - * numerical suffix. - */ - newName(prefix?: string): string - /** - * Associate an anchor with `node`. If `name` is empty, a new name will be generated. - * To remove an anchor, use `setAnchor(null, name)`. - */ - setAnchor(node: Node | null, name?: string): void | string - } - interface TagPrefix { handle: string prefix: string diff --git a/src/doc/Schema.d.ts b/src/doc/Schema.d.ts deleted file mode 100644 index b2522813..00000000 --- a/src/doc/Schema.d.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Node, Pair, Scalar, YAMLMap, YAMLSeq } from '../ast' -import { StringifyContext } from '../stringify/stringify' -import { CreateNodeContext } from './createNode' - -export class Schema { - constructor(options: Schema.Options) - knownTags: { [key: string]: Schema.Tag } - merge: boolean - map: Schema.Tag - name: Schema.Name - seq: Schema.Tag - sortMapEntries: ((a: Pair, b: Pair) => number) | null - tags: Schema.Tag[] -} - -export namespace Schema { - type Name = 'core' | 'failsafe' | 'json' | 'yaml-1.1' - - interface Options { - /** - * Array of additional tags to include in the schema, or a function that may - * modify the schema's base tag array. - */ - customTags?: (TagId | Tag)[] | ((tags: Tag[]) => Tag[]) | null - /** - * Enable support for `<<` merge keys. - * - * Default: `false` for YAML 1.2, `true` for earlier versions - */ - merge?: boolean - /** - * When using the `'core'` schema, support parsing values with these - * explicit YAML 1.1 tags: - * - * `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. - * - * Default `true` - */ - resolveKnownTags?: boolean - /** - * The base schema to use. - * - * Default: `"core"` for YAML 1.2, `"yaml-1.1"` for earlier versions - */ - schema?: Name - /** - * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. - * - * Default: `false` - */ - sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) - /** - * @deprecated Use `customTags` instead. - */ - tags?: Options['customTags'] - } - - type TagId = - | 'binary' - | 'bool' - | 'float' - | 'floatExp' - | 'floatNaN' - | 'floatTime' - | 'int' - | 'intHex' - | 'intOct' - | 'intTime' - | 'null' - | 'omap' - | 'pairs' - | 'set' - | 'timestamp' - - interface Tag { - /** - * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. - */ - createNode?: ( - schema: Schema, - value: any, - ctx: CreateNodeContext - ) => YAMLMap | YAMLSeq | Scalar - /** - * If `true`, together with `test` allows for values to be stringified without - * an explicit tag. For most cases, it's unlikely that you'll actually want to - * use this, even if you first think you do. - */ - default: boolean - /** - * If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them. - */ - format?: string - /** - * Used by `YAML.createNode` to detect your data type, e.g. using `typeof` or - * `instanceof`. - */ - identify(value: any): boolean - /** - * The `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations. - */ - nodeClass?: new () => any - /** - * Used by some tags to configure their stringification, where applicable. - */ - options?: object - /** - * Turns a value into an AST node. - * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. - */ - resolve( - value: string | YAMLMap | YAMLSeq, - onError: (message: string) => void - ): Node | any - /** - * Optional function stringifying the AST node in the current context. If your - * data includes a suitable `.toString()` method, you can probably leave this - * undefined and use the default stringifier. - * - * @param item The node being stringified. - * @param ctx Contains the stringifying context variables. - * @param onComment Callback to signal that the stringifier includes the - * item's comment in its output. - * @param onChompKeep Callback to signal that the output uses a block scalar - * type with the `+` chomping indicator. - */ - stringify?: ( - item: Node, - ctx: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void - ) => string - /** - * The identifier for your data type, with which its stringified form will be - * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified - * `tag:domain,date:foo`. - */ - tag: string - /** - * Together with `default` allows for values to be stringified without an - * explicit tag and detected using a regular expression. For most cases, it's - * unlikely that you'll actually want to use these, even if you first think - * you do. - */ - test?: RegExp - } -} diff --git a/src/doc/Schema.js b/src/doc/Schema.js deleted file mode 100644 index 50bd3f1d..00000000 --- a/src/doc/Schema.js +++ /dev/null @@ -1,30 +0,0 @@ -import { schemas, tags } from '../tags/index.js' -import { getSchemaTags } from './getSchemaTags.js' - -const sortMapEntriesByKey = (a, b) => - a.key < b.key ? -1 : a.key > b.key ? 1 : 0 - -const coreKnownTags = { - 'tag:yaml.org,2002:binary': tags.binary, - 'tag:yaml.org,2002:omap': tags.omap, - 'tag:yaml.org,2002:pairs': tags.pairs, - 'tag:yaml.org,2002:set': tags.set, - 'tag:yaml.org,2002:timestamp': tags.timestamp -} - -export class Schema { - constructor({ customTags, merge, resolveKnownTags, schema, sortMapEntries }) { - this.merge = !!merge - this.name = schema - this.knownTags = resolveKnownTags ? coreKnownTags : {} - this.tags = getSchemaTags(schemas, tags, customTags, schema) - - // Used by createNode(), to avoid circular dependencies - this.map = tags.map - this.seq = tags.seq - - // Used by createMap() - this.sortMapEntries = - sortMapEntries === true ? sortMapEntriesByKey : sortMapEntries || null - } -} diff --git a/src/doc/Schema.ts b/src/doc/Schema.ts new file mode 100644 index 00000000..6a8f0200 --- /dev/null +++ b/src/doc/Schema.ts @@ -0,0 +1,88 @@ +import type { Pair } from '../ast/Pair.js' +import { schemas, tags } from '../tags/index.js' +import type { Tag, TagId } from '../tags/types.js' +import { getSchemaTags } from './getSchemaTags.js' + +export type SchemaName = 'core' | 'failsafe' | 'json' | 'yaml-1.1' + +export interface SchemaOptions { + /** + * Array of additional tags to include in the schema, or a function that may + * modify the schema's base tag array. + */ + customTags?: + | Array + | ((tags: Array) => Array) + | null + + /** + * Enable support for `<<` merge keys. + * + * Default: `false` for YAML 1.2, `true` for earlier versions + */ + merge?: boolean + + /** + * When using the `'core'` schema, support parsing values with these + * explicit YAML 1.1 tags: + * + * `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. + * + * Default `true` + */ + resolveKnownTags?: boolean + + /** + * The base schema to use. + * + * Default: `"core"` for YAML 1.2, `"yaml-1.1"` for earlier versions + */ + schema?: SchemaName + + /** + * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`. + * + * Default: `false` + */ + sortMapEntries?: boolean | ((a: Pair, b: Pair) => number) +} + +const sortMapEntriesByKey = (a: Pair, b: Pair) => + a.key < b.key ? -1 : a.key > b.key ? 1 : 0 + +const coreKnownTags = { + 'tag:yaml.org,2002:binary': tags.binary, + 'tag:yaml.org,2002:omap': tags.omap, + 'tag:yaml.org,2002:pairs': tags.pairs, + 'tag:yaml.org,2002:set': tags.set, + 'tag:yaml.org,2002:timestamp': tags.timestamp +} + +export class Schema { + knownTags: Record + merge: boolean + name: SchemaName + sortMapEntries: ((a: Pair, b: Pair) => number) | null + tags: Tag[] + + // Used by createNode(), to avoid circular dependencies + map = tags.map + seq = tags.seq + + constructor({ + customTags, + merge, + resolveKnownTags, + schema, + sortMapEntries + }: SchemaOptions) { + this.merge = !!merge + this.name = schema || 'core' + this.knownTags = resolveKnownTags ? coreKnownTags : {} + this.tags = getSchemaTags(schemas, tags, customTags, this.name) + + // Used by createMap() + this.sortMapEntries = + sortMapEntries === true ? sortMapEntriesByKey : sortMapEntries || null + } +} diff --git a/src/doc/applyReviver.js b/src/doc/applyReviver.ts similarity index 89% rename from src/doc/applyReviver.js rename to src/doc/applyReviver.ts index 5aa94912..b00edf3d 100644 --- a/src/doc/applyReviver.js +++ b/src/doc/applyReviver.ts @@ -1,3 +1,5 @@ +export type Reviver = (key: unknown, value: unknown) => unknown + /** * Applies the JSON.parse reviver algorithm as defined in the ECMA-262 spec, * in section 24.5.1.1 "Runtime Semantics: InternalizeJSONProperty" of the @@ -5,7 +7,12 @@ * * Includes extensions for handling Map and Set objects. */ -export function applyReviver(reviver, obj, key, val) { +export function applyReviver( + reviver: Reviver, + obj: unknown, + key: unknown, + val: any +) { if (val && typeof val === 'object') { if (Array.isArray(val)) { for (let i = 0, len = val.length; i < len; ++i) { diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index 65ab23eb..8a5a5f14 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -2,6 +2,7 @@ import { Alias } from '../ast/index.js' import { Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' import { defaultTagPrefix } from '../constants.js' +import type { Tag } from '../tags/types.js' import type { Replacer } from './Document.js' import type { Schema } from './Schema.js' @@ -13,7 +14,7 @@ export interface CreateNodeAliasRef { export interface CreateNodeContext { keepUndefined?: boolean onAlias(source: CreateNodeAliasRef): Alias - onTagObj?: (tagObj: Schema.Tag) => void + onTagObj?: (tagObj: Tag) => void prevObjects: Map replacer?: Replacer schema: Schema @@ -23,7 +24,7 @@ export interface CreateNodeContext { function findTagObject( value: unknown, tagName: string | null, - tags: Schema.Tag[] + tags: Tag[] ) { if (tagName) { const match = tags.filter(t => t.tag === tagName) diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 1807bbe5..732d0d44 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -1,5 +1,6 @@ +import { Node } from '../ast/Node.js' +import { visit } from '../visit.js' import type { Document } from './Document.js' -import { listTagNames } from './listTagNames.js' const escapeChars: Record = { '!': '%21', @@ -152,10 +153,21 @@ export class Directives { const lines = this.yaml.explicit ? [`%YAML ${this.yaml.version || '1.2'}`] : [] - const tagNames = doc && listTagNames(doc.contents) - for (const [handle, prefix] of Object.entries(this.tags)) { + + const tagEntries = Object.entries(this.tags) + + let tagNames: string[] + if (doc && tagEntries.length > 0) { + const tags: Record = {} + visit(doc.contents, (_key, node) => { + if (node instanceof Node && node.tag) tags[node.tag] = true + }) + tagNames = Object.keys(tags) + } else tagNames = [] + + for (const [handle, prefix] of tagEntries) { if (handle === '!!' && prefix === 'tag:yaml.org,2002:') continue - if (!tagNames || tagNames.some(tn => tn.startsWith(prefix))) + if (!doc || tagNames.some(tn => tn.startsWith(prefix))) lines.push(`%TAG ${handle} ${prefix}`) } return lines.join('\n') diff --git a/src/doc/getSchemaTags.js b/src/doc/getSchemaTags.js deleted file mode 100644 index 1e848faf..00000000 --- a/src/doc/getSchemaTags.js +++ /dev/null @@ -1,31 +0,0 @@ -export function getSchemaTags(schemas, knownTags, customTags, schemaId) { - let tags = schemas[schemaId.replace(/\W/g, '')] // 'yaml-1.1' -> 'yaml11' - if (!tags) { - const keys = Object.keys(schemas) - .map(key => JSON.stringify(key)) - .join(', ') - throw new Error(`Unknown schema "${schemaId}"; use one of ${keys}`) - } - - if (Array.isArray(customTags)) { - for (const tag of customTags) tags = tags.concat(tag) - } else if (typeof customTags === 'function') { - tags = customTags(tags.slice()) - } - - for (let i = 0; i < tags.length; ++i) { - const tag = tags[i] - if (typeof tag === 'string') { - const tagObj = knownTags[tag] - if (!tagObj) { - const keys = Object.keys(knownTags) - .map(key => JSON.stringify(key)) - .join(', ') - throw new Error(`Unknown custom tag "${tag}"; use one of ${keys}`) - } - tags[i] = tagObj - } - } - - return tags -} diff --git a/src/doc/getSchemaTags.ts b/src/doc/getSchemaTags.ts new file mode 100644 index 00000000..4d1576af --- /dev/null +++ b/src/doc/getSchemaTags.ts @@ -0,0 +1,38 @@ +import type { SchemaId, Tag, TagId } from '../tags/types.js' +import type { SchemaName } from './Schema.js' + +export function getSchemaTags( + schemas: Record, + knownTags: Record, + customTags: + | Array + | ((tags: Array) => Array) + | null + | undefined, + schemaName: SchemaName +) { + const schemaId = schemaName.replace(/\W/g, '') as SchemaId // 'yaml-1.1' -> 'yaml11' + let tags: Array = schemas[schemaId] + if (!tags) { + const keys = Object.keys(schemas) + .map(key => JSON.stringify(key)) + .join(', ') + throw new Error(`Unknown schema "${schemaName}"; use one of ${keys}`) + } + + if (Array.isArray(customTags)) { + for (const tag of customTags) tags = tags.concat(tag) + } else if (typeof customTags === 'function') { + tags = customTags(tags.slice()) + } + + return tags.map(tag => { + if (typeof tag !== 'string') return tag + const tagObj = knownTags[tag] + if (tagObj) return tagObj + const keys = Object.keys(knownTags) + .map(key => JSON.stringify(key)) + .join(', ') + throw new Error(`Unknown custom tag "${tag}"; use one of ${keys}`) + }) +} diff --git a/src/doc/listTagNames.ts b/src/doc/listTagNames.ts deleted file mode 100644 index 41b0b02f..00000000 --- a/src/doc/listTagNames.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Collection, Node, Pair, Scalar } from '../ast/index.js' - -function visit(node: unknown, tags: Record) { - if (node && typeof node === 'object') { - if (node instanceof Collection) { - const { tag } = node - if (tag) tags[tag] = true - node.items.forEach(n => visit(n, tags)) - } else if (node instanceof Pair) { - visit(node.key, tags) - visit(node.value, tags) - } else if (node instanceof Scalar) { - const { tag } = node - if (tag) tags[tag] = true - } - } - return tags -} - -export const listTagNames = (node: Node) => Object.keys(visit(node, {})) diff --git a/src/options.ts b/src/options.ts index 6451eb75..b78afed6 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,5 @@ import { LogLevelId, defaultTagPrefix } from './constants.js' -import type { Schema } from './doc/Schema.js' +import type { SchemaOptions } from './doc/Schema.js' import type { LineCounter } from './parse/line-counter.js' import { binaryOptions, @@ -9,7 +9,7 @@ import { strOptions } from './tags/options.js' -export interface Options extends Schema.Options { +export interface Options extends SchemaOptions { /** * Default prefix for anchors. * diff --git a/src/public-api.ts b/src/public-api.ts index 0b4439da..270b4770 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -73,11 +73,7 @@ export function parseDocument( * nulls, booleans, numbers and strings. */ export function parse(src: string, options?: Options): any -export function parse( - src: string, - reviver: (key: string, value: any) => any, - options?: Options -): any +export function parse(src: string, reviver: Reviver, options?: Options): any export function parse( src: string, diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index f239b6de..f5a3905d 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -3,7 +3,7 @@ import { Node } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' import { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document' -import { Schema } from '../doc/Schema.js' +import type { Tag } from '../tags/types.js' import { stringifyString } from './stringifyString.js' export interface StringifyContext { @@ -19,14 +19,14 @@ export interface StringifyContext { [key: string]: unknown } -function getTagObject(tags: Schema.Tag[], item: Node) { +function getTagObject(tags: Tag[], item: Node) { if (item.tag) { const match = tags.filter(t => t.tag === item.tag) if (match.length > 0) return match.find(t => t.format === (item as Scalar).format) || match[0] } - let tagObj: Schema.Tag | undefined = undefined + let tagObj: Tag | undefined = undefined let obj: unknown if (item instanceof Scalar) { obj = item.value @@ -49,7 +49,7 @@ function getTagObject(tags: Schema.Tag[], item: Node) { // needs to be called before value stringifier to allow for circular anchor refs function stringifyProps( node: Node, - tagObj: Schema.Tag, + tagObj: Tag, { anchors, doc }: StringifyContext ) { const props = [] @@ -75,7 +75,7 @@ export function stringify( if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) if (item instanceof Alias) return item.toString(ctx) - let tagObj: Schema.Tag | undefined = undefined + let tagObj: Tag | undefined = undefined const node: Node = item instanceof Node ? item diff --git a/src/tags/index.d.ts b/src/tags/index.d.ts new file mode 100644 index 00000000..d2f476f0 --- /dev/null +++ b/src/tags/index.d.ts @@ -0,0 +1,4 @@ +import { SchemaId, Tag, TagId } from './types' + +export const schemas: Record +export const tags: Record diff --git a/src/tags/types.ts b/src/tags/types.ts new file mode 100644 index 00000000..9e48542b --- /dev/null +++ b/src/tags/types.ts @@ -0,0 +1,105 @@ +import type { Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' +import type { CreateNodeContext } from '../doc/createNode.js' +import type { Schema } from '../doc/Schema.js' +import type { StringifyContext } from '../stringify/stringify.js' + +export type SchemaId = 'core' | 'failsafe' | 'json' | 'yaml11' + +export type TagId = + | 'binary' + | 'bool' + | 'float' + | 'floatExp' + | 'floatNaN' + | 'floatTime' + | 'int' + | 'intHex' + | 'intOct' + | 'intTime' + | 'null' + | 'omap' + | 'pairs' + | 'set' + | 'timestamp' + +export interface Tag { + /** + * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. + */ + createNode?: ( + schema: Schema, + value: any, + ctx: CreateNodeContext + ) => YAMLMap | YAMLSeq | Scalar + + /** + * If `true`, together with `test` allows for values to be stringified without + * an explicit tag. For most cases, it's unlikely that you'll actually want to + * use this, even if you first think you do. + */ + default: boolean + + /** + * If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them. + */ + format?: string + + /** + * Used by `YAML.createNode` to detect your data type, e.g. using `typeof` or + * `instanceof`. + */ + identify(value: any): boolean + + /** + * The `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations. + */ + nodeClass?: new () => any + + /** + * Used by some tags to configure their stringification, where applicable. + */ + options?: object + + /** + * Turns a value into an AST node. + * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. + */ + resolve( + value: string | YAMLMap | YAMLSeq, + onError: (message: string) => void + ): Node | any + + /** + * Optional function stringifying the AST node in the current context. If your + * data includes a suitable `.toString()` method, you can probably leave this + * undefined and use the default stringifier. + * + * @param item The node being stringified. + * @param ctx Contains the stringifying context variables. + * @param onComment Callback to signal that the stringifier includes the + * item's comment in its output. + * @param onChompKeep Callback to signal that the output uses a block scalar + * type with the `+` chomping indicator. + */ + stringify?: ( + item: Node, + ctx: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) => string + + /** + * The identifier for your data type, with which its stringified form will be + * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified + * `tag:domain,date:foo`. + */ + tag: string + + /** + * Together with `default` allows for values to be stringified without an + * explicit tag and detected using a regular expression. For most cases, it's + * unlikely that you'll actually want to use these, even if you first think + * you do. + */ + test?: RegExp +} From 1be58f80db28c72039ab81cd73611a7825cf952c Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 22 Feb 2021 01:39:50 +0200 Subject: [PATCH 12/24] Refactor Document as TypeScript, dropping wrapScalars option --- src/ast/Alias.ts | 4 +- src/ast/Collection.ts | 8 +- src/ast/Pair.ts | 4 +- src/ast/YAMLMap.ts | 4 +- src/ast/YAMLSeq.ts | 2 +- src/ast/toJS.ts | 19 +- src/doc/Anchors.ts | 2 +- src/doc/Document.d.ts | 153 ---------- src/doc/Document.js | 311 --------------------- src/doc/Document.ts | 495 +++++++++++++++++++++++++++++++++ src/doc/createNode.ts | 29 +- src/doc/directives.ts | 2 +- src/options.ts | 7 +- src/stringify/stringify.ts | 5 +- src/tags/core.js | 3 +- src/tags/json.js | 3 +- src/tags/types.ts | 6 +- src/tags/yaml-1.1/index.js | 3 +- tests/doc/collection-access.js | 4 +- tests/doc/createNode.js | 175 +++--------- tests/doc/types.js | 4 +- 21 files changed, 596 insertions(+), 647 deletions(-) delete mode 100644 src/doc/Document.d.ts delete mode 100644 src/doc/Document.js create mode 100644 src/doc/Document.ts diff --git a/src/ast/Alias.ts b/src/ast/Alias.ts index 5d7966a2..71cc91de 100644 --- a/src/ast/Alias.ts +++ b/src/ast/Alias.ts @@ -29,7 +29,7 @@ export class Alias extends Node { if (!ctx) return toJS(this.source, typeof arg === 'string' ? arg : null, ctx) const { anchors, maxAliasCount } = ctx - const anchor = anchors.get(this.source) + const anchor = anchors && anchors.get(this.source) /* istanbul ignore if */ if (!anchor || anchor.res === undefined) { const msg = 'This should not happen: Alias anchor was not resolved?' @@ -64,7 +64,7 @@ export class Alias extends Node { function getAliasCount(node: unknown, anchors: ToJSContext['anchors']): number { if (node instanceof Alias) { - const anchor = anchors.get(node.source) + const anchor = anchors && anchors.get(node.source) return anchor ? anchor.count * anchor.aliasCount : 0 } else if (node instanceof Collection) { let count = 0 diff --git a/src/ast/Collection.ts b/src/ast/Collection.ts index a551a14b..6fc0f6c6 100644 --- a/src/ast/Collection.ts +++ b/src/ast/Collection.ts @@ -31,13 +31,12 @@ export function collectionFromPath( v = o } } - return createNode(v, null, { + return createNode(v, undefined, { onAlias() { throw new Error('Repeated objects are not supported here') }, prevObjects: new Map(), - schema, - wrapScalars: false + schema }) } @@ -61,9 +60,10 @@ export declare namespace Collection { export abstract class Collection extends Node { static maxFlowStringSingleLineLength = 60 - items: unknown[] = [] schema: Schema | undefined + declare items: unknown[] + declare type?: | Type.MAP | Type.FLOW_MAP diff --git a/src/ast/Pair.ts b/src/ast/Pair.ts index 3d755c98..fb5f70ff 100644 --- a/src/ast/Pair.ts +++ b/src/ast/Pair.ts @@ -16,8 +16,8 @@ export function createPair( value: unknown, ctx: CreateNodeContext ) { - const k = createNode(key, null, ctx) - const v = createNode(value, null, ctx) + const k = createNode(key, undefined, ctx) + const v = createNode(value, undefined, ctx) return new Pair(k, v) } diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index b563e300..d48a1edc 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -28,9 +28,9 @@ export class YAMLMap extends Collection { return 'tag:yaml.org,2002:map' } - type?: Type.FLOW_MAP | Type.MAP + items: Pair[] = [] - declare items: Pair[] + type?: Type.FLOW_MAP | Type.MAP /** * Adds a value to the collection. diff --git a/src/ast/YAMLSeq.ts b/src/ast/YAMLSeq.ts index 497b4e98..cd7a8064 100644 --- a/src/ast/YAMLSeq.ts +++ b/src/ast/YAMLSeq.ts @@ -16,7 +16,7 @@ export class YAMLSeq extends Collection { return 'tag:yaml.org,2002:seq' } - declare items: T[] + items: T[] = [] type?: Type.FLOW_SEQ | Type.SEQ diff --git a/src/ast/toJS.ts b/src/ast/toJS.ts index 9846e2bb..f7e3daa6 100644 --- a/src/ast/toJS.ts +++ b/src/ast/toJS.ts @@ -1,11 +1,16 @@ -import { Document } from '../doc/Document.js' +import type { Document } from '../doc/Document.js' +import type { stringify } from '../stringify/stringify.js' import { Node } from './index.js' +export interface ToJSAnchorValue { + alias: string[] + aliasCount: number + count: number + res?: unknown +} + export interface ToJSContext { - anchors: Map< - Node, - { alias: string[]; aliasCount: number; count: number; res?: unknown } - > + anchors: Map | null doc: Document indentStep: string keep: boolean @@ -14,8 +19,8 @@ export interface ToJSContext { maxAliasCount: number onCreate?: (res: unknown) => void - /** Requiring directly in Pair would create circular dependencies */ - stringify: () => string + /** Requiring this directly in Pair would create circular dependencies */ + stringify: typeof stringify } /** diff --git a/src/doc/Anchors.ts b/src/doc/Anchors.ts index f380c6e8..6c0f3a9d 100644 --- a/src/doc/Anchors.ts +++ b/src/doc/Anchors.ts @@ -1,7 +1,7 @@ import { Alias, Merge, Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' export class Anchors { - private map: Record = Object.create(null) + map: Record = Object.create(null) private prefix: string constructor(prefix: string) { diff --git a/src/doc/Document.d.ts b/src/doc/Document.d.ts deleted file mode 100644 index b577848c..00000000 --- a/src/doc/Document.d.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Collection, Node, Pair } from '../ast' -import { Type } from '../constants' -import { YAMLError, YAMLWarning } from '../errors' -import { Options } from '../options' -import type { Tag, TagId } from '../tags/types' -import { Anchors } from './Anchors' -import { Reviver } from './applyReviver' -import { Directives } from './directives' -import { Schema, SchemaName } from './Schema' - -export type Replacer = any[] | ((key: any, value: any) => boolean) -export type { Anchors, Reviver } - -export interface CreateNodeOptions { - onTagObj?: (tagObj: Tag) => void - - /** - * Filter or modify values while creating a node. - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter - */ - replacer?: Replacer - /** - * Specify the collection type, e.g. `"!!omap"`. Note that this requires the - * corresponding tag to be available in this document's schema. - */ - tag?: string - /** - * Wrap plain values in `Scalar` objects. - * - * Default: `true` - */ - wrapScalars?: boolean -} - -export class Document extends Collection { - // cstNode?: CST.Document - /** - * @param value - The initial value for the document, which will be wrapped - * in a Node container. - */ - constructor(value?: any, options?: Options) - constructor(value: any, replacer: null | Replacer, options?: Options) - - directives: Directives - - tag: never - directivesEndMarker?: boolean - type: Type.DOCUMENT - /** - * Anchors associated with the document's nodes; - * also provides alias & merge node creators. - */ - anchors: Anchors - /** The document contents. */ - contents: any - /** Errors encountered during parsing. */ - errors: YAMLError[] - /** - * The schema used with the document. Use `setSchema()` to change or - * initialise. - */ - options: Required - - // FIXME required by Collection, currently optional in Document - declare schema: Schema - - /** - * Array of prefixes; each will have a string `handle` that - * starts and ends with `!` and a string `prefix` that the handle will be replaced by. - */ - tagPrefixes: Document.TagPrefix[] - /** - * The parsed version of the source document; - * if true-ish, stringified output will include a `%YAML` directive. - */ - version?: string - /** Warnings encountered during parsing. */ - warnings: YAMLWarning[] - - add(value: unknown): void - delete(key: unknown): boolean - get(key: unknown, keepScalar?: boolean): unknown - has(key: unknown): boolean - set(key: unknown, value: unknown): void - - /** - * Convert any value into a `Node` using the current schema, recursively - * turning objects into collections. - */ - createNode( - value: any, - { replacer, tag, wrapScalars }?: CreateNodeOptions - ): Node - /** - * Convert a key and a value into a `Pair` using the current schema, - * recursively wrapping all values as `Scalar` or `Collection` nodes. - * - * @param options If `wrapScalars` is not `false`, wraps plain values in - * `Scalar` objects. - */ - createPair(key: any, value: any, options?: { wrapScalars?: boolean }): Pair - - /** - * When a document is created with `new YAML.Document()`, the schema object is - * not set as it may be influenced by parsed directives; call this with no - * arguments to set it manually, or with arguments to change the schema used - * by the document. - */ - setSchema( - id?: Options['version'] | SchemaName, - customTags?: (TagId | Tag)[] - ): void - /** Set `handle` as a shorthand string for the `prefix` tag namespace. */ - setTagPrefix(handle: string, prefix: string): void - /** - * A plain JavaScript representation of the document `contents`. - * - * @param mapAsMap - Use Map rather than Object to represent mappings. - * Overrides values set in Document or global options. - * @param onAnchor - If defined, called with the resolved `value` and - * reference `count` for each anchor in the document. - * @param reviver - A function that may filter or modify the output JS value - */ - toJS(opt?: { - mapAsMap?: boolean - onAnchor?: (value: any, count: number) => void - reviver?: Reviver - }): any - /** - * A JSON representation of the document `contents`. - * - * @param arg Used by `JSON.stringify` to indicate the array index or property - * name. - */ - toJSON(arg?: string): any - /** A YAML representation of the document. */ - toString(): string -} - -export namespace Document { - interface Parsed extends Document { - contents: Node.Parsed | null - range: [number, number] - /** The schema used with the document. */ - schema: Schema - } - - interface TagPrefix { - handle: string - prefix: string - } -} diff --git a/src/doc/Document.js b/src/doc/Document.js deleted file mode 100644 index ecdc79c0..00000000 --- a/src/doc/Document.js +++ /dev/null @@ -1,311 +0,0 @@ -import { - Alias, - Collection, - Node, - Pair, - Scalar, - collectionFromPath, - isEmptyPath, - toJS -} from '../ast/index.js' -import { defaultOptions, documentOptions } from '../options.js' -import { addComment } from '../stringify/addComment.js' -import { stringify } from '../stringify/stringify.js' - -import { Anchors } from './Anchors.js' -import { Schema } from './Schema.js' -import { applyReviver } from './applyReviver.js' -import { createNode } from './createNode.js' -import { Directives } from './directives.js' - -function assertCollection(contents) { - if (contents instanceof Collection) return true - throw new Error('Expected a YAML collection as document contents') -} - -export class Document { - static defaults = documentOptions - - constructor(value, replacer, options) { - if ( - options === undefined && - replacer && - typeof replacer === 'object' && - !Array.isArray(replacer) - ) { - options = replacer - replacer = undefined - } - - this.options = Object.assign({}, defaultOptions, options) - this.anchors = new Anchors(this.options.anchorPrefix) - this.commentBefore = null - this.comment = null - this.directives = new Directives({ version: this.options.version }) - this.directivesEndMarker = null - this.errors = [] - this.schema = null - this.tagPrefixes = [] - this.warnings = [] - - // note that this.schema is left as null here - this.contents = - value === undefined ? null : this.createNode(value, { replacer }) - } - - add(value) { - assertCollection(this.contents) - return this.contents.add(value) - } - - addIn(path, value) { - assertCollection(this.contents) - this.contents.addIn(path, value) - } - - createNode( - value, - { keepUndefined, onTagObj, replacer, tag, wrapScalars } = {} - ) { - this.setSchema() - if (typeof replacer === 'function') - value = replacer.call({ '': value }, '', value) - else if (Array.isArray(replacer)) { - const keyToStr = v => - typeof v === 'number' || v instanceof String || v instanceof Number - const asStr = replacer.filter(keyToStr).map(String) - if (asStr.length > 0) replacer = replacer.concat(asStr) - } - if (typeof keepUndefined !== 'boolean') - keepUndefined = !!this.options.keepUndefined - const aliasNodes = [] - const ctx = { - keepUndefined, - onAlias(source) { - const alias = new Alias(source) - aliasNodes.push(alias) - return alias - }, - onTagObj, - prevObjects: new Map(), - replacer, - schema: this.schema, - wrapScalars: wrapScalars !== false - } - const node = createNode(value, tag, ctx) - for (const alias of aliasNodes) { - // With circular references, the source node is only resolved after all of - // its child nodes are. This is why anchors are set only after all of the - // nodes have been created. - alias.source = alias.source.node - let name = this.anchors.getName(alias.source) - if (!name) { - name = this.anchors.newName() - this.anchors.map[name] = alias.source - } - } - return node - } - - createPair(key, value, options = {}) { - const k = this.createNode(key, options) - const v = this.createNode(value, options) - return new Pair(k, v) - } - - delete(key) { - assertCollection(this.contents) - return this.contents.delete(key) - } - - deleteIn(path) { - if (isEmptyPath(path)) { - if (this.contents == null) return false - this.contents = null - return true - } - assertCollection(this.contents) - return this.contents.deleteIn(path) - } - - getDefaults() { - return ( - Document.defaults[this.directives.yaml.version] || - Document.defaults[this.options.version] || - {} - ) - } - - get(key, keepScalar) { - return this.contents instanceof Collection - ? this.contents.get(key, keepScalar) - : undefined - } - - getIn(path, keepScalar) { - if (isEmptyPath(path)) - return !keepScalar && this.contents instanceof Scalar - ? this.contents.value - : this.contents - return this.contents instanceof Collection - ? this.contents.getIn(path, keepScalar) - : undefined - } - - has(key) { - return this.contents instanceof Collection ? this.contents.has(key) : false - } - - hasIn(path) { - if (isEmptyPath(path)) return this.contents !== undefined - return this.contents instanceof Collection - ? this.contents.hasIn(path) - : false - } - - set(key, value) { - if (this.contents == null) { - this.setSchema() - this.contents = collectionFromPath(this.schema, [key], value) - } else { - assertCollection(this.contents) - this.contents.set(key, value) - } - } - - setIn(path, value) { - if (isEmptyPath(path)) this.contents = value - else if (this.contents == null) { - this.setSchema() - this.contents = collectionFromPath(this.schema, path, value) - } else { - assertCollection(this.contents) - this.contents.setIn(path, value) - } - } - - setSchema(id, customTags) { - if (!id && !customTags && this.schema) return - if (typeof id === 'number') id = id.toFixed(1) - if (id === '1.1' || id === '1.2') { - this.directives.yaml.version = id - delete this.options.schema - } else if (id && typeof id === 'string') { - this.options.schema = id - } - if (Array.isArray(customTags)) this.options.customTags = customTags - const opt = Object.assign({}, this.getDefaults(), this.options) - this.schema = new Schema(opt) - } - - setTagPrefix(handle, prefix) { - if (handle[0] !== '!' || handle[handle.length - 1] !== '!') - throw new Error('Handle must start and end with !') - if (prefix) { - const prev = this.tagPrefixes.find(p => p.handle === handle) - if (prev) prev.prefix = prefix - else this.tagPrefixes.push({ handle, prefix }) - } else { - this.tagPrefixes = this.tagPrefixes.filter(p => p.handle !== handle) - } - } - - toJS({ json, jsonArg, mapAsMap, onAnchor, reviver } = {}) { - const anchorNodes = Object.values(this.anchors.map).map(node => [ - node, - { alias: [], aliasCount: 0, count: 1 } - ]) - const anchors = anchorNodes.length > 0 ? new Map(anchorNodes) : null - const ctx = { - anchors, - doc: this, - indentStep: ' ', - keep: !json, - mapAsMap: - typeof mapAsMap === 'boolean' ? mapAsMap : !!this.options.mapAsMap, - mapKeyWarned: false, - maxAliasCount: this.options.maxAliasCount, - stringify // Requiring directly in Pair would create circular dependencies - } - const res = toJS(this.contents, jsonArg || '', ctx) - if (typeof onAnchor === 'function' && anchors) - for (const { count, res } of anchors.values()) onAnchor(res, count) - return typeof reviver === 'function' - ? applyReviver(reviver, { '': res }, '', res) - : res - } - - toJSON(jsonArg, onAnchor) { - return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor }) - } - - toString() { - if (this.errors.length > 0) - throw new Error('Document with errors cannot be stringified') - const indentSize = this.options.indent - if (!Number.isInteger(indentSize) || indentSize <= 0) { - const s = JSON.stringify(indentSize) - throw new Error(`"indent" option must be a positive integer, not ${s}`) - } - this.setSchema() - const lines = [] - let hasDirectives = false - const dir = this.directives.toString(this) - if (dir) { - lines.push(dir) - hasDirectives = true - } - if (hasDirectives || this.directivesEndMarker) lines.push('---') - if (this.commentBefore) { - if (hasDirectives || !this.directivesEndMarker) lines.unshift('') - lines.unshift(this.commentBefore.replace(/^/gm, '#')) - } - const ctx = { - anchors: Object.create(null), - doc: this, - indent: '', - indentStep: ' '.repeat(indentSize), - stringify // Requiring directly in nodes would create circular dependencies - } - let chompKeep = false - let contentComment = null - if (this.contents) { - if (this.contents instanceof Node) { - if ( - this.contents.spaceBefore && - (hasDirectives || this.directivesEndMarker) - ) - lines.push('') - if (this.contents.commentBefore) - lines.push(this.contents.commentBefore.replace(/^/gm, '#')) - // top-level block scalars need to be indented if followed by a comment - ctx.forceBlockIndent = !!this.comment - contentComment = this.contents.comment - } - const onChompKeep = contentComment ? null : () => (chompKeep = true) - let body = stringify( - this.contents, - ctx, - () => (contentComment = null), - onChompKeep - ) - if (contentComment) body = addComment(body, '', contentComment) - if ( - (body[0] === '|' || body[0] === '>') && - lines[lines.length - 1] === '---' - ) { - // Top-level block scalars with a preceding doc marker ought to use the - // same line for their header. - lines[lines.length - 1] = `--- ${body}` - } else lines.push(body) - } else { - lines.push(stringify(this.contents, ctx)) - } - if (this.comment) { - if ((!chompKeep || contentComment) && lines[lines.length - 1] !== '') - lines.push('') - lines.push(this.comment.replace(/^/gm, '#')) - } - return lines.join('\n') + '\n' - } -} diff --git a/src/doc/Document.ts b/src/doc/Document.ts new file mode 100644 index 00000000..df7acc75 --- /dev/null +++ b/src/doc/Document.ts @@ -0,0 +1,495 @@ +import { + Alias, + Collection, + Node, + Pair, + Scalar, + YAMLMap, + YAMLSeq, + collectionFromPath, + isEmptyPath, + toJS, + ToJSContext +} from '../ast/index.js' +import { Type } from '../constants' +import type { YAMLError, YAMLWarning } from '../errors' +import { + DocumentOptions, + Options, + defaultOptions, + documentOptions +} from '../options.js' +import { addComment } from '../stringify/addComment.js' +import { stringify, StringifyContext } from '../stringify/stringify.js' +import type { Tag, TagId } from '../tags/types' + +import { Anchors } from './Anchors.js' +import { Schema, SchemaName, SchemaOptions } from './Schema.js' +import { Reviver, applyReviver } from './applyReviver.js' +import { createNode, CreateNodeContext } from './createNode.js' +import { Directives } from './directives.js' +import { ToJSAnchorValue } from '../ast/toJS.js' + +export type Replacer = any[] | ((key: any, value: any) => unknown) +export type { Anchors, Reviver } + +export interface CreateNodeOptions { + keepUndefined?: boolean | null + + onTagObj?: (tagObj: Tag) => void + + /** + * Filter or modify values while creating a node. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter + */ + replacer?: Replacer + + /** + * Specify the collection type, e.g. `"!!omap"`. Note that this requires the + * corresponding tag to be available in this document's schema. + */ + tag?: string +} + +export interface ToJSOptions { + json?: boolean + jsonArg?: string | null + mapAsMap?: boolean + onAnchor?: (value: unknown, count: number) => void + reviver?: Reviver +} + +export declare namespace Document { + interface Parsed extends Document { + contents: Node.Parsed | null + range: [number, number] + /** The schema used with the document. */ + schema: Schema + } + + interface TagPrefix { + handle: string + prefix: string + } +} + +export class Document { + static defaults = documentOptions + + /** + * Anchors associated with the document's nodes; + * also provides alias & merge node creators. + */ + anchors: Anchors + + /** A comment before this Document */ + commentBefore: string | null = null + + /** A comment immediately after this Document */ + comment: string | null = null + + /** The document contents. */ + contents: unknown + + directives: Directives + + directivesEndMarker: boolean = false + + /** Errors encountered during parsing. */ + errors: YAMLError[] = [] + + options: Required & SchemaOptions + + // FIXME required by Collection, currently optional in Document + /** + * The schema used with the document. Use `setSchema()` to change or + * initialise. + */ + declare schema: Schema + + /** + * Array of prefixes; each will have a string `handle` that + * starts and ends with `!` and a string `prefix` that the handle will be replaced by. + */ + tagPrefixes: Document.TagPrefix[] = [] + + type: Type.DOCUMENT = Type.DOCUMENT + + /** + * The parsed version of the source document; + * if true-ish, stringified output will include a `%YAML` directive. + */ + version?: string + + /** Warnings encountered during parsing. */ + warnings: YAMLWarning[] = [] + + /** + * @param value - The initial value for the document, which will be wrapped + * in a Node container. + */ + constructor(value?: any, options?: Options) + constructor(value: any, replacer: null | Replacer, options?: Options) + constructor( + value?: unknown, + replacer?: Replacer | Options | null, + options?: Options + ) { + let _replacer: Replacer | undefined = undefined + if (typeof replacer === 'function' || Array.isArray(replacer)) { + _replacer = replacer + } else if (options === undefined && replacer) { + options = replacer + replacer = undefined + } + + this.options = Object.assign({}, defaultOptions, options) + this.anchors = new Anchors(this.options.anchorPrefix) + this.directives = new Directives({ version: this.options.version }) + + // @ts-ignore FIXME + this.schema = null + + // note that this.schema is left as null here + this.contents = + value === undefined + ? null + : this.createNode(value, { replacer: _replacer }) + } + + /** Adds a value to the document. */ + add(value: any) { + if (assertCollection(this.contents)) this.contents.add(value) + } + + /** Adds a value to the document. */ + addIn(path: Iterable, value: unknown) { + if (assertCollection(this.contents)) this.contents.addIn(path, value) + } + + /** + * Convert any value into a `Node` using the current schema, recursively + * turning objects into collections. + */ + createNode( + value: unknown, + { keepUndefined, onTagObj, replacer, tag }: CreateNodeOptions = {} + ): Node { + this.setSchema() + if (typeof replacer === 'function') + value = replacer.call({ '': value }, '', value) + else if (Array.isArray(replacer)) { + const keyToStr = (v: unknown) => + typeof v === 'number' || v instanceof String || v instanceof Number + const asStr = replacer.filter(keyToStr).map(String) + if (asStr.length > 0) replacer = replacer.concat(asStr) + } + if (typeof keepUndefined !== 'boolean') + keepUndefined = !!this.options.keepUndefined + const aliasNodes: Alias[] = [] + const ctx: CreateNodeContext = { + keepUndefined, + onAlias(source) { + // These get fixed later in createNode() + const alias = new Alias((source as unknown) as Node) + aliasNodes.push(alias) + return alias + }, + onTagObj, + prevObjects: new Map(), + replacer, + schema: this.schema + } + const node = createNode(value, tag, ctx) + for (const alias of aliasNodes) { + // With circular references, the source node is only resolved after all of + // its child nodes are. This is why anchors are set only after all of the + // nodes have been created. + alias.source = (alias.source as any).node as Node + let name = this.anchors.getName(alias.source) + if (!name) { + name = this.anchors.newName() + this.anchors.map[name] = alias.source + } + } + return node + } + + /** + * Convert a key and a value into a `Pair` using the current schema, + * recursively wrapping all values as `Scalar` or `Collection` nodes. + */ + createPair( + key: unknown, + value: unknown, + options: CreateNodeOptions = {} + ) { + const k = this.createNode(key, options) as K + const v = this.createNode(value, options) as V + return new Pair(k, v) + } + + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + delete(key: any) { + return assertCollection(this.contents) ? this.contents.delete(key) : false + } + + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + deleteIn(path: Iterable) { + if (isEmptyPath(path)) { + if (this.contents == null) return false + this.contents = null + return true + } + return assertCollection(this.contents) + ? this.contents.deleteIn(path) + : false + } + + getDefaults() { + return ( + Document.defaults[this.directives.yaml.version] || + Document.defaults[this.options.version] || + {} + ) + } + + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + get(key: unknown, keepScalar?: boolean) { + return this.contents instanceof Collection + ? this.contents.get(key, keepScalar) + : undefined + } + + /** + * Returns item at `path`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path: Iterable, keepScalar?: boolean) { + if (isEmptyPath(path)) + return !keepScalar && this.contents instanceof Scalar + ? this.contents.value + : this.contents + return this.contents instanceof Collection + ? this.contents.getIn(path, keepScalar) + : undefined + } + + /** + * Checks if the document includes a value with the key `key`. + */ + has(key: unknown) { + return this.contents instanceof Collection ? this.contents.has(key) : false + } + + /** + * Checks if the document includes a value at `path`. + */ + hasIn(path: Iterable) { + if (isEmptyPath(path)) return this.contents !== undefined + return this.contents instanceof Collection + ? this.contents.hasIn(path) + : false + } + + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + set(key: any, value: unknown) { + if (this.contents == null) { + this.setSchema() + this.contents = collectionFromPath(this.schema, [key], value) + } else if (assertCollection(this.contents)) { + this.contents.set(key, value) + } + } + + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path: Iterable, value: unknown) { + if (isEmptyPath(path)) this.contents = value + else if (this.contents == null) { + this.setSchema() + this.contents = collectionFromPath(this.schema, Array.from(path), value) + } else if (assertCollection(this.contents)) { + this.contents.setIn(path, value) + } + } + + /** + * When a document is created with `new YAML.Document()`, the schema object is + * not set as it may be influenced by parsed directives; call this with no + * arguments to set it manually, or with arguments to change the schema used + * by the document. + */ + setSchema( + id?: Options['version'] | SchemaName, + customTags?: (TagId | Tag)[] + ) { + if (!id && !customTags && this.schema) return + + // @ts-ignore Never happens in TypeScript + if (typeof id === 'number') id = id.toFixed(1) + + if (id === '1.1' || id === '1.2') { + this.directives.yaml.version = id + delete this.options.schema + } else if (id && typeof id === 'string') { + this.options.schema = id + } + if (Array.isArray(customTags)) this.options.customTags = customTags + const opt = Object.assign({}, this.getDefaults(), this.options) + this.schema = new Schema(opt) + } + + /** Set `handle` as a shorthand string for the `prefix` tag namespace. */ + setTagPrefix(handle: string, prefix: string | null) { + if (handle[0] !== '!' || handle[handle.length - 1] !== '!') + throw new Error('Handle must start and end with !') + if (prefix) { + const prev = this.tagPrefixes.find(p => p.handle === handle) + if (prev) prev.prefix = prefix + else this.tagPrefixes.push({ handle, prefix }) + } else { + this.tagPrefixes = this.tagPrefixes.filter(p => p.handle !== handle) + } + } + + /** + * A plain JavaScript representation of the document `contents`. + * + * @param mapAsMap - Use Map rather than Object to represent mappings. + * Overrides values set in Document or global options. + * @param onAnchor - If defined, called with the resolved `value` and + * reference `count` for each anchor in the document. + * @param reviver - A function that may filter or modify the output JS value + */ + toJS({ json, jsonArg, mapAsMap, onAnchor, reviver }: ToJSOptions = {}) { + const anchorNodes = Object.values(this.anchors.map).map( + node => + [node, { alias: [], aliasCount: 0, count: 1 }] as [ + Node, + ToJSAnchorValue + ] + ) + const anchors = anchorNodes.length > 0 ? new Map(anchorNodes) : null + const ctx: ToJSContext = { + anchors, + doc: this, + indentStep: ' ', + keep: !json, + mapAsMap: + typeof mapAsMap === 'boolean' ? mapAsMap : !!this.options.mapAsMap, + mapKeyWarned: false, + maxAliasCount: this.options.maxAliasCount, + stringify + } + const res = toJS(this.contents, jsonArg || '', ctx) + if (typeof onAnchor === 'function' && anchors) + for (const { count, res } of anchors.values()) onAnchor(res, count) + return typeof reviver === 'function' + ? applyReviver(reviver, { '': res }, '', res) + : res + } + + /** + * A JSON representation of the document `contents`. + * + * @param jsonArg Used by `JSON.stringify` to indicate the array index or + * property name. + */ + toJSON(jsonArg?: string | null, onAnchor?: ToJSOptions['onAnchor']) { + return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor }) + } + + /** A YAML representation of the document. */ + toString() { + if (this.errors.length > 0) + throw new Error('Document with errors cannot be stringified') + const indentSize = this.options.indent + if (!Number.isInteger(indentSize) || indentSize <= 0) { + const s = JSON.stringify(indentSize) + throw new Error(`"indent" option must be a positive integer, not ${s}`) + } + this.setSchema() + const lines = [] + let hasDirectives = false + const dir = this.directives.toString(this) + if (dir) { + lines.push(dir) + hasDirectives = true + } + if (hasDirectives || this.directivesEndMarker) lines.push('---') + if (this.commentBefore) { + if (hasDirectives || !this.directivesEndMarker) lines.unshift('') + lines.unshift(this.commentBefore.replace(/^/gm, '#')) + } + const ctx: StringifyContext = { + anchors: Object.create(null), + doc: this, + indent: '', + indentStep: ' '.repeat(indentSize), + stringify // Requiring directly in nodes would create circular dependencies + } + let chompKeep = false + let contentComment = null + if (this.contents) { + if (this.contents instanceof Node) { + if ( + this.contents.spaceBefore && + (hasDirectives || this.directivesEndMarker) + ) + lines.push('') + if (this.contents.commentBefore) + lines.push(this.contents.commentBefore.replace(/^/gm, '#')) + // top-level block scalars need to be indented if followed by a comment + ctx.forceBlockIndent = !!this.comment + contentComment = this.contents.comment + } + const onChompKeep = contentComment ? undefined : () => (chompKeep = true) + let body = stringify( + this.contents, + ctx, + () => (contentComment = null), + onChompKeep + ) + if (contentComment) body = addComment(body, '', contentComment) + if ( + (body[0] === '|' || body[0] === '>') && + lines[lines.length - 1] === '---' + ) { + // Top-level block scalars with a preceding doc marker ought to use the + // same line for their header. + lines[lines.length - 1] = `--- ${body}` + } else lines.push(body) + } else { + lines.push(stringify(this.contents, ctx)) + } + if (this.comment) { + if ((!chompKeep || contentComment) && lines[lines.length - 1] !== '') + lines.push('') + lines.push(this.comment.replace(/^/gm, '#')) + } + return lines.join('\n') + '\n' + } +} + +function assertCollection(contents: unknown): contents is YAMLMap | YAMLSeq { + if (contents instanceof Collection) return true + throw new Error('Expected a YAML collection as document contents') +} diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index 8a5a5f14..1d10aa51 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -1,13 +1,15 @@ -import { Alias } from '../ast/index.js' +import type { Alias } from '../ast/Alias.js' import { Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' +import type { YAMLMap } from '../ast/YAMLMap.js' +import type { YAMLSeq } from '../ast/YAMLSeq.js' import { defaultTagPrefix } from '../constants.js' import type { Tag } from '../tags/types.js' import type { Replacer } from './Document.js' import type { Schema } from './Schema.js' export interface CreateNodeAliasRef { - node: unknown + node: Scalar | YAMLMap | YAMLSeq | undefined value: unknown } @@ -18,12 +20,11 @@ export interface CreateNodeContext { prevObjects: Map replacer?: Replacer schema: Schema - wrapScalars: boolean } function findTagObject( value: unknown, - tagName: string | null, + tagName: string | undefined, tags: Tag[] ) { if (tagName) { @@ -37,11 +38,11 @@ function findTagObject( export function createNode( value: unknown, - tagName: string | null, + tagName: string | undefined, ctx: CreateNodeContext -) { +): Node { if (value instanceof Node) return value - const { onAlias, onTagObj, prevObjects, wrapScalars } = ctx + const { onAlias, onTagObj, prevObjects } = ctx const { map, seq, tags } = ctx.schema if (tagName && tagName.startsWith('!!')) tagName = defaultTagPrefix + tagName.slice(2) @@ -50,8 +51,7 @@ export function createNode( if (!tagObj) { if (value && typeof (value as any).toJSON === 'function') value = (value as any).toJSON() - if (!value || typeof value !== 'object') - return wrapScalars ? new Scalar(value) : value + if (!value || typeof value !== 'object') return new Scalar(value) tagObj = value instanceof Map ? map : Symbol.iterator in Object(value) ? seq : map } @@ -70,12 +70,11 @@ export function createNode( prevObjects.set(value, ref) } - ref.node = tagObj.createNode + const node = tagObj.createNode ? tagObj.createNode(ctx.schema, value, ctx) - : wrapScalars - ? new Scalar(value) - : value - if (tagName && ref.node instanceof Node) ref.node.tag = tagName + : new Scalar(value) + if (tagName) node.tag = tagName + ref.node = node - return ref.node + return node } diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 732d0d44..475582c4 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -157,7 +157,7 @@ export class Directives { const tagEntries = Object.entries(this.tags) let tagNames: string[] - if (doc && tagEntries.length > 0) { + if (doc && tagEntries.length > 0 && doc.contents instanceof Node) { const tags: Record = {} visit(doc.contents, (_key, node) => { if (node instanceof Node && node.tag) tags[node.tag] = true diff --git a/src/options.ts b/src/options.ts index b78afed6..50476df3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -9,7 +9,7 @@ import { strOptions } from './tags/options.js' -export interface Options extends SchemaOptions { +export interface DocumentOptions { /** * Default prefix for anchors. * @@ -100,6 +100,8 @@ export interface Options extends SchemaOptions { version?: '1.1' | '1.2' } +export type Options = DocumentOptions & SchemaOptions + /** * `yaml` defines document-specific options in three places: as an argument of * parse, create and stringify calls, in the values of `YAML.defaultOptions`, @@ -107,9 +109,8 @@ export interface Options extends SchemaOptions { * `YAML.defaultOptions` override version-dependent defaults, and argument * options override both. */ -export const defaultOptions: Options = { +export const defaultOptions: Required = { anchorPrefix: 'a', - customTags: null, indent: 2, indentSeq: true, keepCstNodes: false, diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index f5a3905d..dcf74057 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -79,10 +79,7 @@ export function stringify( const node: Node = item instanceof Node ? item - : ctx.doc.createNode(item, { - onTagObj: o => (tagObj = o), - wrapScalars: true - }) + : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) if (!tagObj) tagObj = getTagObject(ctx.doc.schema.tags, node) diff --git a/src/tags/core.js b/src/tags/core.js index 1b3b84de..260e3a5d 100644 --- a/src/tags/core.js +++ b/src/tags/core.js @@ -19,8 +19,7 @@ function intStringify(node, radix, prefix) { export const nullObj = { identify: value => value == null, - createNode: (schema, value, ctx) => - ctx.wrapScalars ? new Scalar(null) : null, + createNode: () => new Scalar(null), default: true, tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, diff --git a/src/tags/json.js b/src/tags/json.js index abe6f7fe..36a769ba 100644 --- a/src/tags/json.js +++ b/src/tags/json.js @@ -22,8 +22,7 @@ export const json = [ }, { identify: value => value == null, - createNode: (schema, value, ctx) => - ctx.wrapScalars ? new Scalar(null) : null, + createNode: () => new Scalar(null), default: true, tag: 'tag:yaml.org,2002:null', test: /^null$/, diff --git a/src/tags/types.ts b/src/tags/types.ts index 9e48542b..098abea4 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -26,11 +26,11 @@ export interface Tag { /** * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. */ - createNode?: ( + createNode?: ( schema: Schema, - value: any, + value: T, ctx: CreateNodeContext - ) => YAMLMap | YAMLSeq | Scalar + ) => YAMLMap | YAMLSeq | Scalar /** * If `true`, together with `test` allows for values to be stringified without diff --git a/src/tags/yaml-1.1/index.js b/src/tags/yaml-1.1/index.js index 358eb1b7..a3042d2a 100644 --- a/src/tags/yaml-1.1/index.js +++ b/src/tags/yaml-1.1/index.js @@ -12,8 +12,7 @@ import { intTime, floatTime, timestamp } from './timestamp.js' const nullObj = { identify: value => value == null, - createNode: (schema, value, ctx) => - ctx.wrapScalars ? new Scalar(null) : null, + createNode: () => new Scalar(null), default: true, tag: 'tag:yaml.org,2002:null', test: /^(?:~|[Nn]ull|NULL)?$/, diff --git a/tests/doc/collection-access.js b/tests/doc/collection-access.js index ebad72d9..56476db3 100644 --- a/tests/doc/collection-access.js +++ b/tests/doc/collection-access.js @@ -506,7 +506,9 @@ describe('Document', () => { test('setIn on empty document', () => { doc.contents = null doc.setIn(['a', 2], 1) - expect(doc.get('a')).toMatchObject({ items: [null, null, 1] }) + expect(doc.get('a')).toMatchObject({ + items: [{ value: null }, { value: null }, { value: 1 }] + }) }) test('setIn on parsed document', () => { diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js index 842aee4c..903a8f50 100644 --- a/tests/doc/createNode.js +++ b/tests/doc/createNode.js @@ -1,5 +1,11 @@ import * as YAML from '../../src/index.js' -import { Pair, PairType, Scalar, YAMLMap, YAMLSeq } from '../../src/ast/index.js' +import { + Pair, + PairType, + Scalar, + YAMLMap, + YAMLSeq +} from '../../src/ast/index.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' let doc @@ -7,31 +13,6 @@ beforeEach(() => { doc = new YAML.Document() }) -describe('scalars', () => { - describe('createNode(value, { wrapScalars: false })', () => { - test('boolean', () => { - const s = doc.createNode(false, { wrapScalars: false }) - expect(s).toBe(false) - }) - test('null', () => { - const s = doc.createNode(null, { wrapScalars: false }) - expect(s).toBeNull() - }) - test('undefined', () => { - const s = doc.createNode(undefined, { wrapScalars: false }) - expect(s).toBeNull() - }) - test('number', () => { - const s = doc.createNode(3, { wrapScalars: false }) - expect(s).toBe(3) - }) - test('string', () => { - const s = doc.createNode('test', { wrapScalars: false }) - expect(s).toBe('test') - }) - }) -}) - describe('createNode(value)', () => { test('boolean', () => { const s = doc.createNode(false) @@ -61,15 +42,7 @@ describe('createNode(value)', () => { }) describe('explicit tags', () => { - test('wrapScalars: false', () => { - const s = doc.createNode(3, { - tag: 'tag:yaml.org,2002:str', - wrapScalars: false - }) - expect(s).toBe(3) - }) - - test('wrapScalars: true', () => { + test('default tag', () => { const s = doc.createNode(3, { tag: '!!str' }) expect(s).toBeInstanceOf(Scalar) expect(s).toMatchObject({ value: 3, tag: 'tag:yaml.org,2002:str' }) @@ -88,21 +61,13 @@ describe('arrays', () => { expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toHaveLength(0) }) - test('createNode([true], { wrapScalars: false })', () => { - const s = doc.createNode([true], { wrapScalars: false }) + test('createNode([true])', () => { + const s = doc.createNode([true]) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([true]) + expect(s.items).toMatchObject([{ value: true }]) }) describe('[3, ["four", 5]]', () => { const array = [3, ['four', 5]] - test('createNode(value, { wrapScalars: false })', () => { - const s = doc.createNode(array, { wrapScalars: false }) - expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(2) - expect(s.items[0]).toBe(3) - expect(s.items[1]).toBeInstanceOf(YAMLSeq) - expect(s.items[1].items).toMatchObject(['four', 5]) - }) test('createNode(value)', () => { const s = doc.createNode(array) expect(s).toBeInstanceOf(YAMLSeq) @@ -119,8 +84,6 @@ describe('arrays', () => { expect(String(doc)).toBe(res) doc.contents = array expect(String(doc)).toBe(res) - doc.contents = doc.createNode(array, { wrapScalars: false }) - expect(String(doc)).toBe(res) doc.contents = doc.createNode(array) expect(String(doc)).toBe(res) }) @@ -133,12 +96,12 @@ describe('objects', () => { expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toHaveLength(0) }) - test('createNode({ x: true }, { wrapScalars: false })', () => { - const s = doc.createNode({ x: true }, { wrapScalars: false }) + test('createNode({ x: true })', () => { + const s = doc.createNode({ x: true }) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(1) - expect(s.items[0]).toBeInstanceOf(Pair) - expect(s.items[0]).toMatchObject({ key: 'x', value: true }) + expect(s.items).toMatchObject([ + { key: { value: 'x' }, value: { value: true } } + ]) }) test('createNode({ x: true, y: undefined })', () => { const s = doc.createNode({ x: true, y: undefined }) @@ -157,24 +120,6 @@ describe('objects', () => { }) describe('{ x: 3, y: [4], z: { w: "five", v: 6 } }', () => { const object = { x: 3, y: [4], z: { w: 'five', v: 6 } } - test('createNode(value, { wrapScalars: false })', () => { - const s = doc.createNode(object, { wrapScalars: false }) - expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(3) - expect(s.items).toMatchObject([ - { key: 'x', value: 3 }, - { key: 'y', value: { items: [4] } }, - { - key: 'z', - value: { - items: [ - { key: 'w', value: 'five' }, - { key: 'v', value: 6 } - ] - } - } - ]) - }) test('createNode(value)', () => { const s = doc.createNode(object) expect(s).toBeInstanceOf(YAMLMap) @@ -204,8 +149,6 @@ z: expect(String(doc)).toBe(res) doc.contents = object expect(String(doc)).toBe(res) - doc.contents = doc.createNode(object, { wrapScalars: false }) - expect(String(doc)).toBe(res) doc.contents = doc.createNode(object) expect(String(doc)).toBe(res) }) @@ -218,21 +161,13 @@ describe('Set', () => { expect(s).toBeInstanceOf(YAMLSeq) expect(s.items).toHaveLength(0) }) - test('createNode(new Set([true]), { wrapScalars: false })', () => { - const s = doc.createNode(new Set([true]), { wrapScalars: false }) + test('createNode(new Set([true]))', () => { + const s = doc.createNode(new Set([true])) expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toMatchObject([true]) + expect(s.items).toMatchObject([{ value: true }]) }) describe("Set { 3, Set { 'four', 5 } }", () => { const set = new Set([3, new Set(['four', 5])]) - test('createNode(set, { wrapScalars: false })', () => { - const s = doc.createNode(set, { wrapScalars: false }) - expect(s).toBeInstanceOf(YAMLSeq) - expect(s.items).toHaveLength(2) - expect(s.items[0]).toBe(3) - expect(s.items[1]).toBeInstanceOf(YAMLSeq) - expect(s.items[1].items).toMatchObject(['four', 5]) - }) test('createNode(set)', () => { const s = doc.createNode(set) expect(s).toBeInstanceOf(YAMLSeq) @@ -249,8 +184,6 @@ describe('Set', () => { expect(String(doc)).toBe(res) doc.contents = set expect(String(doc)).toBe(res) - doc.contents = doc.createNode(set, { wrapScalars: false }) - expect(String(doc)).toBe(res) doc.contents = doc.createNode(set) expect(String(doc)).toBe(res) }) @@ -281,12 +214,12 @@ describe('Map', () => { expect(s).toBeInstanceOf(YAMLMap) expect(s.items).toHaveLength(0) }) - test('createNode(new Map([["x", true]]), { wrapScalars: false })', () => { - const s = doc.createNode(new Map([['x', true]]), { wrapScalars: false }) + test('createNode(new Map([["x", true]]))', () => { + const s = doc.createNode(new Map([['x', true]])) expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(1) - expect(s.items[0]).toBeInstanceOf(Pair) - expect(s.items[0]).toMatchObject({ key: 'x', value: true }) + expect(s.items).toMatchObject([ + { key: { value: 'x' }, value: { value: true } } + ]) }) describe("Map { 'x' => 3, 'y' => Set { 4 }, Map { 'w' => 'five', 'v' => 6 } => 'z' }", () => { const map = new Map([ @@ -300,24 +233,6 @@ describe('Map', () => { 'z' ] ]) - test('createNode(map, { wrapScalars: false })', () => { - const s = doc.createNode(map, { wrapScalars: false }) - expect(s).toBeInstanceOf(YAMLMap) - expect(s.items).toHaveLength(3) - expect(s.items).toMatchObject([ - { key: 'x', value: 3 }, - { key: 'y', value: { items: [4] } }, - { - key: { - items: [ - { key: 'w', value: 'five' }, - { key: 'v', value: 6 } - ] - }, - value: 'z' - } - ]) - }) test('createNode(map)', () => { const s = doc.createNode(map) expect(s).toBeInstanceOf(YAMLMap) @@ -347,8 +262,6 @@ y: expect(String(doc)).toBe(res) doc.contents = map expect(String(doc)).toBe(res) - doc.contents = doc.createNode(map, { wrapScalars: false }) - expect(String(doc)).toBe(res) doc.contents = doc.createNode(map) expect(String(doc)).toBe(res) }) @@ -368,20 +281,22 @@ describe('circular references', () => { const map = { foo: 'bar' } map.map = map const doc = new YAML.Document(null) - expect(doc.createNode(map, { wrapScalars: false })).toMatchObject({ + expect(doc.createNode(map)).toMatchObject({ items: [ - { key: 'foo', value: 'bar' }, + { key: { value: 'foo' }, value: { value: 'bar' } }, { - key: 'map', + key: { value: 'map' }, value: { type: 'ALIAS', - source: { items: [{ key: 'foo' }, { key: 'map' }] } + source: { + items: [{ key: { value: 'foo' } }, { key: { value: 'map' } }] + } } } ] }) expect(doc.anchors.map).toMatchObject({ - a1: { items: [{ key: 'foo' }, { key: 'map' }] } + a1: { items: [{ key: { value: 'foo' } }, { key: { value: 'map' } }] } }) }) @@ -390,13 +305,13 @@ describe('circular references', () => { const map = { foo: { bar: { baz } } } baz.map = map const doc = new YAML.Document(null) - const node = doc.createNode(map, { wrapScalars: false }) + const node = doc.createNode(map) expect(node.getIn(['foo', 'bar', 'baz', 'map'])).toMatchObject({ type: 'ALIAS', - source: { items: [{ key: 'foo' }] } + source: { items: [{ key: { value: 'foo' } }] } }) expect(doc.anchors.map).toMatchObject({ - a1: { items: [{ key: 'foo' }] } + a1: { items: [{ key: { value: 'foo' } }] } }) }) @@ -405,18 +320,18 @@ describe('circular references', () => { const two = ['two'] const seq = [one, two, one, one, two] const doc = new YAML.Document(null) - expect(doc.createNode(seq, { wrapScalars: false })).toMatchObject({ + expect(doc.createNode(seq)).toMatchObject({ items: [ - { items: ['one'] }, - { items: ['two'] }, - { type: 'ALIAS', source: { items: ['one'] } }, - { type: 'ALIAS', source: { items: ['one'] } }, - { type: 'ALIAS', source: { items: ['two'] } } + { items: [{ value: 'one' }] }, + { items: [{ value: 'two' }] }, + { type: 'ALIAS', source: { items: [{ value: 'one' }] } }, + { type: 'ALIAS', source: { items: [{ value: 'one' }] } }, + { type: 'ALIAS', source: { items: [{ value: 'two' }] } } ] }) expect(doc.anchors.map).toMatchObject({ - a1: { items: ['one'] }, - a2: { items: ['two'] } + a1: { items: [{ value: 'one' }] }, + a2: { items: [{ value: 'two' }] } }) }) @@ -424,10 +339,12 @@ describe('circular references', () => { const baz = { a: 1 } const seq = [{ foo: { bar: { baz } } }, { fe: { fi: { fo: { baz } } } }] const doc = new YAML.Document(null) - const node = doc.createNode(seq, { wrapScalars: false }) + const node = doc.createNode(seq) const source = node.getIn([0, 'foo', 'bar', 'baz']) const alias = node.getIn([1, 'fe', 'fi', 'fo', 'baz']) - expect(source).toMatchObject({ items: [{ key: 'a', value: 1 }] }) + expect(source).toMatchObject({ + items: [{ key: { value: 'a' }, value: { value: 1 } }] + }) expect(alias).toMatchObject({ type: 'ALIAS' }) expect(alias.source).toBe(source) }) diff --git a/tests/doc/types.js b/tests/doc/types.js index e744fa2d..b149033a 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -475,7 +475,7 @@ date (00:00:00Z): 2002-12-14\n`) ['b', 2], ['a', 3] ], - { tag: '!!pairs', wrapScalars: false } + { tag: '!!pairs' } ) expect(doc.contents.tag).toBe('tag:yaml.org,2002:pairs') expect(String(doc)).toBe(`!!pairs\n- a: 1\n- b: 2\n- a: 3\n`) @@ -532,7 +532,7 @@ date (00:00:00Z): 2002-12-14\n`) ['b', 2], ['a', 3] ], - { tag: '!!omap', wrapScalars: false } + { tag: '!!omap' } ) expect(doc.contents).toBeInstanceOf(YAMLOMap) expect(String(doc)).toBe(`!!omap\n- a: 1\n- b: 2\n- a: 3\n`) From 1e8e448563d5b492e03b8722312a634671013b20 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 22 Feb 2021 02:19:41 +0200 Subject: [PATCH 13/24] Always set doc.schema in constructor, based on directives & version --- docs/04_documents.md | 6 +++++- src/compose/compose-doc.ts | 7 ++----- src/compose/composer.ts | 11 +++++------ src/doc/Document.ts | 35 +++++++++++++++-------------------- src/doc/Schema.ts | 3 +++ tests/doc/errors.js | 10 ++++++---- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/04_documents.md b/docs/04_documents.md index 82265df5..2ffc926f 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -63,7 +63,11 @@ The `contents` of a parsed document will always consist of `Scalar`, `Map`, `Seq #### `new YAML.Document(value, replacer?, options = {})` -Creates a new document. If `value` is defined, the document `contents` are initialised with that value, wrapped recursively in appropriate [content nodes](#content-nodes). If `value` is `undefined`, the document's `contents` and `schema` are initialised as `null`. If defined, a `replacer` may filter or modify the initial document contents, following the same algorithm as the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). See [Options](#options) for more information on the last argument. +Creates a new document. +If `value` is defined, the document `contents` are initialised with that value, wrapped recursively in appropriate [content nodes](#content-nodes). +If `value` is `undefined`, the document's `contents` is initialised as `null`. +If defined, a `replacer` may filter or modify the initial document contents, following the same algorithm as the [JSON implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). +See [Options](#options) for more information on the last argument. | Member | Type | Description | | ------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index 627fe5d6..ace3daf0 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -1,4 +1,3 @@ -import { Type } from '../constants.js' import { Directives } from '../doc/directives.js' import { Document } from '../doc/Document.js' import type { Options } from '../options.js' @@ -13,10 +12,8 @@ export function composeDoc( { offset, start, value, end }: Tokens.Document, onError: (offset: number, message: string, warning?: boolean) => void ) { - const doc = new Document(undefined, options) as Document.Parsed - doc.type = Type.DOCUMENT - doc.directives = directives.atDocument() - doc.setSchema() // FIXME: always do this in the constructor + const opts = Object.assign({ directives }, options) + const doc = new Document(undefined, opts) as Document.Parsed const props = resolveProps(doc, start, true, 'doc-start', offset, onError) if (props.found) doc.directivesEndMarker = true diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 691c510d..68e1bdc1 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -50,7 +50,7 @@ export class Composer { private directives: Directives private doc: Document.Parsed | null = null private onDocument: (doc: Document.Parsed) => void - private options: Options + private options: Options | undefined private atDirectives = false private prelude: string[] = [] private errors: YAMLParseError[] = [] @@ -58,10 +58,10 @@ export class Composer { constructor(onDocument: Composer['onDocument'], options?: Options) { this.directives = new Directives({ - version: options?.version || defaultOptions.version || '1.2' + version: options?.version || defaultOptions.version }) this.onDocument = onDocument - this.options = options || defaultOptions + this.options = options } private onError = (offset: number, message: string, warning?: boolean) => { @@ -207,11 +207,10 @@ export class Composer { this.onDocument(this.doc) this.doc = null } else if (forceDoc) { - const doc = new Document(undefined, this.options) as Document.Parsed - doc.directives = this.directives.atDocument() + const opts = Object.assign({ directives: this.directives }, this.options) + const doc = new Document(undefined, opts) as Document.Parsed if (this.atDirectives) this.onError(offset, 'Missing directives-end indicator line') - doc.setSchema() // FIXME: always do this in the constructor doc.range = [0, offset] this.decorate(doc, false) this.onDocument(doc) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index df7acc75..72c1ea93 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -101,12 +101,8 @@ export class Document { options: Required & SchemaOptions - // FIXME required by Collection, currently optional in Document - /** - * The schema used with the document. Use `setSchema()` to change or - * initialise. - */ - declare schema: Schema + /** The schema used with the document. Use `setSchema()` to change. */ + schema: Schema /** * Array of prefixes; each will have a string `handle` that @@ -146,12 +142,15 @@ export class Document { this.options = Object.assign({}, defaultOptions, options) this.anchors = new Anchors(this.options.anchorPrefix) - this.directives = new Directives({ version: this.options.version }) + if (options?.directives) { + this.directives = options.directives.atDocument() + if (options.version && !this.directives.yaml.explicit) + this.directives.yaml.version = options.version + } else this.directives = new Directives({ version: this.options.version }) - // @ts-ignore FIXME - this.schema = null + const schemaOpts = Object.assign({}, this.getDefaults(), this.options) + this.schema = new Schema(schemaOpts) - // note that this.schema is left as null here this.contents = value === undefined ? null @@ -176,7 +175,6 @@ export class Document { value: unknown, { keepUndefined, onTagObj, replacer, tag }: CreateNodeOptions = {} ): Node { - this.setSchema() if (typeof replacer === 'function') value = replacer.call({ '': value }, '', value) else if (Array.isArray(replacer)) { @@ -231,7 +229,7 @@ export class Document { } /** - * Removes a value from the collection. + * Removes a value from the document. * @returns `true` if the item was found and removed. */ delete(key: any) { @@ -239,7 +237,7 @@ export class Document { } /** - * Removes a value from the collection. + * Removes a value from the document. * @returns `true` if the item was found and removed. */ deleteIn(path: Iterable) { @@ -310,7 +308,6 @@ export class Document { */ set(key: any, value: unknown) { if (this.contents == null) { - this.setSchema() this.contents = collectionFromPath(this.schema, [key], value) } else if (assertCollection(this.contents)) { this.contents.set(key, value) @@ -324,7 +321,6 @@ export class Document { setIn(path: Iterable, value: unknown) { if (isEmptyPath(path)) this.contents = value else if (this.contents == null) { - this.setSchema() this.contents = collectionFromPath(this.schema, Array.from(path), value) } else if (assertCollection(this.contents)) { this.contents.setIn(path, value) @@ -338,10 +334,10 @@ export class Document { * by the document. */ setSchema( - id?: Options['version'] | SchemaName, + id: Options['version'] | SchemaName | null, customTags?: (TagId | Tag)[] ) { - if (!id && !customTags && this.schema) return + if (!id && !customTags) return // @ts-ignore Never happens in TypeScript if (typeof id === 'number') id = id.toFixed(1) @@ -353,8 +349,8 @@ export class Document { this.options.schema = id } if (Array.isArray(customTags)) this.options.customTags = customTags - const opt = Object.assign({}, this.getDefaults(), this.options) - this.schema = new Schema(opt) + const schemaOpts = Object.assign({}, this.getDefaults(), this.options) + this.schema = new Schema(schemaOpts) } /** Set `handle` as a shorthand string for the `prefix` tag namespace. */ @@ -426,7 +422,6 @@ export class Document { const s = JSON.stringify(indentSize) throw new Error(`"indent" option must be a positive integer, not ${s}`) } - this.setSchema() const lines = [] let hasDirectives = false const dir = this.directives.toString(this) diff --git a/src/doc/Schema.ts b/src/doc/Schema.ts index 6a8f0200..8c9a6b83 100644 --- a/src/doc/Schema.ts +++ b/src/doc/Schema.ts @@ -1,6 +1,7 @@ import type { Pair } from '../ast/Pair.js' import { schemas, tags } from '../tags/index.js' import type { Tag, TagId } from '../tags/types.js' +import { Directives } from './directives.js' import { getSchemaTags } from './getSchemaTags.js' export type SchemaName = 'core' | 'failsafe' | 'json' | 'yaml-1.1' @@ -15,6 +16,8 @@ export interface SchemaOptions { | ((tags: Array) => Array) | null + directives?: Directives + /** * Enable support for `<<` merge keys. * diff --git a/tests/doc/errors.js b/tests/doc/errors.js index ab770972..289800be 100644 --- a/tests/doc/errors.js +++ b/tests/doc/errors.js @@ -282,13 +282,15 @@ describe.skip('pretty errors', () => { describe('invalid options', () => { test('unknown schema', () => { - const doc = new YAML.Document(undefined, { schema: 'foo' }) - expect(() => doc.setSchema()).toThrow(/Unknown schema/) + expect(() => new YAML.Document(undefined, { schema: 'foo' })).toThrow( + /Unknown schema/ + ) }) test('unknown custom tag', () => { - const doc = new YAML.Document(undefined, { customTags: ['foo'] }) - expect(() => doc.setSchema()).toThrow(/Unknown custom tag/) + expect(() => new YAML.Document(undefined, { customTags: ['foo'] })).toThrow( + /Unknown custom tag/ + ) }) }) From 7f491bfcb49c5ec5a880d0fbd8c0cbb8dbb82b11 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 22 Feb 2021 03:18:36 +0200 Subject: [PATCH 14/24] Fix imports, adding .js extensions & resolving most circular dependencies --- src/ast/Node.ts | 6 +++--- src/compose/compose-collection.ts | 13 +++++++++---- src/compose/compose-doc.ts | 2 +- src/compose/compose-node.ts | 8 ++++++-- src/compose/compose-scalar.ts | 4 ++-- src/compose/composer.ts | 3 ++- src/compose/resolve-block-map.ts | 6 ++++-- src/compose/resolve-block-seq.ts | 5 +++-- src/compose/resolve-flow-collection.ts | 8 ++++++-- src/compose/resolve-merge-pair.ts | 7 ++++++- src/doc/Document.ts | 6 +++--- src/errors.ts | 2 +- src/stringify/stringify.ts | 2 +- src/stringify/stringifyNumber.ts | 2 +- src/tags/index.d.ts | 2 +- 15 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/ast/Node.ts b/src/ast/Node.ts index 4b8bc5bb..b2623061 100644 --- a/src/ast/Node.ts +++ b/src/ast/Node.ts @@ -1,6 +1,6 @@ -import type { Type } from '../constants' -import { StringifyContext } from '../stringify/stringify' -import type { PairType } from './Pair' +import type { Type } from '../constants.js' +import { StringifyContext } from '../stringify/stringify.js' +import type { PairType } from './Pair.js' export declare namespace Node { interface Parsed extends Node { diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index 89677b11..45b4661e 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,15 +1,20 @@ -import { Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' +import { Node } from '../ast/Node.js' +import { Scalar } from '../ast/Scalar.js' +import type { YAMLMap } from '../ast/YAMLMap.js' +import type { YAMLSeq } from '../ast/YAMLSeq.js' import type { Document } from '../doc/Document.js' import type { BlockMap, BlockSequence, FlowCollection } from '../parse/tokens.js' +import type { ComposeNode } from './compose-node.js' import { resolveBlockMap } from './resolve-block-map.js' import { resolveBlockSeq } from './resolve-block-seq.js' import { resolveFlowCollection } from './resolve-flow-collection.js' export function composeCollection( + CN: ComposeNode, doc: Document.Parsed, token: BlockMap | BlockSequence | FlowCollection, anchor: string | null, @@ -19,15 +24,15 @@ export function composeCollection( let coll: YAMLMap.Parsed | YAMLSeq.Parsed switch (token.type) { case 'block-map': { - coll = resolveBlockMap(doc, token, anchor, onError) + coll = resolveBlockMap(CN, doc, token, anchor, onError) break } case 'block-seq': { - coll = resolveBlockSeq(doc, token, anchor, onError) + coll = resolveBlockSeq(CN, doc, token, anchor, onError) break } case 'flow-collection': { - coll = resolveFlowCollection(doc, token, anchor, onError) + coll = resolveFlowCollection(CN, doc, token, anchor, onError) break } } diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index ace3daf0..63e8b090 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -1,4 +1,4 @@ -import { Directives } from '../doc/directives.js' +import type { Directives } from '../doc/directives.js' import { Document } from '../doc/Document.js' import type { Options } from '../options.js' import type * as Tokens from '../parse/tokens.js' diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 800eff8a..2c45f348 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,4 +1,5 @@ -import { Alias, Node } from '../ast/index.js' +import { Alias } from '../ast/Alias.js' +import type { Node } from '../ast/Node.js' import type { Document } from '../doc/Document.js' import type { FlowScalar, Token } from '../parse/tokens.js' import { composeCollection } from './compose-collection.js' @@ -13,6 +14,9 @@ export interface Props { tagName: string } +const CN = { composeNode, composeEmptyNode } +export type ComposeNode = typeof CN + export function composeNode( doc: Document.Parsed, token: Token, @@ -36,7 +40,7 @@ export function composeNode( case 'block-map': case 'block-seq': case 'flow-collection': - node = composeCollection(doc, token, anchor, tagName, onError) + node = composeCollection(CN, doc, token, anchor, tagName, onError) break default: console.log(token) diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index 75ba3bae..9335a016 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -1,5 +1,5 @@ -import { Scalar } from '../ast/index.js' -import { Document } from '../doc/Document.js' +import { Scalar } from '../ast/Scalar.js' +import type { Document } from '../doc/Document.js' import type { Schema } from '../doc/Schema.js' import type { BlockScalar, FlowScalar } from '../parse/tokens.js' import type { Tag } from '../tags/types.js' diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 68e1bdc1..d5c2cd08 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -1,4 +1,5 @@ -import { Collection, Node } from '../ast/index.js' +import { Collection } from '../ast/Collection.js' +import type { Node } from '../ast/Node.js' import { Directives } from '../doc/directives.js' import { Document } from '../doc/Document.js' import { YAMLParseError, YAMLWarning } from '../errors.js' diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 7860fdc0..02bc75ad 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -1,8 +1,9 @@ -import { Pair, YAMLMap } from '../ast/index.js' +import { Pair } from '../ast/Pair.js' +import { YAMLMap } from '../ast/YAMLMap.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' import type { BlockMap } from '../parse/tokens.js' -import { composeEmptyNode, composeNode } from './compose-node.js' +import type { ComposeNode } from './compose-node.js' import { resolveMergePair } from './resolve-merge-pair.js' import { resolveProps } from './resolve-props.js' import { containsNewline } from './util-contains-newline.js' @@ -10,6 +11,7 @@ import { containsNewline } from './util-contains-newline.js' const startColMsg = 'All mapping items must start at the same column' export function resolveBlockMap( + { composeNode, composeEmptyNode }: ComposeNode, doc: Document.Parsed, { indent, items, offset }: BlockMap, anchor: string | null, diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index 33dcc076..75f98465 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -1,11 +1,12 @@ -import { YAMLSeq } from '../ast/index.js' +import { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' import type { BlockSequence } from '../parse/tokens.js' -import { composeEmptyNode, composeNode } from './compose-node.js' +import type { ComposeNode } from './compose-node.js' import { resolveProps } from './resolve-props.js' export function resolveBlockSeq( + { composeNode, composeEmptyNode }: ComposeNode, doc: Document.Parsed, { items, offset }: BlockSequence, anchor: string | null, diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index ec5b9646..e70b8701 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -1,13 +1,17 @@ -import { Node, Pair, YAMLMap, YAMLSeq } from '../ast/index.js' +import { Node } from '../ast/Node.js' +import { Pair } from '../ast/Pair.js' +import { YAMLMap } from '../ast/YAMLMap.js' +import { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' import type { FlowCollection, SourceToken, Token } from '../parse/tokens.js' -import { composeEmptyNode, composeNode } from './compose-node.js' +import type { ComposeNode } from './compose-node.js' import { resolveEnd } from './resolve-end.js' import { resolveMergePair } from './resolve-merge-pair.js' import { containsNewline } from './util-contains-newline.js' export function resolveFlowCollection( + { composeNode, composeEmptyNode }: ComposeNode, doc: Document.Parsed, fc: FlowCollection, _anchor: string | null, diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index 4b01b872..e0c9e672 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -1,4 +1,9 @@ -import { Alias, Merge, Node, Pair, Scalar, YAMLMap } from '../ast/index.js' +import { Alias } from '../ast/Alias.js' +import { Merge } from '../ast/Merge.js' +import type { Node } from '../ast/Node.js' +import type { Pair } from '../ast/Pair.js' +import { Scalar } from '../ast/Scalar.js' +import { YAMLMap } from '../ast/YAMLMap.js' export function resolveMergePair( pair: Pair, diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 72c1ea93..cdd70721 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -11,8 +11,8 @@ import { toJS, ToJSContext } from '../ast/index.js' -import { Type } from '../constants' -import type { YAMLError, YAMLWarning } from '../errors' +import { Type } from '../constants.js' +import type { YAMLError, YAMLWarning } from '../errors.js' import { DocumentOptions, Options, @@ -21,7 +21,7 @@ import { } from '../options.js' import { addComment } from '../stringify/addComment.js' import { stringify, StringifyContext } from '../stringify/stringify.js' -import type { Tag, TagId } from '../tags/types' +import type { Tag, TagId } from '../tags/types.js' import { Anchors } from './Anchors.js' import { Schema, SchemaName, SchemaOptions } from './Schema.js' diff --git a/src/errors.ts b/src/errors.ts index 1bf87d45..d9f4d036 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,4 @@ -// import { Type } from './constants' +// import { Type } from './constants.js' // interface LinePos { line: number; col: number } export class YAMLError extends Error { diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index dcf74057..c4362385 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -2,7 +2,7 @@ import { Alias } from '../ast/Alias.js' import { Node } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' import { Scalar } from '../ast/Scalar.js' -import type { Document } from '../doc/Document' +import type { Document } from '../doc/Document.js' import type { Tag } from '../tags/types.js' import { stringifyString } from './stringifyString.js' diff --git a/src/stringify/stringifyNumber.ts b/src/stringify/stringifyNumber.ts index 4cc9fb13..15de27a4 100644 --- a/src/stringify/stringifyNumber.ts +++ b/src/stringify/stringifyNumber.ts @@ -1,4 +1,4 @@ -import { Scalar } from '../ast' +import type { Scalar } from '../ast/Scalar.js' export function stringifyNumber({ format, diff --git a/src/tags/index.d.ts b/src/tags/index.d.ts index d2f476f0..14e5e4aa 100644 --- a/src/tags/index.d.ts +++ b/src/tags/index.d.ts @@ -1,4 +1,4 @@ -import { SchemaId, Tag, TagId } from './types' +import { SchemaId, Tag, TagId } from './types.js' export const schemas: Record export const tags: Record From e42211b150e498dbf1aee5d027faf1e892b63404 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Fri, 26 Feb 2021 19:52:02 +0200 Subject: [PATCH 15/24] Refactor failsafe & JSON tags as TypeScript, fixing tag object types --- src/ast/Pair.ts | 8 --- src/ast/YAMLMap.ts | 6 +- src/ast/YAMLSeq.ts | 5 +- src/compose/compose-collection.ts | 24 +++++++- src/compose/compose-scalar.ts | 12 ++-- src/doc/Document.ts | 6 +- src/doc/Schema.ts | 13 ++-- src/doc/createNode.ts | 14 ++--- src/doc/getSchemaTags.ts | 12 ++-- src/stringify/stringify.ts | 12 ++-- src/tags/failsafe/{index.js => index.ts} | 0 src/tags/failsafe/{map.js => map.ts} | 12 ++-- src/tags/failsafe/{seq.js => seq.ts} | 15 +++-- src/tags/failsafe/{string.js => string.ts} | 3 +- src/tags/index.d.ts | 16 ++++- src/tags/index.js | 1 + src/tags/{json.js => json.ts} | 35 ++++++----- src/tags/types.ts | 70 +++++++++++++--------- src/tags/yaml-1.1/omap.js | 1 + src/tags/yaml-1.1/pairs.js | 1 + src/tags/yaml-1.1/set.js | 1 + tests/doc/errors.js | 18 ++++++ 22 files changed, 181 insertions(+), 104 deletions(-) rename src/tags/failsafe/{index.js => index.ts} (100%) rename src/tags/failsafe/{map.js => map.ts} (68%) rename src/tags/failsafe/{seq.js => seq.ts} (53%) rename src/tags/failsafe/{string.js => string.ts} (84%) rename src/tags/{json.js => json.ts} (69%) diff --git a/src/ast/Pair.ts b/src/ast/Pair.ts index fb5f70ff..7565bac0 100644 --- a/src/ast/Pair.ts +++ b/src/ast/Pair.ts @@ -9,7 +9,6 @@ import { Node } from './Node.js' import { Scalar } from './Scalar.js' import { YAMLSeq } from './YAMLSeq.js' import { toJS, ToJSContext } from './toJS.js' -import type { YAMLMap } from './index.js' export function createPair( key: unknown, @@ -26,13 +25,6 @@ export enum PairType { MERGE_PAIR = 'MERGE_PAIR' } -export declare namespace Pair { - interface Parsed extends Pair { - key: Scalar | YAMLMap | YAMLSeq - value: Scalar | YAMLMap | YAMLSeq | null - } -} - export class Pair extends Node { /** Always Node or null when parsed, but can be set to anything. */ key: K diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index d48a1edc..2c3f5219 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -1,6 +1,7 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' +import { Node } from './Node.js' import { Pair } from './Pair.js' import { Scalar, isScalarValue } from './Scalar.js' import { ToJSContext } from './toJS.js' @@ -17,8 +18,9 @@ export function findPair(items: Iterable, key: unknown) { } export declare namespace YAMLMap { - interface Parsed extends YAMLMap { - items: Pair.Parsed[] + interface Parsed + extends YAMLMap { + items: Pair[] range: [number, number] } } diff --git a/src/ast/YAMLSeq.ts b/src/ast/YAMLSeq.ts index cd7a8064..785c5450 100644 --- a/src/ast/YAMLSeq.ts +++ b/src/ast/YAMLSeq.ts @@ -1,12 +1,13 @@ import { Type } from '../constants.js' import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' +import { Node } from './Node.js' import { Scalar, isScalarValue } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' export declare namespace YAMLSeq { - interface Parsed extends YAMLSeq { - items: Node[] + interface Parsed extends YAMLSeq { + items: T[] range: [number, number] } } diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index 45b4661e..6d7a8169 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -2,12 +2,14 @@ import { Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' import type { YAMLMap } from '../ast/YAMLMap.js' import type { YAMLSeq } from '../ast/YAMLSeq.js' +import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' import type { BlockMap, BlockSequence, FlowCollection } from '../parse/tokens.js' +import { CollectionTag } from '../tags/types.js' import type { ComposeNode } from './compose-node.js' import { resolveBlockMap } from './resolve-block-map.js' import { resolveBlockSeq } from './resolve-block-seq.js' @@ -46,10 +48,28 @@ export function composeCollection( return coll } - let tag = doc.schema.tags.find(t => t.tag === tagName) + let expType: 'map' | 'seq' // | null = null + switch (coll.type) { + case Type.FLOW_MAP: + case Type.MAP: + expType = 'map' + break + case Type.FLOW_SEQ: + case Type.SEQ: + expType = 'seq' + break + default: + onError(coll.range[0], `Unexpected collection type: ${coll.type}`) + coll.tag = tagName + return coll + } + + let tag = doc.schema.tags.find( + t => t.collection === expType && t.tag === tagName + ) as CollectionTag | undefined if (!tag) { const kt = doc.schema.knownTags[tagName] - if (kt) { + if (kt && kt.collection === expType) { doc.schema.tags.push(Object.assign({}, kt, { default: false })) tag = kt } else { diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index 9335a016..d6ae9e1d 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -2,7 +2,7 @@ import { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' import type { Schema } from '../doc/Schema.js' import type { BlockScalar, FlowScalar } from '../parse/tokens.js' -import type { Tag } from '../tags/types.js' +import type { ScalarTag } from '../tags/types.js' import { resolveBlockScalar } from './resolve-block-scalar.js' import { resolveFlowScalar } from './resolve-flow-scalar.js' @@ -43,7 +43,9 @@ export function composeScalar( } const defaultScalarTag = (schema: Schema) => - schema.tags.find(tag => tag.tag === 'tag:yaml.org,2002:str') + schema.tags.find( + tag => !tag.collection && tag.tag === 'tag:yaml.org,2002:str' + ) as ScalarTag | undefined function findScalarTagByName( schema: Schema, @@ -52,16 +54,16 @@ function findScalarTagByName( onError: (offset: number, message: string, warning?: boolean) => void ) { if (tagName === '!') return defaultScalarTag(schema) // non-specific tag - const matchWithTest: Tag[] = [] + const matchWithTest: ScalarTag[] = [] for (const tag of schema.tags) { - if (tag.tag === tagName) { + if (!tag.collection && tag.tag === tagName) { if (tag.default && tag.test) matchWithTest.push(tag) else return tag } } for (const tag of matchWithTest) if (tag.test?.test(value)) return tag const kt = schema.knownTags[tagName] - if (kt) { + if (kt && !kt.collection) { // Ensure that the known tag is available for stringifying, // but does not get used by default. schema.tags.push(Object.assign({}, kt, { default: false, test: undefined })) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index cdd70721..86ef0ac6 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -21,7 +21,7 @@ import { } from '../options.js' import { addComment } from '../stringify/addComment.js' import { stringify, StringifyContext } from '../stringify/stringify.js' -import type { Tag, TagId } from '../tags/types.js' +import type { TagId, TagObj } from '../tags/types.js' import { Anchors } from './Anchors.js' import { Schema, SchemaName, SchemaOptions } from './Schema.js' @@ -36,7 +36,7 @@ export type { Anchors, Reviver } export interface CreateNodeOptions { keepUndefined?: boolean | null - onTagObj?: (tagObj: Tag) => void + onTagObj?: (tagObj: TagObj) => void /** * Filter or modify values while creating a node. @@ -335,7 +335,7 @@ export class Document { */ setSchema( id: Options['version'] | SchemaName | null, - customTags?: (TagId | Tag)[] + customTags?: (TagId | TagObj)[] ) { if (!id && !customTags) return diff --git a/src/doc/Schema.ts b/src/doc/Schema.ts index 8c9a6b83..9ed62134 100644 --- a/src/doc/Schema.ts +++ b/src/doc/Schema.ts @@ -1,20 +1,19 @@ import type { Pair } from '../ast/Pair.js' import { schemas, tags } from '../tags/index.js' -import type { Tag, TagId } from '../tags/types.js' +import type { CollectionTag, ScalarTag, TagId, TagObj } from '../tags/types.js' import { Directives } from './directives.js' import { getSchemaTags } from './getSchemaTags.js' export type SchemaName = 'core' | 'failsafe' | 'json' | 'yaml-1.1' +type TagValue = TagId | ScalarTag | CollectionTag + export interface SchemaOptions { /** * Array of additional tags to include in the schema, or a function that may * modify the schema's base tag array. */ - customTags?: - | Array - | ((tags: Array) => Array) - | null + customTags?: TagValue[] | ((tags: TagValue[]) => TagValue[]) | null directives?: Directives @@ -62,11 +61,11 @@ const coreKnownTags = { } export class Schema { - knownTags: Record + knownTags: Record merge: boolean name: SchemaName sortMapEntries: ((a: Pair, b: Pair) => number) | null - tags: Tag[] + tags: Array // Used by createNode(), to avoid circular dependencies map = tags.map diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index 1d10aa51..2f5b3bea 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -1,22 +1,20 @@ import type { Alias } from '../ast/Alias.js' import { Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' -import type { YAMLMap } from '../ast/YAMLMap.js' -import type { YAMLSeq } from '../ast/YAMLSeq.js' import { defaultTagPrefix } from '../constants.js' -import type { Tag } from '../tags/types.js' +import type { TagObj } from '../tags/types.js' import type { Replacer } from './Document.js' import type { Schema } from './Schema.js' export interface CreateNodeAliasRef { - node: Scalar | YAMLMap | YAMLSeq | undefined + node: Node | undefined value: unknown } export interface CreateNodeContext { keepUndefined?: boolean onAlias(source: CreateNodeAliasRef): Alias - onTagObj?: (tagObj: Tag) => void + onTagObj?: (tagObj: TagObj) => void prevObjects: Map replacer?: Replacer schema: Schema @@ -25,7 +23,7 @@ export interface CreateNodeContext { function findTagObject( value: unknown, tagName: string | undefined, - tags: Tag[] + tags: TagObj[] ) { if (tagName) { const match = tags.filter(t => t.tag === tagName) @@ -56,7 +54,7 @@ export function createNode( value instanceof Map ? map : Symbol.iterator in Object(value) ? seq : map } if (onTagObj) { - onTagObj(tagObj) + onTagObj(tagObj as TagObj) delete ctx.onTagObj } @@ -70,7 +68,7 @@ export function createNode( prevObjects.set(value, ref) } - const node = tagObj.createNode + const node = tagObj?.createNode ? tagObj.createNode(ctx.schema, value, ctx) : new Scalar(value) if (tagName) node.tag = tagName diff --git a/src/doc/getSchemaTags.ts b/src/doc/getSchemaTags.ts index 4d1576af..93d65d06 100644 --- a/src/doc/getSchemaTags.ts +++ b/src/doc/getSchemaTags.ts @@ -1,18 +1,18 @@ -import type { SchemaId, Tag, TagId } from '../tags/types.js' +import type { SchemaId, TagId, TagObj } from '../tags/types.js' import type { SchemaName } from './Schema.js' export function getSchemaTags( - schemas: Record, - knownTags: Record, + schemas: Record, + knownTags: Record, customTags: - | Array - | ((tags: Array) => Array) + | Array + | ((tags: Array) => Array) | null | undefined, schemaName: SchemaName ) { const schemaId = schemaName.replace(/\W/g, '') as SchemaId // 'yaml-1.1' -> 'yaml11' - let tags: Array = schemas[schemaId] + let tags: Array = schemas[schemaId] if (!tags) { const keys = Object.keys(schemas) .map(key => JSON.stringify(key)) diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index c4362385..d7fa39f8 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -3,7 +3,7 @@ import { Node } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' import { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' -import type { Tag } from '../tags/types.js' +import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' export interface StringifyContext { @@ -19,14 +19,14 @@ export interface StringifyContext { [key: string]: unknown } -function getTagObject(tags: Tag[], item: Node) { +function getTagObject(tags: TagObj[], item: Node) { if (item.tag) { const match = tags.filter(t => t.tag === item.tag) if (match.length > 0) return match.find(t => t.format === (item as Scalar).format) || match[0] } - let tagObj: Tag | undefined = undefined + let tagObj: TagObj | undefined = undefined let obj: unknown if (item instanceof Scalar) { obj = item.value @@ -49,7 +49,7 @@ function getTagObject(tags: Tag[], item: Node) { // needs to be called before value stringifier to allow for circular anchor refs function stringifyProps( node: Node, - tagObj: Tag, + tagObj: TagObj, { anchors, doc }: StringifyContext ) { const props = [] @@ -75,7 +75,7 @@ export function stringify( if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) if (item instanceof Alias) return item.toString(ctx) - let tagObj: Tag | undefined = undefined + let tagObj: TagObj | undefined = undefined const node: Node = item instanceof Node ? item @@ -89,7 +89,7 @@ export function stringify( const str = typeof tagObj.stringify === 'function' - ? tagObj.stringify(node, ctx, onComment, onChompKeep) + ? tagObj.stringify(node as Scalar, ctx, onComment, onChompKeep) : node instanceof Scalar ? stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep) diff --git a/src/tags/failsafe/index.js b/src/tags/failsafe/index.ts similarity index 100% rename from src/tags/failsafe/index.js rename to src/tags/failsafe/index.ts diff --git a/src/tags/failsafe/map.js b/src/tags/failsafe/map.ts similarity index 68% rename from src/tags/failsafe/map.js rename to src/tags/failsafe/map.ts index 5a880a66..81d3e1e9 100644 --- a/src/tags/failsafe/map.js +++ b/src/tags/failsafe/map.ts @@ -1,10 +1,13 @@ import { createPair } from '../../ast/Pair.js' import { YAMLMap } from '../../ast/YAMLMap.js' +import type { CreateNodeContext } from '../../doc/createNode.js' +import type { Schema } from '../../doc/Schema.js' +import type { CollectionTag } from '../types.js' -function createMap(schema, obj, ctx) { +function createMap(schema: Schema, obj: unknown, ctx: CreateNodeContext) { const { keepUndefined, replacer } = ctx const map = new YAMLMap(schema) - const add = (key, value) => { + const add = (key: unknown, value: unknown) => { if (typeof replacer === 'function') value = replacer.call(obj, key, value) else if (Array.isArray(replacer) && !replacer.includes(key)) return if (value !== undefined || keepUndefined) @@ -13,7 +16,7 @@ function createMap(schema, obj, ctx) { if (obj instanceof Map) { for (const [key, value] of obj) add(key, value) } else if (obj && typeof obj === 'object') { - for (const key of Object.keys(obj)) add(key, obj[key]) + for (const key of Object.keys(obj)) add(key, (obj as any)[key]) } if (typeof schema.sortMapEntries === 'function') { map.items.sort(schema.sortMapEntries) @@ -21,7 +24,8 @@ function createMap(schema, obj, ctx) { return map } -export const map = { +export const map: CollectionTag = { + collection: 'map', createNode: createMap, default: true, nodeClass: YAMLMap, diff --git a/src/tags/failsafe/seq.js b/src/tags/failsafe/seq.ts similarity index 53% rename from src/tags/failsafe/seq.js rename to src/tags/failsafe/seq.ts index 8b79f153..15973616 100644 --- a/src/tags/failsafe/seq.js +++ b/src/tags/failsafe/seq.ts @@ -1,23 +1,26 @@ import { YAMLSeq } from '../../ast/YAMLSeq.js' -import { createNode } from '../../doc/createNode.js' +import { CreateNodeContext, createNode } from '../../doc/createNode.js' +import type { Schema } from '../../doc/Schema.js' +import type { CollectionTag } from '../types.js' -function createSeq(schema, obj, ctx) { +function createSeq(schema: Schema, obj: unknown, ctx: CreateNodeContext) { const { replacer } = ctx const seq = new YAMLSeq(schema) - if (obj && obj[Symbol.iterator]) { + if (obj && Symbol.iterator in Object(obj)) { let i = 0 - for (let it of obj) { + for (let it of obj as Iterable) { if (typeof replacer === 'function') { const key = obj instanceof Set ? it : String(i++) it = replacer.call(obj, key, it) } - seq.items.push(createNode(it, null, ctx)) + seq.items.push(createNode(it, undefined, ctx)) } } return seq } -export const seq = { +export const seq: CollectionTag = { + collection: 'seq', createNode: createSeq, default: true, nodeClass: YAMLSeq, diff --git a/src/tags/failsafe/string.js b/src/tags/failsafe/string.ts similarity index 84% rename from src/tags/failsafe/string.js rename to src/tags/failsafe/string.ts index f1cd90d9..4a7d70c0 100644 --- a/src/tags/failsafe/string.js +++ b/src/tags/failsafe/string.ts @@ -1,7 +1,8 @@ import { stringifyString } from '../../stringify/stringifyString.js' import { strOptions } from '../options.js' +import type { ScalarTag } from '../types.js' -export const string = { +export const string: ScalarTag = { identify: value => typeof value === 'string', default: true, tag: 'tag:yaml.org,2002:str', diff --git a/src/tags/index.d.ts b/src/tags/index.d.ts index 14e5e4aa..25faa3e6 100644 --- a/src/tags/index.d.ts +++ b/src/tags/index.d.ts @@ -1,4 +1,14 @@ -import { SchemaId, Tag, TagId } from './types.js' +import { TagId, TagObj } from './types.js' -export const schemas: Record -export const tags: Record +import { failsafe } from './failsafe/index.js' +import { json } from './json.js' + +export const schemas: { + core: TagObj[] + failsafe: typeof failsafe + json: typeof json + yaml11: TagObj[] +} + +//export const schemas: Record +export const tags: Record diff --git a/src/tags/index.js b/src/tags/index.js index 5b6c954e..b028ae34 100644 --- a/src/tags/index.js +++ b/src/tags/index.js @@ -15,6 +15,7 @@ import { yaml11 } from './yaml-1.1/index.js' import { map } from './failsafe/map.js' import { seq } from './failsafe/seq.js' + import { binary } from './yaml-1.1/binary.js' import { omap } from './yaml-1.1/omap.js' import { pairs } from './yaml-1.1/pairs.js' diff --git a/src/tags/json.js b/src/tags/json.ts similarity index 69% rename from src/tags/json.js rename to src/tags/json.ts index 36a769ba..067c1ae1 100644 --- a/src/tags/json.js +++ b/src/tags/json.ts @@ -4,15 +4,15 @@ import { Scalar } from '../ast/Scalar.js' import { map } from './failsafe/map.js' import { seq } from './failsafe/seq.js' import { intOptions } from './options.js' +import { CollectionTag, ScalarTag } from './types.js' -const intIdentify = value => - typeof value === 'bigint' || Number.isInteger(value) +function intIdentify(value: unknown): value is number | BigInt { + return typeof value === 'bigint' || Number.isInteger(value) +} -const stringifyJSON = ({ value }) => JSON.stringify(value) +const stringifyJSON = ({ value }: Scalar) => JSON.stringify(value) -export const json = [ - map, - seq, +const jsonScalars: ScalarTag[] = [ { identify: value => typeof value === 'string', default: true, @@ -53,13 +53,20 @@ export const json = [ test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/, resolve: str => parseFloat(str), stringify: stringifyJSON - }, - { - default: true, - test: /^/, - resolve(str, onError) { - onError(`Unresolved plain scalar ${JSON.stringify(str)}`) - return str - } } ] + +const jsonError: ScalarTag = { + default: true, + tag: '', + test: /^/, + resolve(str, onError) { + onError(`Unresolved plain scalar ${JSON.stringify(str)}`) + return str + } +} + +export const json = ([map, seq] as Array).concat( + jsonScalars, + jsonError +) diff --git a/src/tags/types.ts b/src/tags/types.ts index 098abea4..69fe6979 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -22,15 +22,11 @@ export type TagId = | 'set' | 'timestamp' -export interface Tag { +interface TagBase { /** * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes. */ - createNode?: ( - schema: Schema, - value: T, - ctx: CreateNodeContext - ) => YAMLMap | YAMLSeq | Scalar + createNode?: (schema: Schema, value: unknown, ctx: CreateNodeContext) => Node /** * If `true`, together with `test` allows for values to be stringified without @@ -40,7 +36,8 @@ export interface Tag { default: boolean /** - * If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them. + * If a tag has multiple forms that should be parsed and/or stringified + * differently, use `format` to identify them. */ format?: string @@ -48,31 +45,35 @@ export interface Tag { * Used by `YAML.createNode` to detect your data type, e.g. using `typeof` or * `instanceof`. */ - identify(value: any): boolean + identify?: (value: unknown) => boolean /** - * The `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations. + * Used by some tags to configure their stringification, where applicable. */ - nodeClass?: new () => any + options?: object /** - * Used by some tags to configure their stringification, where applicable. + * The identifier for your data type, with which its stringified form will be + * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified + * `tag:domain,date:foo`. */ - options?: object + tag: string +} + +export interface ScalarTag extends TagBase { + collection?: never + nodeClass?: never /** * Turns a value into an AST node. * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. */ - resolve( - value: string | YAMLMap | YAMLSeq, - onError: (message: string) => void - ): Node | any + resolve(value: string, onError: (message: string) => void): unknown /** - * Optional function stringifying the AST node in the current context. If your - * data includes a suitable `.toString()` method, you can probably leave this - * undefined and use the default stringifier. + * Optional function stringifying a Scalar node. If your data includes a + * suitable `.toString()` method, you can probably leave this undefined and + * use the default stringifier. * * @param item The node being stringified. * @param ctx Contains the stringifying context variables. @@ -82,19 +83,12 @@ export interface Tag { * type with the `+` chomping indicator. */ stringify?: ( - item: Node, + item: Scalar, ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ) => string - /** - * The identifier for your data type, with which its stringified form will be - * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified - * `tag:domain,date:foo`. - */ - tag: string - /** * Together with `default` allows for values to be stringified without an * explicit tag and detected using a regular expression. For most cases, it's @@ -103,3 +97,25 @@ export interface Tag { */ test?: RegExp } + +export interface CollectionTag extends TagBase { + stringify?: never + test?: never + + /** The source collection type supported by this tag. */ + collection: 'map' | 'seq' + + /** + * The `Node` child class that implements this tag. + * If set, used to select this tag when stringifying. + */ + nodeClass?: new () => Node + + /** + * Turns a value into an AST node. + * If returning a non-`Node` value, the output will be wrapped as a `Scalar`. + */ + resolve(value: YAMLMap | YAMLSeq, onError: (message: string) => void): unknown +} + +export type TagObj = ScalarTag | CollectionTag diff --git a/src/tags/yaml-1.1/omap.js b/src/tags/yaml-1.1/omap.js index cb390569..15c0c485 100644 --- a/src/tags/yaml-1.1/omap.js +++ b/src/tags/yaml-1.1/omap.js @@ -61,6 +61,7 @@ function createOMap(schema, iterable, ctx) { } export const omap = { + collection: 'seq', identify: value => value instanceof Map, nodeClass: YAMLOMap, default: false, diff --git a/src/tags/yaml-1.1/pairs.js b/src/tags/yaml-1.1/pairs.js index eb4dc132..4a945c55 100644 --- a/src/tags/yaml-1.1/pairs.js +++ b/src/tags/yaml-1.1/pairs.js @@ -56,6 +56,7 @@ export function createPairs(schema, iterable, ctx) { } export const pairs = { + collection: 'seq', default: false, tag: 'tag:yaml.org,2002:pairs', resolve: parsePairs, diff --git a/src/tags/yaml-1.1/set.js b/src/tags/yaml-1.1/set.js index f27a8297..8e7de370 100644 --- a/src/tags/yaml-1.1/set.js +++ b/src/tags/yaml-1.1/set.js @@ -74,6 +74,7 @@ function createSet(schema, iterable, ctx) { } export const set = { + collection: 'map', identify: value => value instanceof Set, nodeClass: YAMLSet, default: false, diff --git a/tests/doc/errors.js b/tests/doc/errors.js index 289800be..73e6be76 100644 --- a/tests/doc/errors.js +++ b/tests/doc/errors.js @@ -280,6 +280,24 @@ describe.skip('pretty errors', () => { }) }) +describe('tags on invalid nodes', () => { + test('!!map on scalar', () => { + const doc = YAML.parseDocument('!!map foo') + expect(doc.warnings).toMatchObject([ + { message: 'Unresolved tag: tag:yaml.org,2002:map' } + ]) + expect(doc.toJS()).toBe('foo') + }) + + test('!!str on map', () => { + const doc = YAML.parseDocument('!!str { answer: 42 }') + expect(doc.warnings).toMatchObject([ + { message: 'Unresolved tag: tag:yaml.org,2002:str' } + ]) + expect(doc.toJS()).toMatchObject({ answer: 42 }) + }) +}) + describe('invalid options', () => { test('unknown schema', () => { expect(() => new YAML.Document(undefined, { schema: 'foo' })).toThrow( From 54a6f76726f93dd9ca9e8aa7be513e3fea08ab31 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 00:21:46 +0200 Subject: [PATCH 16/24] Refactor core & yaml-1.1 tags as TypeScript --- src/ast/Scalar.ts | 3 + src/tags/{core.js => core.ts} | 27 ++--- src/tags/index.d.ts | 14 --- src/tags/{index.js => index.ts} | 0 src/tags/options.ts | 7 +- src/tags/yaml-1.1/{binary.js => binary.ts} | 39 +++--- src/tags/yaml-1.1/{index.js => index.ts} | 31 +++-- src/tags/yaml-1.1/{omap.js => omap.ts} | 62 +++++----- src/tags/yaml-1.1/pairs.js | 64 ---------- src/tags/yaml-1.1/pairs.ts | 75 ++++++++++++ src/tags/yaml-1.1/set.js | 84 ------------- src/tags/yaml-1.1/set.ts | 111 ++++++++++++++++++ .../yaml-1.1/{timestamp.js => timestamp.ts} | 54 ++++++--- 13 files changed, 314 insertions(+), 257 deletions(-) rename src/tags/{core.js => core.ts} (83%) delete mode 100644 src/tags/index.d.ts rename src/tags/{index.js => index.ts} (100%) rename src/tags/yaml-1.1/{binary.js => binary.ts} (69%) rename src/tags/yaml-1.1/{index.js => index.ts} (84%) rename src/tags/yaml-1.1/{omap.js => omap.ts} (51%) delete mode 100644 src/tags/yaml-1.1/pairs.js create mode 100644 src/tags/yaml-1.1/pairs.ts delete mode 100644 src/tags/yaml-1.1/set.js create mode 100644 src/tags/yaml-1.1/set.ts rename src/tags/yaml-1.1/{timestamp.js => timestamp.ts} (66%) diff --git a/src/ast/Scalar.ts b/src/ast/Scalar.ts index 280365b9..c93b67a1 100644 --- a/src/ast/Scalar.ts +++ b/src/ast/Scalar.ts @@ -33,6 +33,9 @@ export class Scalar extends Node { declare minFractionDigits?: number + /** Set during parsing to the source string value */ + declare source?: string + constructor(value: T) { super() this.value = value diff --git a/src/tags/core.js b/src/tags/core.ts similarity index 83% rename from src/tags/core.js rename to src/tags/core.ts index 260e3a5d..9824fa15 100644 --- a/src/tags/core.js +++ b/src/tags/core.ts @@ -1,23 +1,22 @@ -/* global BigInt */ - import { Scalar } from '../ast/Scalar.js' import { stringifyNumber } from '../stringify/stringifyNumber.js' import { failsafe } from './failsafe/index.js' import { boolOptions, intOptions, nullOptions } from './options.js' +import { ScalarTag } from './types.js' -const intIdentify = value => +const intIdentify = (value: unknown): value is number | bigint => typeof value === 'bigint' || Number.isInteger(value) -const intResolve = (src, offset, radix) => - intOptions.asBigInt ? BigInt(src) : parseInt(src.substring(offset), radix) +const intResolve = (str: string, offset: number, radix: number) => + intOptions.asBigInt ? BigInt(str) : parseInt(str.substring(offset), radix) -function intStringify(node, radix, prefix) { +function intStringify(node: Scalar, radix: number, prefix: string) { const { value } = node if (intIdentify(value) && value >= 0) return prefix + value.toString(radix) return stringifyNumber(node) } -export const nullObj = { +export const nullObj: ScalarTag & { test: RegExp } = { identify: value => value == null, createNode: () => new Scalar(null), default: true, @@ -29,7 +28,7 @@ export const nullObj = { source && nullObj.test.test(source) ? source : nullOptions.nullStr } -export const boolObj = { +export const boolObj: ScalarTag & { test: RegExp } = { identify: value => typeof value === 'boolean', default: true, tag: 'tag:yaml.org,2002:bool', @@ -45,7 +44,7 @@ export const boolObj = { } } -export const octObj = { +export const octObj: ScalarTag = { identify: value => intIdentify(value) && value >= 0, default: true, tag: 'tag:yaml.org,2002:int', @@ -56,7 +55,7 @@ export const octObj = { stringify: node => intStringify(node, 8, '0o') } -export const intObj = { +export const intObj: ScalarTag = { identify: intIdentify, default: true, tag: 'tag:yaml.org,2002:int', @@ -66,7 +65,7 @@ export const intObj = { stringify: stringifyNumber } -export const hexObj = { +export const hexObj: ScalarTag = { identify: value => intIdentify(value) && value >= 0, default: true, tag: 'tag:yaml.org,2002:int', @@ -77,7 +76,7 @@ export const hexObj = { stringify: node => intStringify(node, 16, '0x') } -export const nanObj = { +export const nanObj: ScalarTag = { identify: value => typeof value === 'number', default: true, tag: 'tag:yaml.org,2002:float', @@ -91,7 +90,7 @@ export const nanObj = { stringify: stringifyNumber } -export const expObj = { +export const expObj: ScalarTag = { identify: value => typeof value === 'number', default: true, tag: 'tag:yaml.org,2002:float', @@ -101,7 +100,7 @@ export const expObj = { stringify: ({ value }) => Number(value).toExponential() } -export const floatObj = { +export const floatObj: ScalarTag = { identify: value => typeof value === 'number', default: true, tag: 'tag:yaml.org,2002:float', diff --git a/src/tags/index.d.ts b/src/tags/index.d.ts deleted file mode 100644 index 25faa3e6..00000000 --- a/src/tags/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TagId, TagObj } from './types.js' - -import { failsafe } from './failsafe/index.js' -import { json } from './json.js' - -export const schemas: { - core: TagObj[] - failsafe: typeof failsafe - json: typeof json - yaml11: TagObj[] -} - -//export const schemas: Record -export const tags: Record diff --git a/src/tags/index.js b/src/tags/index.ts similarity index 100% rename from src/tags/index.js rename to src/tags/index.ts diff --git a/src/tags/options.ts b/src/tags/options.ts index 80ab4dab..b686ff18 100644 --- a/src/tags/options.ts +++ b/src/tags/options.ts @@ -6,7 +6,12 @@ export const binaryOptions = { * * Default: `'BLOCK_LITERAL'` */ - defaultType: Type.BLOCK_LITERAL, + defaultType: Type.BLOCK_LITERAL as + | Type.BLOCK_FOLDED + | Type.BLOCK_LITERAL + | Type.PLAIN + | Type.QUOTE_DOUBLE + | Type.QUOTE_SINGLE, /** * Maximum line width for `!!binary`. diff --git a/src/tags/yaml-1.1/binary.js b/src/tags/yaml-1.1/binary.ts similarity index 69% rename from src/tags/yaml-1.1/binary.js rename to src/tags/yaml-1.1/binary.ts index fcd255e6..016bda40 100644 --- a/src/tags/yaml-1.1/binary.js +++ b/src/tags/yaml-1.1/binary.ts @@ -1,13 +1,17 @@ /* global atob, btoa, Buffer */ +import type { Scalar } from '../../ast/index.js' import { Type } from '../../constants.js' import { stringifyString } from '../../stringify/stringifyString.js' import { binaryOptions as options } from '../options.js' +import type { ScalarTag } from '../types.js' -export const binary = { +export const binary: ScalarTag = { identify: value => value instanceof Uint8Array, // Buffer inherits from Uint8Array default: false, tag: 'tag:yaml.org,2002:binary', + options, + /** * Returns a Buffer in node and an Uint8Array in browsers * @@ -32,37 +36,38 @@ export const binary = { return src } }, - options, - stringify: ({ comment, type, value }, ctx, onComment, onChompKeep) => { - let src + + stringify({ comment, type, value }, ctx, onComment, onChompKeep) { + const buf = value as Uint8Array // checked earlier by binary.identify() + let str: string if (typeof Buffer === 'function') { - src = - value instanceof Buffer - ? value.toString('base64') - : Buffer.from(value.buffer).toString('base64') + str = + buf instanceof Buffer + ? buf.toString('base64') + : Buffer.from(buf.buffer).toString('base64') } else if (typeof btoa === 'function') { let s = '' - for (let i = 0; i < value.length; ++i) s += String.fromCharCode(value[i]) - src = btoa(s) + for (let i = 0; i < buf.length; ++i) s += String.fromCharCode(buf[i]) + str = btoa(s) } else { throw new Error( 'This environment does not support writing binary tags; either Buffer or btoa is required' ) } + if (!type) type = options.defaultType - if (type === Type.QUOTE_DOUBLE) { - value = src - } else { + if (type !== Type.QUOTE_DOUBLE) { const { lineWidth } = options - const n = Math.ceil(src.length / lineWidth) + const n = Math.ceil(str.length / lineWidth) const lines = new Array(n) for (let i = 0, o = 0; i < n; ++i, o += lineWidth) { - lines[i] = src.substr(o, lineWidth) + lines[i] = str.substr(o, lineWidth) } - value = lines.join(type === Type.BLOCK_LITERAL ? '\n' : ' ') + str = lines.join(type === Type.BLOCK_LITERAL ? '\n' : ' ') } + return stringifyString( - { comment, type, value }, + { comment, type, value: str } as Scalar, ctx, onComment, onChompKeep diff --git a/src/tags/yaml-1.1/index.js b/src/tags/yaml-1.1/index.ts similarity index 84% rename from src/tags/yaml-1.1/index.js rename to src/tags/yaml-1.1/index.ts index a3042d2a..da610116 100644 --- a/src/tags/yaml-1.1/index.js +++ b/src/tags/yaml-1.1/index.ts @@ -1,16 +1,15 @@ -/* global BigInt */ - import { Scalar } from '../../ast/Scalar.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { failsafe } from '../failsafe/index.js' import { boolOptions, intOptions, nullOptions } from '../options.js' +import { ScalarTag } from '../types.js' import { binary } from './binary.js' import { omap } from './omap.js' import { pairs } from './pairs.js' import { set } from './set.js' import { intTime, floatTime, timestamp } from './timestamp.js' -const nullObj = { +const nullObj: ScalarTag & { test: RegExp } = { identify: value => value == null, createNode: () => new Scalar(null), default: true, @@ -22,13 +21,13 @@ const nullObj = { source && nullObj.test.test(source) ? source : nullOptions.nullStr } -const boolStringify = ({ value, source }) => { +const boolStringify = ({ value, source }: Scalar) => { const boolObj = value ? trueObj : falseObj if (source && boolObj.test.test(source)) return source return value ? boolOptions.trueStr : boolOptions.falseStr } -const trueObj = { +const trueObj: ScalarTag & { test: RegExp } = { identify: value => value === true, default: true, tag: 'tag:yaml.org,2002:bool', @@ -38,7 +37,7 @@ const trueObj = { stringify: boolStringify } -const falseObj = { +const falseObj: ScalarTag & { test: RegExp } = { identify: value => value === false, default: true, tag: 'tag:yaml.org,2002:bool', @@ -48,10 +47,10 @@ const falseObj = { stringify: boolStringify } -const intIdentify = value => +const intIdentify = (value: unknown): value is number | bigint => typeof value === 'bigint' || Number.isInteger(value) -function intResolve(str, offset, radix) { +function intResolve(str: string, offset: number, radix: number) { const sign = str[0] if (sign === '-' || sign === '+') offset += 1 str = str.substring(offset).replace(/_/g, '') @@ -74,7 +73,7 @@ function intResolve(str, offset, radix) { return sign === '-' ? -1 * n : n } -function intStringify(node, radix, prefix) { +function intStringify(node: Scalar, radix: number, prefix: string) { const { value } = node if (intIdentify(value)) { const str = value.toString(radix) @@ -94,7 +93,7 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'BIN', test: /^[-+]?0b[0-1_]+$/, - resolve: str => intResolve(str, 2, 2), + resolve: (str: string) => intResolve(str, 2, 2), stringify: node => intStringify(node, 2, '0b') }, { @@ -103,7 +102,7 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'OCT', test: /^[-+]?0[0-7_]+$/, - resolve: str => intResolve(str, 1, 8), + resolve: (str: string) => intResolve(str, 1, 8), stringify: node => intStringify(node, 8, '0') }, { @@ -111,7 +110,7 @@ export const yaml11 = failsafe.concat( default: true, tag: 'tag:yaml.org,2002:int', test: /^[-+]?[0-9][0-9_]*$/, - resolve: str => intResolve(str, 0, 10), + resolve: (str: string) => intResolve(str, 0, 10), stringify: stringifyNumber }, { @@ -120,7 +119,7 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:int', format: 'HEX', test: /^[-+]?0x[0-9a-fA-F_]+$/, - resolve: str => intResolve(str, 2, 16), + resolve: (str: string) => intResolve(str, 2, 16), stringify: node => intStringify(node, 16, '0x') }, { @@ -128,7 +127,7 @@ export const yaml11 = failsafe.concat( default: true, tag: 'tag:yaml.org,2002:float', test: /^[-+]?\.(?:inf|Inf|INF|nan|NaN|NAN)$/, - resolve: str => + resolve: (str: string) => str.slice(-3).toLowerCase() === 'nan' ? NaN : str[0] === '-' @@ -142,7 +141,7 @@ export const yaml11 = failsafe.concat( tag: 'tag:yaml.org,2002:float', format: 'EXP', test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/, - resolve: str => parseFloat(str.replace(/_/g, '')), + resolve: (str: string) => parseFloat(str.replace(/_/g, '')), stringify: ({ value }) => Number(value).toExponential() }, { @@ -150,7 +149,7 @@ export const yaml11 = failsafe.concat( default: true, tag: 'tag:yaml.org,2002:float', test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/, - resolve(str) { + resolve(str: string) { const node = new Scalar(parseFloat(str.replace(/_/g, ''))) const dot = str.indexOf('.') if (dot !== -1) { diff --git a/src/tags/yaml-1.1/omap.js b/src/tags/yaml-1.1/omap.ts similarity index 51% rename from src/tags/yaml-1.1/omap.js rename to src/tags/yaml-1.1/omap.ts index 15c0c485..2743da48 100644 --- a/src/tags/yaml-1.1/omap.js +++ b/src/tags/yaml-1.1/omap.ts @@ -2,8 +2,9 @@ import { Pair } from '../../ast/Pair.js' import { Scalar } from '../../ast/Scalar.js' import { YAMLMap } from '../../ast/YAMLMap.js' import { YAMLSeq } from '../../ast/YAMLSeq.js' -import { toJS } from '../../ast/toJS.js' -import { createPairs, parsePairs } from './pairs.js' +import { toJS, ToJSContext } from '../../ast/toJS.js' +import { createPairs, resolvePairs } from './pairs.js' +import { CollectionTag } from '../types.js' export class YAMLOMap extends YAMLSeq { static tag = 'tag:yaml.org,2002:omap' @@ -19,7 +20,12 @@ export class YAMLOMap extends YAMLSeq { has = YAMLMap.prototype.has.bind(this) set = YAMLMap.prototype.set.bind(this) - toJSON(_, ctx) { + /** + * If `ctx` is given, the return type is actually `Map`, + * but TypeScript won't allow widening the signature of a child method. + */ + toJSON(_?: unknown, ctx?: ToJSContext) { + if (!ctx) return super.toJSON(_) const map = new Map() if (ctx && ctx.onCreate) ctx.onCreate(map) for (const pair of this.items) { @@ -34,38 +40,36 @@ export class YAMLOMap extends YAMLSeq { throw new Error('Ordered maps must not include duplicate keys') map.set(key, value) } - return map + return (map as unknown) as unknown[] } } -function parseOMap(seq, onError) { - const pairs = parsePairs(seq, onError) - const seenKeys = [] - for (const { key } of pairs.items) { - if (key instanceof Scalar) { - if (seenKeys.includes(key.value)) { - onError(`Ordered maps must not include duplicate keys: ${key.value}`) - } else { - seenKeys.push(key.value) - } - } - } - return Object.assign(new YAMLOMap(), pairs) -} - -function createOMap(schema, iterable, ctx) { - const pairs = createPairs(schema, iterable, ctx) - const omap = new YAMLOMap() - omap.items = pairs.items - return omap -} - -export const omap = { +export const omap: CollectionTag = { collection: 'seq', identify: value => value instanceof Map, nodeClass: YAMLOMap, default: false, tag: 'tag:yaml.org,2002:omap', - resolve: parseOMap, - createNode: createOMap + + resolve(seq, onError) { + const pairs = resolvePairs(seq, onError) + const seenKeys: unknown[] = [] + for (const { key } of pairs.items) { + if (key instanceof Scalar) { + if (seenKeys.includes(key.value)) { + onError(`Ordered maps must not include duplicate keys: ${key.value}`) + } else { + seenKeys.push(key.value) + } + } + } + return Object.assign(new YAMLOMap(), pairs) + }, + + createNode(schema, iterable, ctx) { + const pairs = createPairs(schema, iterable, ctx) + const omap = new YAMLOMap() + omap.items = pairs.items + return omap + } } diff --git a/src/tags/yaml-1.1/pairs.js b/src/tags/yaml-1.1/pairs.js deleted file mode 100644 index 4a945c55..00000000 --- a/src/tags/yaml-1.1/pairs.js +++ /dev/null @@ -1,64 +0,0 @@ -import { createPair, Pair } from '../../ast/Pair.js' -import { YAMLMap } from '../../ast/YAMLMap.js' -import { YAMLSeq } from '../../ast/YAMLSeq.js' - -export function parsePairs(seq, onError) { - if (seq instanceof YAMLSeq) { - for (let i = 0; i < seq.items.length; ++i) { - let item = seq.items[i] - if (item instanceof Pair) continue - else if (item instanceof YAMLMap) { - if (item.items.length > 1) - onError('Each pair must have its own sequence indicator') - const pair = item.items[0] || new Pair() - if (item.commentBefore) - pair.commentBefore = pair.commentBefore - ? `${item.commentBefore}\n${pair.commentBefore}` - : item.commentBefore - if (item.comment) - pair.comment = pair.comment - ? `${item.comment}\n${pair.comment}` - : item.comment - item = pair - } - seq.items[i] = item instanceof Pair ? item : new Pair(item) - } - } else onError('Expected a sequence for this tag') - return seq -} - -export function createPairs(schema, iterable, ctx) { - const { replacer } = ctx - const pairs = new YAMLSeq(schema) - pairs.tag = 'tag:yaml.org,2002:pairs' - let i = 0 - for (let it of iterable) { - if (typeof replacer === 'function') - it = replacer.call(iterable, String(i++), it) - let key, value - if (Array.isArray(it)) { - if (it.length === 2) { - key = it[0] - value = it[1] - } else throw new TypeError(`Expected [key, value] tuple: ${it}`) - } else if (it && it instanceof Object) { - const keys = Object.keys(it) - if (keys.length === 1) { - key = keys[0] - value = it[key] - } else throw new TypeError(`Expected { key: value } tuple: ${it}`) - } else { - key = it - } - pairs.items.push(createPair(key, value, ctx)) - } - return pairs -} - -export const pairs = { - collection: 'seq', - default: false, - tag: 'tag:yaml.org,2002:pairs', - resolve: parsePairs, - createNode: createPairs -} diff --git a/src/tags/yaml-1.1/pairs.ts b/src/tags/yaml-1.1/pairs.ts new file mode 100644 index 00000000..f6613900 --- /dev/null +++ b/src/tags/yaml-1.1/pairs.ts @@ -0,0 +1,75 @@ +import { createPair, Pair } from '../../ast/Pair.js' +import { YAMLMap } from '../../ast/YAMLMap.js' +import { YAMLSeq } from '../../ast/YAMLSeq.js' +import type { CreateNodeContext } from '../../doc/createNode.js' +import type { Schema } from '../../doc/Schema.js' +import type { CollectionTag } from '../types.js' + +export function resolvePairs( + seq: YAMLSeq | YAMLMap, + onError: (message: string) => void +) { + if (seq instanceof YAMLSeq) { + for (let i = 0; i < seq.items.length; ++i) { + let item = seq.items[i] + if (item instanceof Pair) continue + else if (item instanceof YAMLMap) { + if (item.items.length > 1) + onError('Each pair must have its own sequence indicator') + const pair = item.items[0] || new Pair(null) + if (item.commentBefore) + pair.commentBefore = pair.commentBefore + ? `${item.commentBefore}\n${pair.commentBefore}` + : item.commentBefore + if (item.comment) + pair.comment = pair.comment + ? `${item.comment}\n${pair.comment}` + : item.comment + item = pair + } + seq.items[i] = item instanceof Pair ? item : new Pair(item) + } + } else onError('Expected a sequence for this tag') + return seq as YAMLSeq +} + +export function createPairs( + schema: Schema, + iterable: unknown, + ctx: CreateNodeContext +) { + const { replacer } = ctx + const pairs = new YAMLSeq(schema) + pairs.tag = 'tag:yaml.org,2002:pairs' + let i = 0 + if (iterable && Symbol.iterator in Object(iterable)) + for (let it of iterable as Iterable) { + if (typeof replacer === 'function') + it = replacer.call(iterable, String(i++), it) + let key: unknown, value: unknown + if (Array.isArray(it)) { + if (it.length === 2) { + key = it[0] + value = it[1] + } else throw new TypeError(`Expected [key, value] tuple: ${it}`) + } else if (it && it instanceof Object) { + const keys = Object.keys(it) + if (keys.length === 1) { + key = keys[0] + value = (it as any)[key as string] + } else throw new TypeError(`Expected { key: value } tuple: ${it}`) + } else { + key = it + } + pairs.items.push(createPair(key, value, ctx)) + } + return pairs +} + +export const pairs: CollectionTag = { + collection: 'seq', + default: false, + tag: 'tag:yaml.org,2002:pairs', + resolve: resolvePairs, + createNode: createPairs +} diff --git a/src/tags/yaml-1.1/set.js b/src/tags/yaml-1.1/set.js deleted file mode 100644 index 8e7de370..00000000 --- a/src/tags/yaml-1.1/set.js +++ /dev/null @@ -1,84 +0,0 @@ -import { createPair, Pair } from '../../ast/Pair.js' -import { Scalar } from '../../ast/Scalar.js' -import { YAMLMap, findPair } from '../../ast/YAMLMap.js' - -export class YAMLSet extends YAMLMap { - static tag = 'tag:yaml.org,2002:set' - - constructor(schema) { - super(schema) - this.tag = YAMLSet.tag - } - - add(key) { - const pair = key instanceof Pair ? key : new Pair(key) - const prev = findPair(this.items, pair.key) - if (!prev) this.items.push(pair) - } - - get(key, keepPair) { - const pair = findPair(this.items, key) - return !keepPair && pair instanceof Pair - ? pair.key instanceof Scalar - ? pair.key.value - : pair.key - : pair - } - - set(key, value) { - if (typeof value !== 'boolean') - throw new Error( - `Expected boolean value for set(key, value) in a YAML set, not ${typeof value}` - ) - const prev = findPair(this.items, key) - if (prev && !value) { - this.items.splice(this.items.indexOf(prev), 1) - } else if (!prev && value) { - this.items.push(new Pair(key)) - } - } - - toJSON(_, ctx) { - return super.toJSON(_, ctx, Set) - } - - toString(ctx, onComment, onChompKeep) { - if (!ctx) return JSON.stringify(this) - if (this.hasAllNullValues(true)) - return super.toString( - Object.assign({}, ctx, { allNullValues: true }), - onComment, - onChompKeep - ) - else throw new Error('Set items must all have null values') - } -} - -function parseSet(map, onError) { - if (map instanceof YAMLMap) { - if (map.hasAllNullValues(true)) return Object.assign(new YAMLSet(), map) - else onError('Set items must all have null values') - } else onError('Expected a mapping for this tag') - return map -} - -function createSet(schema, iterable, ctx) { - const { replacer } = ctx - const set = new YAMLSet(schema) - for (let value of iterable) { - if (typeof replacer === 'function') - value = replacer.call(iterable, value, value) - set.items.push(createPair(value, null, ctx)) - } - return set -} - -export const set = { - collection: 'map', - identify: value => value instanceof Set, - nodeClass: YAMLSet, - default: false, - tag: 'tag:yaml.org,2002:set', - resolve: parseSet, - createNode: createSet -} diff --git a/src/tags/yaml-1.1/set.ts b/src/tags/yaml-1.1/set.ts new file mode 100644 index 00000000..fa72884b --- /dev/null +++ b/src/tags/yaml-1.1/set.ts @@ -0,0 +1,111 @@ +import { createPair, Pair } from '../../ast/Pair.js' +import { Scalar } from '../../ast/Scalar.js' +import { ToJSContext } from '../../ast/toJS.js' +import { YAMLMap, findPair } from '../../ast/YAMLMap.js' +import type { Schema } from '../../doc/Schema.js' +import type { StringifyContext } from '../../stringify/stringify.js' +import type { CollectionTag } from '../types.js' + +export class YAMLSet extends YAMLMap | null> { + static tag = 'tag:yaml.org,2002:set' + + constructor(schema?: Schema) { + super(schema) + this.tag = YAMLSet.tag + } + + add( + key: + | T + | Pair | null> + | { key: T; value: Scalar | null } + ) { + let pair: Pair | null> + if (key instanceof Pair) pair = key + else if ( + typeof key === 'object' && + 'key' in key && + 'value' in key && + key.value === null + ) + pair = new Pair(key.key, null) + else pair = new Pair(key as T, null) + const prev = findPair(this.items, pair.key) + if (!prev) this.items.push(pair) + } + + get(key?: T, keepPair?: boolean) { + const pair = findPair(this.items, key) + return !keepPair && pair instanceof Pair + ? pair.key instanceof Scalar + ? pair.key.value + : pair.key + : pair + } + + set(key: T, value: boolean): void + + /** Will throw; `value` must be boolean */ + set(key: T, value: null): void + set(key: T, value: boolean | null) { + if (typeof value !== 'boolean') + throw new Error( + `Expected boolean value for set(key, value) in a YAML set, not ${typeof value}` + ) + const prev = findPair(this.items, key) + if (prev && !value) { + this.items.splice(this.items.indexOf(prev), 1) + } else if (!prev && value) { + this.items.push(new Pair(key)) + } + } + + toJSON(_?: unknown, ctx?: ToJSContext) { + return super.toJSON(_, ctx, Set) + } + + toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ) { + if (!ctx) return JSON.stringify(this) + if (this.hasAllNullValues(true)) + return super.toString( + Object.assign({}, ctx, { allNullValues: true }), + onComment, + onChompKeep + ) + else throw new Error('Set items must all have null values') + } +} + +export const set: CollectionTag = { + collection: 'map', + identify: value => value instanceof Set, + nodeClass: YAMLSet, + default: false, + tag: 'tag:yaml.org,2002:set', + + resolve(map, onError) { + if (map instanceof YAMLMap) { + if (map.hasAllNullValues(true)) return Object.assign(new YAMLSet(), map) + else onError('Set items must all have null values') + } else onError('Expected a mapping for this tag') + return map + }, + + createNode(schema, iterable, ctx) { + const { replacer } = ctx + const set = new YAMLSet(schema) + if (iterable && Symbol.iterator in Object(iterable)) + for (let value of iterable as Iterable) { + if (typeof replacer === 'function') + value = replacer.call(iterable, value, value) + set.items.push( + createPair(value, null, ctx) as Pair> + ) + } + return set + } +} diff --git a/src/tags/yaml-1.1/timestamp.js b/src/tags/yaml-1.1/timestamp.ts similarity index 66% rename from src/tags/yaml-1.1/timestamp.js rename to src/tags/yaml-1.1/timestamp.ts index cd325d90..d1181c12 100644 --- a/src/tags/yaml-1.1/timestamp.js +++ b/src/tags/yaml-1.1/timestamp.ts @@ -1,24 +1,37 @@ /* global BigInt */ -import { intOptions } from '../options.js' +import type { Scalar } from '../../ast/index.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' +import { intOptions } from '../options.js' +import type { ScalarTag } from '../types.js' -const parseSexagesimal = (str, isInt) => { +/** Internal types handle bigint as number, because TS can't figure it out. */ +function parseSexagesimal(str: string, isInt: B) { const sign = str[0] const parts = sign === '-' || sign === '+' ? str.substring(1) : str - const num = n => (isInt && intOptions.asBigInt ? BigInt(n) : Number(n)) + const num = (n: unknown) => + isInt && intOptions.asBigInt + ? ((BigInt(n) as unknown) as number) + : Number(n) const res = parts .replace(/_/g, '') .split(':') .reduce((res, p) => res * num(60) + num(p), num(0)) - return sign === '-' ? num(-1) * res : res + return (sign === '-' ? num(-1) * res : res) as B extends true + ? number | bigint + : number } -// hhhh:mm:ss.sss -const stringifySexagesimal = ({ value }) => { - let num = n => n - if (typeof value === 'bigint') num = n => BigInt(n) - else if (isNaN(value) || !isFinite(value)) return stringifyNumber(value) +/** + * hhhh:mm:ss.sss + * + * Internal types handle bigint as number, because TS can't figure it out. + */ +function stringifySexagesimal(node: Scalar) { + let { value } = node as Scalar + let num = (n: number) => n + if (typeof value === 'bigint') num = n => (BigInt(n) as unknown) as number + else if (isNaN(value) || !isFinite(value)) return stringifyNumber(node) let sign = '' if (value < 0) { sign = '-' @@ -45,7 +58,7 @@ const stringifySexagesimal = ({ value }) => { ) } -export const intTime = { +export const intTime: ScalarTag = { identify: value => typeof value === 'bigint' || Number.isInteger(value), default: true, tag: 'tag:yaml.org,2002:int', @@ -55,7 +68,7 @@ export const intTime = { stringify: stringifySexagesimal } -export const floatTime = { +export const floatTime: ScalarTag = { identify: value => typeof value === 'number', default: true, tag: 'tag:yaml.org,2002:float', @@ -65,10 +78,11 @@ export const floatTime = { stringify: stringifySexagesimal } -export const timestamp = { +export const timestamp: ScalarTag & { test: RegExp } = { identify: value => value instanceof Date, default: true, tag: 'tag:yaml.org,2002:timestamp', + // If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part // may be omitted altogether, resulting in a date format. In such a case, the time part is // assumed to be 00:00:00Z (start of day, UTC). @@ -80,11 +94,13 @@ export const timestamp = { '(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?' + // Z | +5 | -03:30 ')?$' ), + resolve(str) { - let [, year, month, day, hour, minute, second, millisec, tz] = str.match( - timestamp.test - ) - if (millisec) millisec = (millisec + '00').substr(1, 3) + const match = str.match(timestamp.test) + if (!match) + throw new Error('!!timestamp expects a date, starting with yyyy-mm-dd') + const [, year, month, day, hour, minute, second] = match.map(Number) + const millisec = match[7] ? Number((match[7] + '00').substr(1, 3)) : 0 let date = Date.UTC( year, month - 1, @@ -92,8 +108,9 @@ export const timestamp = { hour || 0, minute || 0, second || 0, - millisec || 0 + millisec ) + const tz = match[8] if (tz && tz !== 'Z') { let d = parseSexagesimal(tz, false) if (Math.abs(d) < 30) d *= 60 @@ -101,6 +118,7 @@ export const timestamp = { } return new Date(date) }, + stringify: ({ value }) => - value.toISOString().replace(/((T00:00)?:00)?\.000Z$/, '') + (value as Date).toISOString().replace(/((T00:00)?:00)?\.000Z$/, '') } From b3c8986070b0f06fed4fb3f140865c4e9b2cfda7 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 11:48:54 +0200 Subject: [PATCH 17/24] Identify nodes by symbols rather than instanceof (no more dependency loops!) BREAKING CHANGE: The Node base class is no longer exported. Prior users of it should instead use the new `isNode` function. --- src/ast/Alias.ts | 27 +++++++--- src/ast/Collection.ts | 31 +++++------ src/ast/Merge.ts | 10 ++-- src/ast/Node.ts | 71 +++++++++++++++++++++++--- src/ast/Pair.ts | 46 ++++++++++------- src/ast/Scalar.ts | 6 ++- src/ast/YAMLMap.ts | 26 ++++++---- src/ast/YAMLSeq.ts | 52 +++++++++++++++---- src/ast/index.ts | 2 +- src/ast/toJS.ts | 2 +- src/compose/compose-collection.ts | 9 ++-- src/compose/compose-node.ts | 4 +- src/compose/compose-scalar.ts | 9 ++-- src/compose/composer.ts | 7 ++- src/compose/resolve-flow-collection.ts | 10 ++-- src/compose/resolve-merge-pair.ts | 14 ++--- src/doc/Anchors.ts | 16 +++--- src/doc/Document.ts | 44 +++++++++------- src/doc/createNode.ts | 10 +++- src/doc/directives.ts | 6 +-- src/stringify/stringify.ts | 23 ++++----- src/tags/failsafe/map.ts | 3 +- src/tags/failsafe/seq.ts | 3 +- src/tags/yaml-1.1/omap.ts | 9 ++-- src/tags/yaml-1.1/pairs.ts | 9 ++-- src/tags/yaml-1.1/set.ts | 9 ++-- src/types.ts | 5 +- src/visit.ts | 49 +++++++++++------- types.js | 1 - types.mjs | 1 - 30 files changed, 321 insertions(+), 193 deletions(-) diff --git a/src/ast/Alias.ts b/src/ast/Alias.ts index 71cc91de..d1007e82 100644 --- a/src/ast/Alias.ts +++ b/src/ast/Alias.ts @@ -1,8 +1,14 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' -import { Collection } from './Collection.js' -import { Node } from './Node.js' -import { Pair } from './Pair.js' +import { + ALIAS, + isAlias, + isCollection, + isPair, + Node, + NodeBase, + NODE_TYPE +} from './Node.js' import { toJS, ToJSContext } from './toJS.js' export declare namespace Alias { @@ -11,7 +17,8 @@ export declare namespace Alias { } } -export class Alias extends Node { +export class Alias extends NodeBase { + [NODE_TYPE] = ALIAS source: T type: Type.ALIAS = Type.ALIAS @@ -50,7 +57,11 @@ export class Alias extends Node { // Only called when stringifying an alias mapping key while constructing // Object output. - toString({ anchors, doc, implicitKey, inStringifyKey }: StringifyContext) { + toString( + { anchors, doc, implicitKey, inStringifyKey }: StringifyContext, + _onComment?: () => void, + _onChompKeep?: () => void + ) { let anchor = Object.keys(anchors).find(a => anchors[a] === this.source) if (!anchor && inStringifyKey) anchor = doc.anchors.getName(this.source) || doc.anchors.newName() @@ -63,17 +74,17 @@ export class Alias extends Node { } function getAliasCount(node: unknown, anchors: ToJSContext['anchors']): number { - if (node instanceof Alias) { + if (isAlias(node)) { const anchor = anchors && anchors.get(node.source) return anchor ? anchor.count * anchor.aliasCount : 0 - } else if (node instanceof Collection) { + } else if (isCollection(node)) { let count = 0 for (const item of node.items) { const c = getAliasCount(item, anchors) if (c > count) count = c } return count - } else if (node instanceof Pair) { + } else if (isPair(node)) { const kc = getAliasCount(node.key, anchors) const vc = getAliasCount(node.value, anchors) return Math.max(kc, vc) diff --git a/src/ast/Collection.ts b/src/ast/Collection.ts index 6fc0f6c6..6d01166f 100644 --- a/src/ast/Collection.ts +++ b/src/ast/Collection.ts @@ -3,10 +3,8 @@ import { createNode } from '../doc/createNode.js' import type { Schema } from '../doc/Schema.js' import { addComment } from '../stringify/addComment.js' import type { StringifyContext } from '../stringify/stringify.js' - -import { Node } from './Node.js' +import { isCollection, isNode, isPair, isScalar, NodeBase, NODE_TYPE } from './Node.js' import type { Pair } from './Pair.js' -import { Scalar } from './Scalar.js' export function collectionFromPath( schema: Schema, @@ -57,10 +55,12 @@ export declare namespace Collection { str: string } } -export abstract class Collection extends Node { +export abstract class Collection extends NodeBase { static maxFlowStringSingleLineLength = 60 - schema: Schema | undefined + schema: Schema | undefined; + + declare [NODE_TYPE]: symbol declare items: unknown[] @@ -118,7 +118,7 @@ export abstract class Collection extends Node { else { const [key, ...rest] = path const node = this.get(key, true) - if (node instanceof Collection) node.addIn(rest, value) + if (isCollection(node)) node.addIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) else @@ -135,7 +135,7 @@ export abstract class Collection extends Node { deleteIn([key, ...rest]: Iterable): boolean { if (rest.length === 0) return this.delete(key) const node = this.get(key, true) - if (node instanceof Collection) return node.deleteIn(rest) + if (isCollection(node)) return node.deleteIn(rest) else throw new Error( `Expected YAML collection at ${key}. Remaining path: ${rest}` @@ -150,21 +150,18 @@ export abstract class Collection extends Node { getIn([key, ...rest]: Iterable, keepScalar?: boolean): unknown { const node = this.get(key, true) if (rest.length === 0) - return !keepScalar && node instanceof Scalar ? node.value : node - else - return node instanceof Collection - ? node.getIn(rest, keepScalar) - : undefined + return !keepScalar && isScalar(node) ? node.value : node + else return isCollection(node) ? node.getIn(rest, keepScalar) : undefined } hasAllNullValues(allowScalar?: boolean) { return this.items.every(node => { - if (!node || (node as Node).type !== 'PAIR') return false + if (!node || isNode(node)) return false const n = (node as Pair).value return ( n == null || (allowScalar && - n instanceof Scalar && + isScalar(n) && n.value == null && !n.commentBefore && !n.comment && @@ -179,7 +176,7 @@ export abstract class Collection extends Node { hasIn([key, ...rest]: Iterable): boolean { if (rest.length === 0) return this.has(key) const node = this.get(key, true) - return node instanceof Collection ? node.hasIn(rest) : false + return isCollection(node) ? node.hasIn(rest) : false } /** @@ -191,7 +188,7 @@ export abstract class Collection extends Node { this.set(key, value) } else { const node = this.get(key, true) - if (node instanceof Collection) node.setIn(rest, value) + if (isCollection(node)) node.setIn(rest, value) else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value)) else @@ -217,7 +214,7 @@ export abstract class Collection extends Node { const nodes = this.items.reduce( (nodes: Collection.StringifyNode[], item, i) => { let comment: string | null = null - if (item instanceof Node) { + if (isNode(item) || isPair(item)) { if (!chompKeep && item.spaceBefore) nodes.push({ type: 'comment', str: '' }) diff --git a/src/ast/Merge.ts b/src/ast/Merge.ts index bad7e210..0d844675 100644 --- a/src/ast/Merge.ts +++ b/src/ast/Merge.ts @@ -1,10 +1,9 @@ import { StringifyContext } from '../stringify/stringify.js' import type { Alias } from './index.js' -import { Node } from './Node.js' +import { isMap, isNode, isPair, isSeq } from './Node.js' import { Pair, PairType } from './Pair.js' import { Scalar } from './Scalar.js' import type { ToJSContext } from './toJS.js' -import { YAMLMap } from './YAMLMap.js' import { YAMLSeq } from './YAMLSeq.js' export class Merge extends Pair> { @@ -15,8 +14,8 @@ export class Merge extends Pair> { declare value: YAMLSeq constructor(pair?: Pair>) { - if (pair instanceof Pair && pair.value instanceof Node) { - if (pair.value instanceof YAMLSeq) super(pair.key, pair.value) + if (isPair(pair) && isNode(pair.value)) { + if (isSeq(pair.value)) super(pair.key, pair.value) else { const seq = new YAMLSeq() seq.items.push(pair.value) @@ -45,8 +44,7 @@ export class Merge extends Pair> { | Record ) { for (const { source } of this.value.items) { - if (!(source instanceof YAMLMap)) - throw new Error('Merge sources must be maps') + if (!isMap(source)) throw new Error('Merge sources must be maps') const srcMap = source.toJSON(null, ctx, Map) for (const [key, value] of srcMap) { if (map instanceof Map) { diff --git a/src/ast/Node.ts b/src/ast/Node.ts index b2623061..baead79d 100644 --- a/src/ast/Node.ts +++ b/src/ast/Node.ts @@ -1,14 +1,21 @@ import type { Type } from '../constants.js' -import { StringifyContext } from '../stringify/stringify.js' -import type { PairType } from './Pair.js' +import type { Document } from '../doc/Document.js' +import type { StringifyContext } from '../stringify/stringify.js' +import type { Alias } from './Alias.js' +import type { Pair, PairType } from './Pair.js' +import type { Scalar } from './Scalar.js' +import type { YAMLMap } from './YAMLMap.js' +import type { YAMLSeq } from './YAMLSeq.js' -export declare namespace Node { - interface Parsed extends Node { - range: [number, number] - } -} +export type Node = Alias | Scalar | YAMLMap | YAMLSeq + +export type ParsedNode = + | Alias.Parsed + | Scalar.Parsed + | YAMLMap.Parsed + | YAMLSeq.Parsed -export abstract class Node { +export abstract class NodeBase { /** A comment on or immediately after this */ declare comment?: string | null @@ -42,3 +49,51 @@ export abstract class Node { /** The type of this node */ declare type?: Type | PairType } + +export const ALIAS = Symbol.for('yaml.alias') +export const DOC = Symbol.for('yaml.document') +export const MAP = Symbol.for('yaml.map') +export const PAIR = Symbol.for('yaml.pair') +export const SCALAR = Symbol.for('yaml.scalar') +export const SEQ = Symbol.for('yaml.seq') +export const NODE_TYPE = Symbol.for('yaml.node.type') + +export const isAlias = (node: any): node is Alias => + !!node && typeof node === 'object' && node[NODE_TYPE] === ALIAS + +export const isDocument = (node: any): node is Document => + !!node && typeof node === 'object' && node[NODE_TYPE] === DOC + +export const isMap = (node: any): node is YAMLMap => + !!node && typeof node === 'object' && node[NODE_TYPE] === MAP + +export const isPair = (node: any): node is Pair => + !!node && typeof node === 'object' && node[NODE_TYPE] === PAIR + +export const isScalar = (node: any): node is Scalar => + !!node && typeof node === 'object' && node[NODE_TYPE] === SCALAR + +export const isSeq = (node: any): node is YAMLSeq => + !!node && typeof node === 'object' && node[NODE_TYPE] === SEQ + +export function isCollection(node: any): node is YAMLMap | YAMLSeq { + if (node && typeof node === 'object') + switch (node[NODE_TYPE]) { + case MAP: + case SEQ: + return true + } + return false +} + +export function isNode(node: any): node is Node { + if (node && typeof node === 'object') + switch (node[NODE_TYPE]) { + case ALIAS: + case MAP: + case SCALAR: + case SEQ: + return true + } + return false +} diff --git a/src/ast/Pair.ts b/src/ast/Pair.ts index 7565bac0..034a83e3 100644 --- a/src/ast/Pair.ts +++ b/src/ast/Pair.ts @@ -4,11 +4,18 @@ import { warn } from '../log.js' import { addComment } from '../stringify/addComment.js' import { StringifyContext } from '../stringify/stringify.js' -import { Collection } from './Collection.js' -import { Node } from './Node.js' import { Scalar } from './Scalar.js' -import { YAMLSeq } from './YAMLSeq.js' import { toJS, ToJSContext } from './toJS.js' +import { + isCollection, + isNode, + isScalar, + isSeq, + Node, + NodeBase, + NODE_TYPE, + PAIR +} from './Node.js' export function createPair( key: unknown, @@ -25,7 +32,9 @@ export enum PairType { MERGE_PAIR = 'MERGE_PAIR' } -export class Pair extends Node { +export class Pair extends NodeBase { + [NODE_TYPE] = PAIR + /** Always Node or null when parsed, but can be set to anything. */ key: K @@ -43,12 +52,12 @@ export class Pair extends Node { // @ts-ignore This is fine. get commentBefore() { - return this.key instanceof Node ? this.key.commentBefore : undefined + return isNode(this.key) ? this.key.commentBefore : undefined } set commentBefore(cb) { if (this.key == null) this.key = new Scalar(null) as any // FIXME - if (this.key instanceof Node) this.key.commentBefore = cb + if (isNode(this.key)) this.key.commentBefore = cb else { const msg = 'Pair.commentBefore is an alias for Pair.key.commentBefore. To set it, the key must be a Node.' @@ -58,12 +67,12 @@ export class Pair extends Node { // @ts-ignore This is fine. get spaceBefore() { - return this.key instanceof Node ? this.key.spaceBefore : undefined + return isNode(this.key) ? this.key.spaceBefore : undefined } set spaceBefore(sb) { if (this.key == null) this.key = new Scalar(null) as any // FIXME - if (this.key instanceof Node) this.key.spaceBefore = sb + if (isNode(this.key)) this.key.spaceBefore = sb else { const msg = 'Pair.spaceBefore is an alias for Pair.key.spaceBefore. To set it, the key must be a Node.' @@ -112,12 +121,12 @@ export class Pair extends Node { if (!ctx || !ctx.doc) return JSON.stringify(this) const { indent: indentSize, indentSeq, simpleKeys } = ctx.doc.options let { key, value }: { key: K; value: V | Node | null } = this - let keyComment = (key instanceof Node && key.comment) || null + let keyComment = (isNode(key) && key.comment) || null if (simpleKeys) { if (keyComment) { throw new Error('With simple keys, key nodes cannot have comments') } - if (key instanceof Collection) { + if (isCollection(key)) { const msg = 'With simple keys, collection cannot be used as a key value' throw new Error(msg) } @@ -126,10 +135,9 @@ export class Pair extends Node { !simpleKeys && (!key || (keyComment && value == null) || - (key instanceof Node - ? key instanceof Collection || - key.type === Type.BLOCK_FOLDED || - key.type === Type.BLOCK_LITERAL + isCollection(key) || + (isScalar(key) + ? key.type === Type.BLOCK_FOLDED || key.type === Type.BLOCK_LITERAL : typeof key === 'object')) const { allNullValues, doc, indent, indentStep, stringify } = ctx ctx = Object.assign({}, ctx, { @@ -178,7 +186,7 @@ export class Pair extends Node { let vcb = '' let valueComment = null - if (value instanceof Node) { + if (isNode(value)) { if (value.spaceBefore) vcb = '\n' if (value.commentBefore) { const cs = value.commentBefore.replace(/^/gm, `${ctx.indent}#`) @@ -189,7 +197,7 @@ export class Pair extends Node { value = doc.createNode(value) } ctx.implicitKey = false - if (!explicitKey && !keyComment && !this.comment && value instanceof Scalar) + if (!explicitKey && !keyComment && !this.comment && isScalar(value)) ctx.indentAtStart = str.length + 1 chompKeep = false if ( @@ -197,7 +205,7 @@ export class Pair extends Node { indentSize >= 2 && !ctx.inFlow && !explicitKey && - value instanceof YAMLSeq && + isSeq(value) && value.type !== Type.FLOW_SEQ && !value.tag && !doc.anchors.getName(value) @@ -214,7 +222,7 @@ export class Pair extends Node { let ws = ' ' if (vcb || keyComment || this.comment) { ws = `${vcb}\n${ctx.indent}` - } else if (!explicitKey && value instanceof Collection) { + } else if (!explicitKey && isCollection(value)) { const flow = valueStr[0] === '[' || valueStr[0] === '{' if (!flow || valueStr.includes('\n')) ws = `\n${ctx.indent}` } else if (valueStr[0] === '\n') ws = '' @@ -230,7 +238,7 @@ function stringifyKey( ) { if (jsKey === null) return '' if (typeof jsKey !== 'object') return String(jsKey) - if (key instanceof Node && ctx && ctx.doc) { + if (isNode(key) && ctx && ctx.doc) { const strKey = key.toString({ anchors: Object.create(null), doc: ctx.doc, diff --git a/src/ast/Scalar.ts b/src/ast/Scalar.ts index c93b67a1..7176574f 100644 --- a/src/ast/Scalar.ts +++ b/src/ast/Scalar.ts @@ -1,5 +1,5 @@ import { Type } from '../constants.js' -import { Node } from './Node.js' +import { NodeBase, NODE_TYPE, SCALAR } from './Node.js' import { toJS, ToJSContext } from './toJS.js' export const isScalarValue = (value: unknown) => @@ -19,7 +19,9 @@ export declare namespace Scalar { | Type.QUOTE_SINGLE } -export class Scalar extends Node { +export class Scalar extends NodeBase { + [NODE_TYPE] = SCALAR + value: T declare type?: Scalar.Type diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index 2c3f5219..aed1a6d8 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -1,17 +1,21 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { Node } from './Node.js' +import type { Node } from './index.js' +import { isPair, isScalar, MAP, NODE_TYPE } from './Node.js' import { Pair } from './Pair.js' -import { Scalar, isScalarValue } from './Scalar.js' +import { isScalarValue } from './Scalar.js' import { ToJSContext } from './toJS.js' -export function findPair(items: Iterable, key: unknown) { - const k = key instanceof Scalar ? key.value : key +export function findPair( + items: Iterable>, + key: unknown +) { + const k = isScalar(key) ? key.value : key for (const it of items) { - if (it instanceof Pair) { + if (isPair(it)) { if (it.key === key || it.key === k) return it - if (it.key instanceof Scalar && it.key.value === k) return it + if (isScalar(it.key) && it.key.value === k) return it } } return undefined @@ -30,6 +34,8 @@ export class YAMLMap extends Collection { return 'tag:yaml.org,2002:map' } + [NODE_TYPE] = MAP + items: Pair[] = [] type?: Type.FLOW_MAP | Type.MAP @@ -42,7 +48,7 @@ export class YAMLMap extends Collection { */ add(pair: Pair | { key: K; value: V }, overwrite?: boolean) { let _pair: Pair - if (pair instanceof Pair) _pair = pair + if (isPair(pair)) _pair = pair else if (!pair || typeof pair !== 'object' || !('key' in pair)) { // In TypeScript, this never happens. _pair = new Pair(pair as any, (pair as any).value) @@ -53,7 +59,7 @@ export class YAMLMap extends Collection { if (prev) { if (!overwrite) throw new Error(`Key ${_pair.key} already set`) // For scalars, keep the old node & its comments and anchors - if (prev.value instanceof Scalar && isScalarValue(_pair.value)) + if (isScalar(prev.value) && isScalarValue(_pair.value)) prev.value.value = _pair.value else prev.value = _pair.value } else if (sortEntries) { @@ -75,7 +81,7 @@ export class YAMLMap extends Collection { get(key: K, keepScalar?: boolean) { const it = findPair(this.items, key) const node = it && it.value - return !keepScalar && node instanceof Scalar ? node.value : node + return !keepScalar && isScalar(node) ? node.value : node } has(key: K) { @@ -105,7 +111,7 @@ export class YAMLMap extends Collection { ) { if (!ctx) return JSON.stringify(this) for (const item of this.items) { - if (!(item instanceof Pair)) + if (!isPair(item)) throw new Error( `Map items must all be pairs; found ${JSON.stringify(item)} instead` ) diff --git a/src/ast/YAMLSeq.ts b/src/ast/YAMLSeq.ts index 785c5450..4b3b1ab2 100644 --- a/src/ast/YAMLSeq.ts +++ b/src/ast/YAMLSeq.ts @@ -1,12 +1,13 @@ import { Type } from '../constants.js' import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { Node } from './Node.js' -import { Scalar, isScalarValue } from './Scalar.js' +import { Node, NODE_TYPE, SEQ, isScalar } from './Node.js' +import type { Pair } from './Pair.js' +import { isScalarValue } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' export declare namespace YAMLSeq { - interface Parsed extends YAMLSeq { + interface Parsed extends YAMLSeq { items: T[] range: [number, number] } @@ -17,6 +18,8 @@ export class YAMLSeq extends Collection { return 'tag:yaml.org,2002:seq' } + [NODE_TYPE] = SEQ + items: T[] = [] type?: Type.FLOW_SEQ | Type.SEQ @@ -25,31 +28,60 @@ export class YAMLSeq extends Collection { this.items.push(value) } - delete(key: number | string | Scalar) { + /** + * Removes a value from the collection. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + * + * @returns `true` if the item was found and removed. + */ + delete(key: unknown) { const idx = asItemIndex(key) if (typeof idx !== 'number') return false const del = this.items.splice(idx, 1) return del.length > 0 } - get(key: number | string | Scalar, keepScalar?: boolean) { + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + */ + get(key: unknown, keepScalar?: boolean) { const idx = asItemIndex(key) if (typeof idx !== 'number') return undefined const it = this.items[idx] - return !keepScalar && it instanceof Scalar ? it.value : it + return !keepScalar && isScalar(it) ? it.value : it } - has(key: number | string | Scalar) { + /** + * Checks if the collection includes a value with the key `key`. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + */ + has(key: unknown) { const idx = asItemIndex(key) return typeof idx === 'number' && idx < this.items.length } - set(key: number | string | Scalar, value: T) { + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + * + * If `key` does not contain a representation of an integer, this will throw. + * It may be wrapped in a `Scalar`. + */ + set(key: unknown, value: T) { const idx = asItemIndex(key) if (typeof idx !== 'number') throw new Error(`Expected a valid index, not ${key}.`) const prev = this.items[idx] - if (prev instanceof Scalar && isScalarValue(value)) prev.value = value + if (isScalar(prev) && isScalarValue(value)) prev.value = value else this.items[idx] = value } @@ -81,7 +113,7 @@ export class YAMLSeq extends Collection { } function asItemIndex(key: unknown): number | null { - let idx = key instanceof Scalar ? key.value : key + let idx = isScalar(key) ? key.value : key if (idx && typeof idx === 'string') idx = Number(idx) return typeof idx === 'number' && Number.isInteger(idx) && idx >= 0 ? idx diff --git a/src/ast/index.ts b/src/ast/index.ts index c51c8810..5be6b9c4 100644 --- a/src/ast/index.ts +++ b/src/ast/index.ts @@ -1,7 +1,7 @@ export { Alias } from './Alias.js' export { Collection, collectionFromPath, isEmptyPath } from './Collection.js' export { Merge } from './Merge.js' -export { Node } from './Node.js' +export { Node, ParsedNode, isNode } from './Node.js' export { Pair, PairType } from './Pair.js' export { Scalar } from './Scalar.js' export { YAMLMap, findPair } from './YAMLMap.js' diff --git a/src/ast/toJS.ts b/src/ast/toJS.ts index f7e3daa6..9d8e2970 100644 --- a/src/ast/toJS.ts +++ b/src/ast/toJS.ts @@ -1,6 +1,6 @@ import type { Document } from '../doc/Document.js' import type { stringify } from '../stringify/stringify.js' -import { Node } from './index.js' +import type { Node } from './Node.js' export interface ToJSAnchorValue { alias: string[] diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index 6d7a8169..ec7f7cbc 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,4 +1,4 @@ -import { Node } from '../ast/Node.js' +import { isNode, ParsedNode } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' import type { YAMLMap } from '../ast/YAMLMap.js' import type { YAMLSeq } from '../ast/YAMLSeq.js' @@ -80,10 +80,9 @@ export function composeCollection( } const res = tag.resolve(coll, msg => onError(coll.range[0], msg)) - const node = - res instanceof Node - ? (res as Node.Parsed) - : (new Scalar(res) as Scalar.Parsed) + const node = isNode(res) + ? (res as ParsedNode) + : (new Scalar(res) as Scalar.Parsed) node.range = coll.range node.tag = tagName if (tag?.format) (node as Scalar).format = tag.format diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 2c45f348..6f1c54e3 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,5 +1,5 @@ import { Alias } from '../ast/Alias.js' -import type { Node } from '../ast/Node.js' +import type { Node, ParsedNode } from '../ast/index.js' import type { Document } from '../doc/Document.js' import type { FlowScalar, Token } from '../parse/tokens.js' import { composeCollection } from './compose-collection.js' @@ -24,7 +24,7 @@ export function composeNode( onError: (offset: number, message: string, warning?: boolean) => void ) { const { spaceBefore, comment, anchor, tagName } = props - let node: Node.Parsed + let node: ParsedNode switch (token.type) { case 'alias': node = composeAlias(doc, token, onError) diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index d6ae9e1d..7a1c78fc 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -1,3 +1,4 @@ +import { isScalar } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' import type { Schema } from '../doc/Schema.js' @@ -23,13 +24,13 @@ export function composeScalar( ? findScalarTagByName(doc.schema, value, tagName, onError) : findScalarTagByTest(doc.schema, value, token.type === 'scalar') - let scalar: Scalar.Parsed + let scalar: Scalar try { const res = tag ? tag.resolve(value, msg => onError(offset, msg)) : value - scalar = (res instanceof Scalar ? res : new Scalar(res)) as Scalar.Parsed + scalar = isScalar(res) ? res : new Scalar(res) } catch (error) { onError(offset, error.message) - scalar = new Scalar(value) as Scalar.Parsed + scalar = new Scalar(value) } scalar.range = [offset, offset + length] scalar.source = value @@ -39,7 +40,7 @@ export function composeScalar( if (comment) scalar.comment = comment if (anchor) doc.anchors.setAnchor(scalar, anchor) - return scalar + return scalar as Scalar.Parsed } const defaultScalarTag = (schema: Schema) => diff --git a/src/compose/composer.ts b/src/compose/composer.ts index d5c2cd08..1e60e6e1 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -1,5 +1,4 @@ -import { Collection } from '../ast/Collection.js' -import type { Node } from '../ast/Node.js' +import { isCollection } from '../ast/Node.js' import { Directives } from '../doc/directives.js' import { Document } from '../doc/Document.js' import { YAMLParseError, YAMLWarning } from '../errors.js' @@ -80,11 +79,11 @@ export class Composer { } else if (afterEmptyLine || doc.directivesEndMarker || !dc) { doc.commentBefore = comment } else if ( - dc instanceof Collection && + isCollection(dc) && (dc.type === 'MAP' || dc.type === 'SEQ') && dc.items.length > 0 ) { - const it = dc.items[0] as Node + const it = dc.items[0] const cb = it.commentBefore it.commentBefore = cb ? `${comment}\n${cb}` : comment } else { diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index e70b8701..076bf8ff 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -1,4 +1,4 @@ -import { Node } from '../ast/Node.js' +import { isNode, isPair, ParsedNode } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' import { YAMLMap } from '../ast/YAMLMap.js' import { YAMLSeq } from '../ast/YAMLSeq.js' @@ -22,8 +22,8 @@ export function resolveFlowCollection( coll.type = isMap ? Type.FLOW_MAP : Type.FLOW_SEQ if (_anchor) doc.anchors.setAnchor(coll, _anchor) - let key: Node.Parsed | null = null - let value: Node.Parsed | null = null + let key: ParsedNode | null = null + let value: ParsedNode | null = null let spaceBefore = false let comment = '' @@ -98,9 +98,9 @@ export function resolveFlowCollection( if (atValueEnd) { if (hasComment) { let node = coll.items[coll.items.length - 1] - if (node instanceof Pair) node = node.value || node.key + if (isPair(node)) node = node.value || node.key /* istanbul ignore else should not happen */ - if (node instanceof Node) node.comment = comment + if (isNode(node)) node.comment = comment else onError(offset, 'Error adding trailing comment to node') comment = '' hasComment = false diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index e0c9e672..239e8859 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -1,27 +1,27 @@ import { Alias } from '../ast/Alias.js' +import type { ParsedNode } from '../ast/index.js' import { Merge } from '../ast/Merge.js' -import type { Node } from '../ast/Node.js' +import { isAlias, isMap, isScalar } from '../ast/Node.js' import type { Pair } from '../ast/Pair.js' -import { Scalar } from '../ast/Scalar.js' -import { YAMLMap } from '../ast/YAMLMap.js' +import type { Scalar } from '../ast/Scalar.js' export function resolveMergePair( pair: Pair, onError: (offset: number, message: string) => void ) { - if (!(pair.key instanceof Scalar) || pair.key.value !== Merge.KEY) return pair + if (!isScalar(pair.key) || pair.key.value !== Merge.KEY) return pair const merge = new Merge(pair as Pair) for (const node of merge.value.items as Alias.Parsed[]) { - if (node instanceof Alias) { - if (node.source instanceof YAMLMap) { + if (isAlias(node)) { + if (isMap(node.source)) { // ok } else { onError(node.range[0], 'Merge nodes aliases can only point to maps') } } else { onError( - (node as Node.Parsed).range[0], + (node as ParsedNode).range[0], 'Merge nodes can only have Alias nodes as values' ) } diff --git a/src/doc/Anchors.ts b/src/doc/Anchors.ts index 6c0f3a9d..654c9308 100644 --- a/src/doc/Anchors.ts +++ b/src/doc/Anchors.ts @@ -1,4 +1,6 @@ -import { Alias, Merge, Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' +import { Alias } from '../ast/Alias.js' +import { Merge } from '../ast/Merge.js' +import { isAlias, isCollection, isMap, isScalar, Node } from '../ast/Node.js' export class Anchors { map: Record = Object.create(null) @@ -24,9 +26,9 @@ export class Anchors { createMergePair(...sources: Node[]) { const merge = new Merge() merge.value.items = sources.map(s => { - if (s instanceof Alias) { - if (s.source instanceof YAMLMap) return s - } else if (s instanceof YAMLMap) { + if (isAlias(s)) { + if (isMap(s.source)) return s + } else if (isMap(s)) { return this.createAlias(s) } throw new Error('Merge sources must be Map nodes or their Aliases') @@ -74,11 +76,7 @@ export class Anchors { return name } - const valid = - node instanceof Scalar || - node instanceof YAMLSeq || - node instanceof YAMLMap - if (!valid) + if (!isScalar(node) && !isCollection(node)) throw new Error('Anchors may only be set for Scalar, Seq and Map nodes') if (name) { if (/[\x00-\x19\s,[\]{}]/.test(name)) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 86ef0ac6..2b7c62fc 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,16 +1,23 @@ +import { Alias } from '../ast/Alias.js' import { - Alias, - Collection, - Node, - Pair, - Scalar, - YAMLMap, - YAMLSeq, collectionFromPath, isEmptyPath, toJS, ToJSContext } from '../ast/index.js' +import { ToJSAnchorValue } from '../ast/toJS.js' +import { + DOC, + isCollection, + isNode, + isScalar, + Node, + NODE_TYPE, + ParsedNode +} from '../ast/Node.js' +import { Pair } from '../ast/Pair.js' +import type { YAMLMap } from '../ast/YAMLMap.js' +import type { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { YAMLError, YAMLWarning } from '../errors.js' import { @@ -28,7 +35,6 @@ import { Schema, SchemaName, SchemaOptions } from './Schema.js' import { Reviver, applyReviver } from './applyReviver.js' import { createNode, CreateNodeContext } from './createNode.js' import { Directives } from './directives.js' -import { ToJSAnchorValue } from '../ast/toJS.js' export type Replacer = any[] | ((key: any, value: any) => unknown) export type { Anchors, Reviver } @@ -62,7 +68,7 @@ export interface ToJSOptions { export declare namespace Document { interface Parsed extends Document { - contents: Node.Parsed | null + contents: ParsedNode | null range: [number, number] /** The schema used with the document. */ schema: Schema @@ -75,7 +81,9 @@ export declare namespace Document { } export class Document { - static defaults = documentOptions + static defaults = documentOptions; + + [NODE_TYPE] = DOC /** * Anchors associated with the document's nodes; @@ -265,7 +273,7 @@ export class Document { * `true` (collections are always returned intact). */ get(key: unknown, keepScalar?: boolean) { - return this.contents instanceof Collection + return isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined } @@ -277,10 +285,10 @@ export class Document { */ getIn(path: Iterable, keepScalar?: boolean) { if (isEmptyPath(path)) - return !keepScalar && this.contents instanceof Scalar + return !keepScalar && isScalar(this.contents) ? this.contents.value : this.contents - return this.contents instanceof Collection + return isCollection(this.contents) ? this.contents.getIn(path, keepScalar) : undefined } @@ -289,7 +297,7 @@ export class Document { * Checks if the document includes a value with the key `key`. */ has(key: unknown) { - return this.contents instanceof Collection ? this.contents.has(key) : false + return isCollection(this.contents) ? this.contents.has(key) : false } /** @@ -297,9 +305,7 @@ export class Document { */ hasIn(path: Iterable) { if (isEmptyPath(path)) return this.contents !== undefined - return this.contents instanceof Collection - ? this.contents.hasIn(path) - : false + return isCollection(this.contents) ? this.contents.hasIn(path) : false } /** @@ -444,7 +450,7 @@ export class Document { let chompKeep = false let contentComment = null if (this.contents) { - if (this.contents instanceof Node) { + if (isNode(this.contents)) { if ( this.contents.spaceBefore && (hasDirectives || this.directivesEndMarker) @@ -485,6 +491,6 @@ export class Document { } function assertCollection(contents: unknown): contents is YAMLMap | YAMLSeq { - if (contents instanceof Collection) return true + if (isCollection(contents)) return true throw new Error('Expected a YAML collection as document contents') } diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index 2f5b3bea..b2f8e10d 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -1,6 +1,7 @@ import type { Alias } from '../ast/Alias.js' -import { Node } from '../ast/Node.js' +import { isNode, isPair, Node } from '../ast/Node.js' import { Scalar } from '../ast/Scalar.js' +import type { YAMLMap } from '../ast/YAMLMap.js' import { defaultTagPrefix } from '../constants.js' import type { TagObj } from '../tags/types.js' import type { Replacer } from './Document.js' @@ -39,7 +40,12 @@ export function createNode( tagName: string | undefined, ctx: CreateNodeContext ): Node { - if (value instanceof Node) return value + if (isNode(value)) return value as Node + if (isPair(value)) { + const map = ctx.schema.map.createNode?.(ctx.schema, null, ctx) as YAMLMap + map.items.push(value) + return map + } const { onAlias, onTagObj, prevObjects } = ctx const { map, seq, tags } = ctx.schema if (tagName && tagName.startsWith('!!')) diff --git a/src/doc/directives.ts b/src/doc/directives.ts index 475582c4..fd12abd7 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -1,4 +1,4 @@ -import { Node } from '../ast/Node.js' +import { isNode } from '../ast/Node.js' import { visit } from '../visit.js' import type { Document } from './Document.js' @@ -157,10 +157,10 @@ export class Directives { const tagEntries = Object.entries(this.tags) let tagNames: string[] - if (doc && tagEntries.length > 0 && doc.contents instanceof Node) { + if (doc && tagEntries.length > 0 && isNode(doc.contents)) { const tags: Record = {} visit(doc.contents, (_key, node) => { - if (node instanceof Node && node.tag) tags[node.tag] = true + if (isNode(node) && node.tag) tags[node.tag] = true }) tagNames = Object.keys(tags) } else tagNames = [] diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index d7fa39f8..660adfba 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,7 +1,5 @@ -import { Alias } from '../ast/Alias.js' -import { Node } from '../ast/Node.js' -import { Pair } from '../ast/Pair.js' -import { Scalar } from '../ast/Scalar.js' +import { isAlias, isNode, isPair, isScalar, Node } from '../ast/Node.js' +import type { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' @@ -28,7 +26,7 @@ function getTagObject(tags: TagObj[], item: Node) { let tagObj: TagObj | undefined = undefined let obj: unknown - if (item instanceof Scalar) { + if (isScalar(item)) { obj = item.value const match = tags.filter(t => t.identify && t.identify(obj)) tagObj = @@ -72,14 +70,13 @@ export function stringify( onComment?: () => void, onChompKeep?: () => void ): string { - if (item instanceof Pair) return item.toString(ctx, onComment, onChompKeep) - if (item instanceof Alias) return item.toString(ctx) + if (isPair(item)) return item.toString(ctx, onComment, onChompKeep) + if (isAlias(item)) return item.toString(ctx) let tagObj: TagObj | undefined = undefined - const node: Node = - item instanceof Node - ? item - : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) + const node = isNode(item) + ? item + : ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) }) if (!tagObj) tagObj = getTagObject(ctx.doc.schema.tags, node) @@ -90,11 +87,11 @@ export function stringify( const str = typeof tagObj.stringify === 'function' ? tagObj.stringify(node as Scalar, ctx, onComment, onChompKeep) - : node instanceof Scalar + : isScalar(node) ? stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep) if (!props) return str - return node instanceof Scalar || str[0] === '{' || str[0] === '[' + return isScalar(node) || str[0] === '{' || str[0] === '[' ? `${props} ${str}` : `${props}\n${ctx.indent}${str}` } diff --git a/src/tags/failsafe/map.ts b/src/tags/failsafe/map.ts index 81d3e1e9..c2efde00 100644 --- a/src/tags/failsafe/map.ts +++ b/src/tags/failsafe/map.ts @@ -1,3 +1,4 @@ +import { isMap } from '../../ast/Node.js' import { createPair } from '../../ast/Pair.js' import { YAMLMap } from '../../ast/YAMLMap.js' import type { CreateNodeContext } from '../../doc/createNode.js' @@ -31,7 +32,7 @@ export const map: CollectionTag = { nodeClass: YAMLMap, tag: 'tag:yaml.org,2002:map', resolve(map, onError) { - if (!(map instanceof YAMLMap)) onError('Expected a mapping for this tag') + if (!isMap(map)) onError('Expected a mapping for this tag') return map } } diff --git a/src/tags/failsafe/seq.ts b/src/tags/failsafe/seq.ts index 15973616..f73fea56 100644 --- a/src/tags/failsafe/seq.ts +++ b/src/tags/failsafe/seq.ts @@ -1,3 +1,4 @@ +import { isSeq } from '../../ast/Node.js' import { YAMLSeq } from '../../ast/YAMLSeq.js' import { CreateNodeContext, createNode } from '../../doc/createNode.js' import type { Schema } from '../../doc/Schema.js' @@ -26,7 +27,7 @@ export const seq: CollectionTag = { nodeClass: YAMLSeq, tag: 'tag:yaml.org,2002:seq', resolve(seq, onError) { - if (!(seq instanceof YAMLSeq)) onError('Expected a sequence for this tag') + if (!isSeq(seq)) onError('Expected a sequence for this tag') return seq } } diff --git a/src/tags/yaml-1.1/omap.ts b/src/tags/yaml-1.1/omap.ts index 2743da48..ec962518 100644 --- a/src/tags/yaml-1.1/omap.ts +++ b/src/tags/yaml-1.1/omap.ts @@ -1,8 +1,7 @@ -import { Pair } from '../../ast/Pair.js' -import { Scalar } from '../../ast/Scalar.js' -import { YAMLMap } from '../../ast/YAMLMap.js' import { YAMLSeq } from '../../ast/YAMLSeq.js' import { toJS, ToJSContext } from '../../ast/toJS.js' +import { isPair, isScalar } from '../../ast/Node.js' +import { YAMLMap } from '../../ast/YAMLMap.js' import { createPairs, resolvePairs } from './pairs.js' import { CollectionTag } from '../types.js' @@ -30,7 +29,7 @@ export class YAMLOMap extends YAMLSeq { if (ctx && ctx.onCreate) ctx.onCreate(map) for (const pair of this.items) { let key, value - if (pair instanceof Pair) { + if (isPair(pair)) { key = toJS(pair.key, '', ctx) value = toJS(pair.value, key, ctx) } else { @@ -55,7 +54,7 @@ export const omap: CollectionTag = { const pairs = resolvePairs(seq, onError) const seenKeys: unknown[] = [] for (const { key } of pairs.items) { - if (key instanceof Scalar) { + if (isScalar(key)) { if (seenKeys.includes(key.value)) { onError(`Ordered maps must not include duplicate keys: ${key.value}`) } else { diff --git a/src/tags/yaml-1.1/pairs.ts b/src/tags/yaml-1.1/pairs.ts index f6613900..eea8ac91 100644 --- a/src/tags/yaml-1.1/pairs.ts +++ b/src/tags/yaml-1.1/pairs.ts @@ -1,3 +1,4 @@ +import { isMap, isPair, isSeq } from '../../ast/Node.js' import { createPair, Pair } from '../../ast/Pair.js' import { YAMLMap } from '../../ast/YAMLMap.js' import { YAMLSeq } from '../../ast/YAMLSeq.js' @@ -9,11 +10,11 @@ export function resolvePairs( seq: YAMLSeq | YAMLMap, onError: (message: string) => void ) { - if (seq instanceof YAMLSeq) { + if (isSeq(seq)) { for (let i = 0; i < seq.items.length; ++i) { let item = seq.items[i] - if (item instanceof Pair) continue - else if (item instanceof YAMLMap) { + if (isPair(item)) continue + else if (isMap(item)) { if (item.items.length > 1) onError('Each pair must have its own sequence indicator') const pair = item.items[0] || new Pair(null) @@ -27,7 +28,7 @@ export function resolvePairs( : item.comment item = pair } - seq.items[i] = item instanceof Pair ? item : new Pair(item) + seq.items[i] = isPair(item) ? item : new Pair(item) } } else onError('Expected a sequence for this tag') return seq as YAMLSeq diff --git a/src/tags/yaml-1.1/set.ts b/src/tags/yaml-1.1/set.ts index fa72884b..c1671194 100644 --- a/src/tags/yaml-1.1/set.ts +++ b/src/tags/yaml-1.1/set.ts @@ -1,3 +1,4 @@ +import { isMap, isPair, isScalar } from '../../ast/Node.js' import { createPair, Pair } from '../../ast/Pair.js' import { Scalar } from '../../ast/Scalar.js' import { ToJSContext } from '../../ast/toJS.js' @@ -21,7 +22,7 @@ export class YAMLSet extends YAMLMap | null> { | { key: T; value: Scalar | null } ) { let pair: Pair | null> - if (key instanceof Pair) pair = key + if (isPair(key)) pair = key else if ( typeof key === 'object' && 'key' in key && @@ -36,8 +37,8 @@ export class YAMLSet extends YAMLMap | null> { get(key?: T, keepPair?: boolean) { const pair = findPair(this.items, key) - return !keepPair && pair instanceof Pair - ? pair.key instanceof Scalar + return !keepPair && isPair(pair) + ? isScalar(pair.key) ? pair.key.value : pair.key : pair @@ -88,7 +89,7 @@ export const set: CollectionTag = { tag: 'tag:yaml.org,2002:set', resolve(map, onError) { - if (map instanceof YAMLMap) { + if (isMap(map)) { if (map.hasAllNullValues(true)) return Object.assign(new YAMLSet(), map) else onError('Set items must all have null values') } else onError('Expected a mapping for this tag') diff --git a/src/types.ts b/src/types.ts index 8163018d..343cba40 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,9 +4,10 @@ export { Alias, Collection, Merge, - Node, Pair, + ParsedNode, Scalar, YAMLMap, - YAMLSeq + YAMLSeq, + isNode } from './ast/index.js' diff --git a/src/visit.ts b/src/visit.ts index 3870366c..0fe7d879 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,5 +1,16 @@ -import { Alias, Node, Pair, Scalar, YAMLMap, YAMLSeq } from './ast/index.js' -import { Document } from './doc/Document.js' +import type { Alias, Pair, Scalar, YAMLMap, YAMLSeq } from './ast/index.js' +import { + isAlias, + isCollection, + isDocument, + isMap, + isNode, + isPair, + isScalar, + isSeq, + Node +} from './ast/Node.js' +import type { Document } from './doc/Document.js' const BREAK = Symbol('break visit') const SKIP = Symbol('skip children') @@ -8,8 +19,8 @@ const REMOVE = Symbol('remove node') export type visitorFn = ( key: number | 'key' | 'value' | null, node: T, - path: readonly Node[] -) => void | symbol | number | Node + path: readonly (Document | Node | Pair)[] +) => void | symbol | number | Node | Pair export type visitor = | visitorFn @@ -60,7 +71,7 @@ export function visit( Seq?: visitorFn } ) { - if (node instanceof Document) { + if (isDocument(node)) { const cd = _visit(null, node.contents, visitor, Object.freeze([node])) if (cd === REMOVE) node.contents = null } else _visit(null, node, visitor, Object.freeze([])) @@ -83,30 +94,30 @@ function _visit( key: number | 'key' | 'value' | null, node: unknown, visitor: visitor, - path: readonly Node[] -): void | symbol | number | Node { - let ctrl = undefined + path: readonly (Document | Node | Pair)[] +): number | symbol | void { + let ctrl: void | symbol | number | Node | Pair = undefined if (typeof visitor === 'function') ctrl = visitor(key, node, path) - else if (node instanceof YAMLMap) { + else if (isMap(node)) { if (visitor.Map) ctrl = visitor.Map(key, node, path) - } else if (node instanceof YAMLSeq) { + } else if (isSeq(node)) { if (visitor.Seq) ctrl = visitor.Seq(key, node, path) - } else if (node instanceof Pair) { + } else if (isPair(node)) { if (visitor.Pair) ctrl = visitor.Pair(key, node, path) - } else if (node instanceof Scalar) { + } else if (isScalar(node)) { if (visitor.Scalar) ctrl = visitor.Scalar(key, node, path) - } else if (node instanceof Alias) { + } else if (isAlias(node)) { if (visitor.Alias) ctrl = visitor.Alias(key, node, path) } - if (ctrl instanceof Node) { + if (isNode(ctrl) || isPair(ctrl)) { const parent = path[path.length - 1] - if (parent instanceof YAMLMap || parent instanceof YAMLSeq) { + if (isCollection(parent)) { parent.items[key as number] = ctrl - } else if (parent instanceof Pair) { + } else if (isPair(parent)) { if (key === 'key') parent.key = ctrl else parent.value = ctrl - } else if (parent instanceof Document) { + } else if (isDocument(parent)) { parent.contents = ctrl } else { const pt = parent && parent.type @@ -116,7 +127,7 @@ function _visit( } if (typeof ctrl !== 'symbol') { - if (node instanceof YAMLMap || node instanceof YAMLSeq) { + if (isCollection(node)) { path = Object.freeze(path.concat(node)) for (let i = 0; i < node.items.length; ++i) { const ci = _visit(i, node.items[i], visitor, path) @@ -127,7 +138,7 @@ function _visit( i -= 1 } } - } else if (node instanceof Pair) { + } else if (isPair(node)) { path = Object.freeze(path.concat(node)) const ck = _visit('key', node.key, visitor, path) if (ck === BREAK) return BREAK diff --git a/types.js b/types.js index 88747498..9cc17489 100644 --- a/types.js +++ b/types.js @@ -10,7 +10,6 @@ exports.Schema = types.Schema exports.Alias = types.Alias exports.Collection = types.Collection exports.Merge = types.Merge -exports.Node = types.Node exports.Pair = types.Pair exports.Scalar = types.Scalar exports.YAMLMap = types.YAMLMap diff --git a/types.mjs b/types.mjs index 71322884..292621e2 100644 --- a/types.mjs +++ b/types.mjs @@ -10,7 +10,6 @@ export const Schema = types.Schema export const Alias = types.Alias export const Collection = types.Collection export const Merge = types.Merge -export const Node = types.Node export const Pair = types.Pair export const Scalar = types.Scalar export const YAMLMap = types.YAMLMap From bf452358c557fe9261ca37a6ba938ed95c544de1 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 12:08:14 +0200 Subject: [PATCH 18/24] Drop src/ast/index.ts re-exporter --- src/ast/Merge.ts | 2 +- src/ast/YAMLMap.ts | 3 +-- src/ast/index.ts | 9 --------- src/compose/compose-node.ts | 2 +- src/compose/resolve-merge-pair.ts | 3 +-- src/doc/Document.ts | 10 ++-------- src/stringify/stringifyString.ts | 2 +- src/tags/types.ts | 5 ++++- src/tags/yaml-1.1/binary.ts | 2 +- src/tags/yaml-1.1/timestamp.ts | 2 +- src/types.ts | 17 ++++++----------- src/util.ts | 3 ++- src/visit.ts | 6 +++++- tests/doc/createNode.js | 11 ++++------- tests/doc/types.js | 3 ++- 15 files changed, 32 insertions(+), 48 deletions(-) delete mode 100644 src/ast/index.ts diff --git a/src/ast/Merge.ts b/src/ast/Merge.ts index 0d844675..eeda08e7 100644 --- a/src/ast/Merge.ts +++ b/src/ast/Merge.ts @@ -1,5 +1,5 @@ import { StringifyContext } from '../stringify/stringify.js' -import type { Alias } from './index.js' +import type { Alias } from './Alias.js' import { isMap, isNode, isPair, isSeq } from './Node.js' import { Pair, PairType } from './Pair.js' import { Scalar } from './Scalar.js' diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index aed1a6d8..740a22d6 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -1,8 +1,7 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import type { Node } from './index.js' -import { isPair, isScalar, MAP, NODE_TYPE } from './Node.js' +import { isPair, isScalar, MAP, Node, NODE_TYPE } from './Node.js' import { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import { ToJSContext } from './toJS.js' diff --git a/src/ast/index.ts b/src/ast/index.ts deleted file mode 100644 index 5be6b9c4..00000000 --- a/src/ast/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { Alias } from './Alias.js' -export { Collection, collectionFromPath, isEmptyPath } from './Collection.js' -export { Merge } from './Merge.js' -export { Node, ParsedNode, isNode } from './Node.js' -export { Pair, PairType } from './Pair.js' -export { Scalar } from './Scalar.js' -export { YAMLMap, findPair } from './YAMLMap.js' -export { YAMLSeq } from './YAMLSeq.js' -export { ToJSContext, toJS } from './toJS.js' diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 6f1c54e3..84cfa841 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,5 +1,5 @@ import { Alias } from '../ast/Alias.js' -import type { Node, ParsedNode } from '../ast/index.js' +import type { Node, ParsedNode } from '../ast/Node.js' import type { Document } from '../doc/Document.js' import type { FlowScalar, Token } from '../parse/tokens.js' import { composeCollection } from './compose-collection.js' diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index 239e8859..e9a9a29d 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -1,7 +1,6 @@ import { Alias } from '../ast/Alias.js' -import type { ParsedNode } from '../ast/index.js' import { Merge } from '../ast/Merge.js' -import { isAlias, isMap, isScalar } from '../ast/Node.js' +import { isAlias, isMap, isScalar, ParsedNode } from '../ast/Node.js' import type { Pair } from '../ast/Pair.js' import type { Scalar } from '../ast/Scalar.js' diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 2b7c62fc..2ab8654e 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,11 +1,5 @@ import { Alias } from '../ast/Alias.js' -import { - collectionFromPath, - isEmptyPath, - toJS, - ToJSContext -} from '../ast/index.js' -import { ToJSAnchorValue } from '../ast/toJS.js' +import { collectionFromPath, isEmptyPath } from '../ast/Collection.js' import { DOC, isCollection, @@ -16,6 +10,7 @@ import { ParsedNode } from '../ast/Node.js' import { Pair } from '../ast/Pair.js' +import { toJS, ToJSAnchorValue, ToJSContext } from '../ast/toJS.js' import type { YAMLMap } from '../ast/YAMLMap.js' import type { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' @@ -29,7 +24,6 @@ import { import { addComment } from '../stringify/addComment.js' import { stringify, StringifyContext } from '../stringify/stringify.js' import type { TagId, TagObj } from '../tags/types.js' - import { Anchors } from './Anchors.js' import { Schema, SchemaName, SchemaOptions } from './Schema.js' import { Reviver, applyReviver } from './applyReviver.js' diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index 25239f2e..6a993cf9 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -1,4 +1,4 @@ -import type { Scalar } from '../ast/index.js' +import type { Scalar } from '../ast/Scalar.js' import { Type } from '../constants.js' import { strOptions } from '../tags/options.js' import { addCommentBefore } from './addComment.js' diff --git a/src/tags/types.ts b/src/tags/types.ts index 69fe6979..017e4052 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -1,4 +1,7 @@ -import type { Node, Scalar, YAMLMap, YAMLSeq } from '../ast/index.js' +import type { Node } from '../ast/Node.js' +import type { Scalar } from '../ast/Scalar.js' +import type { YAMLMap } from '../ast/YAMLMap.js' +import type { YAMLSeq } from '../ast/YAMLSeq.js' import type { CreateNodeContext } from '../doc/createNode.js' import type { Schema } from '../doc/Schema.js' import type { StringifyContext } from '../stringify/stringify.js' diff --git a/src/tags/yaml-1.1/binary.ts b/src/tags/yaml-1.1/binary.ts index 016bda40..984cdc1c 100644 --- a/src/tags/yaml-1.1/binary.ts +++ b/src/tags/yaml-1.1/binary.ts @@ -1,6 +1,6 @@ /* global atob, btoa, Buffer */ -import type { Scalar } from '../../ast/index.js' +import type { Scalar } from '../../ast/Scalar.js' import { Type } from '../../constants.js' import { stringifyString } from '../../stringify/stringifyString.js' import { binaryOptions as options } from '../options.js' diff --git a/src/tags/yaml-1.1/timestamp.ts b/src/tags/yaml-1.1/timestamp.ts index d1181c12..ceda23b7 100644 --- a/src/tags/yaml-1.1/timestamp.ts +++ b/src/tags/yaml-1.1/timestamp.ts @@ -1,6 +1,6 @@ /* global BigInt */ -import type { Scalar } from '../../ast/index.js' +import type { Scalar } from '../../ast/Scalar.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { intOptions } from '../options.js' import type { ScalarTag } from '../types.js' diff --git a/src/types.ts b/src/types.ts index 343cba40..ecf2727f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,8 @@ export * from './tags/options.js' export { Schema } from './doc/Schema.js' -export { - Alias, - Collection, - Merge, - Pair, - ParsedNode, - Scalar, - YAMLMap, - YAMLSeq, - isNode -} from './ast/index.js' +export { Alias } from './ast/Alias.js' +export { Merge } from './ast/Merge.js' +export { Pair } from './ast/Pair.js' +export { Scalar } from './ast/Scalar.js' +export { YAMLMap } from './ast/YAMLMap.js' +export { YAMLSeq } from './ast/YAMLSeq.js' diff --git a/src/util.ts b/src/util.ts index 71b95a53..2e752883 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ -export { findPair, toJS } from './ast/index.js' +export { findPair } from './ast/YAMLMap.js' +export { toJS, ToJSContext } from './ast/toJS.js' export { stringifyNumber } from './stringify/stringifyNumber.js' export { stringifyString } from './stringify/stringifyString.js' export { Type } from './constants.js' diff --git a/src/visit.ts b/src/visit.ts index 0fe7d879..7cea7790 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,4 +1,4 @@ -import type { Alias, Pair, Scalar, YAMLMap, YAMLSeq } from './ast/index.js' +import type { Alias } from './ast/Alias.js' import { isAlias, isCollection, @@ -10,6 +10,10 @@ import { isSeq, Node } from './ast/Node.js' +import type { Pair } from './ast/Pair.js' +import type { Scalar } from './ast/Scalar.js' +import type { YAMLMap } from './ast/YAMLMap.js' +import type { YAMLSeq } from './ast/YAMLSeq.js' import type { Document } from './doc/Document.js' const BREAK = Symbol('break visit') diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js index 903a8f50..5417aa0d 100644 --- a/tests/doc/createNode.js +++ b/tests/doc/createNode.js @@ -1,11 +1,8 @@ import * as YAML from '../../src/index.js' -import { - Pair, - PairType, - Scalar, - YAMLMap, - YAMLSeq -} from '../../src/ast/index.js' +import { PairType } from '../../src/ast/Pair.js' +import { Scalar } from '../../src/ast/Scalar.js' +import { YAMLMap } from '../../src/ast/YAMLMap.js' +import { YAMLSeq } from '../../src/ast/YAMLSeq.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' let doc diff --git a/tests/doc/types.js b/tests/doc/types.js index b149033a..d514b131 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -1,5 +1,6 @@ import * as YAML from '../../src/index.js' -import { Scalar, YAMLSeq } from '../../src/ast/index.js' +import { Scalar } from '../../src/ast/Scalar.js' +import { YAMLSeq } from '../../src/ast/YAMLSeq.js' import { binary } from '../../src/tags/yaml-1.1/binary.js' import { YAMLOMap } from '../../src/tags/yaml-1.1/omap.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' From 7c4e0fee374011d58d80dfb6ccdb671db66c32a5 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 12:09:37 +0200 Subject: [PATCH 19/24] Drop tests/typings.ts as obsolete --- jest.config.js | 2 +- package.json | 2 +- tests/typings.ts | 118 ----------------------------------------------- 3 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 tests/typings.ts diff --git a/jest.config.js b/jest.config.js index 5f6249c7..27f644ab 100644 --- a/jest.config.js +++ b/jest.config.js @@ -33,7 +33,7 @@ module.exports = { moduleNameMapper, resolver: 'jest-ts-webcompat-resolver', testEnvironment: 'node', - testMatch: ['**/tests/**/*.{js,ts}', '!**/tests/typings.ts'], + testMatch: ['**/tests/**/*.{js,ts}'], testPathIgnorePatterns, transform: { '/(src|tests)/.*\\.(js|ts)$': 'babel-jest' } } diff --git a/package.json b/package.json index 5a661092..c800d4ec 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "test": "jest", "test:browsers": "cd playground && npm test", "test:dist": "npm run build:node && jest", - "test:types": "tsc --moduleResolution node --target ES2017 --noEmit tests/typings.ts", + "test:types": "tsc --noEmit", "docs:install": "cd docs-slate && bundle install", "docs:deploy": "cd docs-slate && ./deploy.sh", "docs": "cd docs-slate && bundle exec middleman server", diff --git a/tests/typings.ts b/tests/typings.ts deleted file mode 100644 index 5e3d03d7..00000000 --- a/tests/typings.ts +++ /dev/null @@ -1,118 +0,0 @@ -// To test types, compile this file with tsc - -import { Document, parse, parseDocument, stringify, visit } from '../src/index' -import { YAMLMap, YAMLSeq, Pair, Scalar } from '../src/types' - -parse('3.14159') -// 3.14159 - -parse('[ true, false, maybe, null ]\n', { version: '1.2' }) -// [ true, false, 'maybe', null ] - -const file = `# file.yml -YAML: - - A human-readable data serialization language - - https://en.wikipedia.org/wiki/YAML -yaml: - - A complete JavaScript implementation - - https://www.npmjs.com/package/yaml` -parse(file, (key, value) => value) -// { YAML: -// [ 'A human-readable data serialization language', -// 'https://en.wikipedia.org/wiki/YAML' ], -// yaml: -// [ 'A complete JavaScript implementation', -// 'https://www.npmjs.com/package/yaml' ] } - -stringify(3.14159) -// '3.14159\n' - -stringify([true, false, 'maybe', null], { version: '1.2' }) -// `- true -// - false -// - maybe -// - null -// ` - -stringify( - { number: 3, plain: 'string', block: 'two\nlines\n' }, - (key, value) => value -) -// `number: 3 -// plain: string -// block: > -// two -// -// lines -// ` - -const src = '[{ a: A }, { b: B }]' -const doc = parseDocument(src) -const seq = doc.contents as YAMLSeq -const { anchors } = doc -const [a, b] = seq.items as YAMLMap.Parsed[] -anchors.setAnchor(a.items[0].value) // 'a1' -anchors.setAnchor(b.items[0].value) // 'a2' -anchors.setAnchor(null, 'a1') // 'a1' -anchors.getName(a) // undefined -anchors.getNode('a2') -// { value: 'B', range: [ 16, 18 ], type: 'PLAIN' } -String(doc) -// [ { a: A }, { b: &a2 B } ] - -const alias = anchors.createAlias(a, 'AA') -seq.items.push(alias) -const refs = new Map() -doc.toJS({ onAnchor: (value, count) => refs.set(value, count) }) -// [ { a: 'A' }, { b: 'B' }, { a: 'A' } ] -String(doc) -// [ &AA { a: A }, { b: &a2 B }, *AA ] -refs -// Map(3) { undefined => 1, 'B' => 1, { a: 'A' } => 2 } - -const merge = anchors.createMergePair(alias) -b.items.push(merge) -doc.toJS() -// [ { a: 'A' }, { b: 'B', a: 'A' }, { a: 'A' } ] -String(doc) -// [ &AA { a: A }, { b: &a2 B, <<: *AA }, *AA ] - -// This creates a circular reference -merge.value.items.push(anchors.createAlias(b)) -doc.toJS() // [RangeError: Maximum call stack size exceeded] -String(doc) -// [ -// &AA { a: A }, -// &a3 { -// b: &a2 B, -// <<: -// [ *AA, *a3 ] -// }, -// *AA -// ] - -const mod: Document = doc -const map = new YAMLMap() -map.items.push(new Pair('foo', 'bar')) -mod.contents = map - -const doc2 = new Document({ bizz: 'fuzz' }) -doc2.add(doc2.createPair('baz', 42)) - -visit(doc, (key, node, path) => console.log(key, node, path)) -visit(doc, { - Scalar(key, node) { - if (key === 3) return 5 - if (typeof node.value === 'number') return doc.createNode(node.value + 1) - }, - Map(_, map) { - if (map.items.length > 3) return visit.SKIP - }, - Pair(_, pair) { - if (pair.key instanceof Scalar && pair.key.value === 'foo') - return visit.REMOVE - }, - Seq(_, seq) { - if (seq.items.length > 3) return visit.BREAK - } -}) From 0a9f34f27ab8aad5dd8761bb93098128bd6180c2 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 12:23:09 +0200 Subject: [PATCH 20/24] Refactor test-events as TypeScript --- rollup.node-config.js | 2 +- src/ast/YAMLMap.ts | 8 +++-- src/ast/YAMLSeq.ts | 6 ++-- src/{test-events.js => test-events.ts} | 45 ++++++++++++++++---------- 4 files changed, 38 insertions(+), 23 deletions(-) rename src/{test-events.js => test-events.ts} (70%) diff --git a/rollup.node-config.js b/rollup.node-config.js index f71139ee..21e6b853 100644 --- a/rollup.node-config.js +++ b/rollup.node-config.js @@ -5,7 +5,7 @@ import typescript from '@rollup/plugin-typescript' export default { input: { index: 'src/index.ts', - 'test-events': 'src/test-events.js', + 'test-events': 'src/test-events.ts', types: 'src/types.ts', util: 'src/util.ts' }, diff --git a/src/ast/YAMLMap.ts b/src/ast/YAMLMap.ts index 740a22d6..5ff834ce 100644 --- a/src/ast/YAMLMap.ts +++ b/src/ast/YAMLMap.ts @@ -1,7 +1,7 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { isPair, isScalar, MAP, Node, NODE_TYPE } from './Node.js' +import { isPair, isScalar, MAP, NODE_TYPE, ParsedNode } from './Node.js' import { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import { ToJSContext } from './toJS.js' @@ -21,8 +21,10 @@ export function findPair( } export declare namespace YAMLMap { - interface Parsed - extends YAMLMap { + interface Parsed< + K extends ParsedNode = ParsedNode, + V extends ParsedNode | null = ParsedNode | null + > extends YAMLMap { items: Pair[] range: [number, number] } diff --git a/src/ast/YAMLSeq.ts b/src/ast/YAMLSeq.ts index 4b3b1ab2..94e680e9 100644 --- a/src/ast/YAMLSeq.ts +++ b/src/ast/YAMLSeq.ts @@ -1,13 +1,15 @@ import { Type } from '../constants.js' import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { Node, NODE_TYPE, SEQ, isScalar } from './Node.js' +import { NODE_TYPE, SEQ, isScalar, ParsedNode } from './Node.js' import type { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' export declare namespace YAMLSeq { - interface Parsed extends YAMLSeq { + interface Parsed< + T extends ParsedNode | Pair = ParsedNode + > extends YAMLSeq { items: T[] range: [number, number] } diff --git a/src/test-events.js b/src/test-events.ts similarity index 70% rename from src/test-events.js rename to src/test-events.ts index 081eec01..4e7efd3a 100644 --- a/src/test-events.js +++ b/src/test-events.ts @@ -1,7 +1,12 @@ +import { isNode, ParsedNode } from './ast/Node.js' +import { Pair } from './ast/Pair.js' +import { Scalar } from './ast/Scalar.js' +import { Document } from './doc/Document.js' import { parseAllDocuments } from './index.js' +import type { Options } from './options.js' // test harness for yaml-test-suite event tests -export function testEvents(src, options) { +export function testEvents(src: string, options?: Options) { const opt = Object.assign({ keepNodeTypes: true, version: '1.2' }, options) const docs = parseAllDocuments(src, opt) const errDoc = docs.find(doc => doc.errors.length > 0) @@ -13,16 +18,16 @@ export function testEvents(src, options) { let root = doc.contents if (Array.isArray(root)) root = root[0] const [rootStart, rootEnd] = doc.range || [0, 0] - let e = doc.errors[0] && doc.errors[0].source - if (e && e.type === 'SEQ_ITEM') e = e.node - if (e && (e.type === 'DOCUMENT' || e.range.start < rootStart)) + let error = doc.errors[0] + if (error && (!error.offset || error.offset < rootStart)) throw new Error() let docStart = '+DOC' if (doc.directivesEndMarker) docStart += ' ---' - else if (doc.contents.range[1] === doc.contents.range[0]) continue + else if (doc.contents && doc.contents.range[1] === doc.contents.range[0]) + continue events.push(docStart) - addEvents(events, doc, e, root) - if (doc.contents && doc.contents.length > 1) throw new Error() + addEvents(events, doc, error?.offset ?? -1, root) + let docEnd = '-DOC' if (rootEnd) { const post = src.slice(rootStart, rootEnd) @@ -37,14 +42,20 @@ export function testEvents(src, options) { return { events, error } } -function addEvents(events, doc, e, node) { +function addEvents( + events: string[], + doc: Document, + errPos: number, + node: ParsedNode | Pair | null +) { if (!node) { events.push('=VAL :') return } - if (e /*&& node.cstNode === e*/) throw new Error() + if (errPos !== -1 && isNode(node) && node.range[0] >= errPos) + throw new Error() let props = '' - let anchor = doc.anchors.getName(node) + let anchor = isNode(node) ? doc.anchors.getName(node) : undefined if (anchor) { if (/\d$/.test(anchor)) { const alt = anchor.replace(/\d$/, '') @@ -58,7 +69,7 @@ function addEvents(events, doc, e, node) { case 'ALIAS': { let alias = doc.anchors.getName(node.source) - if (/\d$/.test(alias)) { + if (alias && /\d$/.test(alias)) { const alt = alias.replace(/\d$/, '') if (doc.anchors.getNode(alt)) alias = alt } @@ -82,15 +93,15 @@ function addEvents(events, doc, e, node) { break case 'PAIR': events.push(`+MAP${props}`) - addEvents(events, doc, e, node.key) - addEvents(events, doc, e, node.value) + addEvents(events, doc, errPos, node.key) + addEvents(events, doc, errPos, node.value) events.push('-MAP') break case 'FLOW_SEQ': case 'SEQ': events.push(`+SEQ${props}`) node.items.forEach(item => { - addEvents(events, doc, e, item) + addEvents(events, doc, errPos, item) }) events.push('-SEQ') break @@ -98,8 +109,8 @@ function addEvents(events, doc, e, node) { case 'MAP': events.push(`+MAP${props}`) node.items.forEach(({ key, value }) => { - addEvents(events, doc, e, key) - addEvents(events, doc, e, value) + addEvents(events, doc, errPos, key) + addEvents(events, doc, errPos, value) }) events.push('-MAP') break @@ -107,7 +118,7 @@ function addEvents(events, doc, e, node) { throw new Error(`Unexpected node type ${node.type}`) } if (scalar) { - const value = node.source + const value = (node as Scalar.Parsed).source .replace(/\\/g, '\\\\') .replace(/\0/g, '\\0') .replace(/\x07/g, '\\a') From ca909be96e530d602e1a39ad4de7c2186407797d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 12:31:51 +0200 Subject: [PATCH 21/24] Rename src/ast/ -> src/nodes/ --- src/compose/compose-collection.ts | 8 ++++---- src/compose/compose-node.ts | 4 ++-- src/compose/compose-scalar.ts | 4 ++-- src/compose/composer.ts | 2 +- src/compose/resolve-block-map.ts | 4 ++-- src/compose/resolve-block-seq.ts | 2 +- src/compose/resolve-flow-collection.ts | 8 ++++---- src/compose/resolve-merge-pair.ts | 10 +++++----- src/doc/Anchors.ts | 6 +++--- src/doc/Document.ts | 18 +++++++++--------- src/doc/Schema.ts | 2 +- src/doc/createNode.ts | 8 ++++---- src/doc/directives.ts | 2 +- src/{ast => nodes}/Alias.ts | 0 src/{ast => nodes}/Collection.ts | 0 src/{ast => nodes}/Merge.ts | 0 src/{ast => nodes}/Node.ts | 0 src/{ast => nodes}/Pair.ts | 0 src/{ast => nodes}/Scalar.ts | 0 src/{ast => nodes}/YAMLMap.ts | 0 src/{ast => nodes}/YAMLSeq.ts | 0 src/{ast => nodes}/toJS.ts | 0 src/stringify/stringify.ts | 4 ++-- src/stringify/stringifyNumber.ts | 2 +- src/stringify/stringifyString.ts | 2 +- src/tags/core.ts | 2 +- src/tags/failsafe/map.ts | 6 +++--- src/tags/failsafe/seq.ts | 4 ++-- src/tags/json.ts | 2 +- src/tags/types.ts | 8 ++++---- src/tags/yaml-1.1/binary.ts | 4 +--- src/tags/yaml-1.1/index.ts | 2 +- src/tags/yaml-1.1/omap.ts | 10 +++++----- src/tags/yaml-1.1/pairs.ts | 8 ++++---- src/tags/yaml-1.1/set.ts | 10 +++++----- src/tags/yaml-1.1/timestamp.ts | 4 +--- src/test-events.ts | 8 ++++---- src/types.ts | 12 ++++++------ src/util.ts | 4 ++-- src/visit.ts | 14 +++++++------- tests/doc/createNode.js | 8 ++++---- tests/doc/types.js | 4 ++-- 42 files changed, 96 insertions(+), 100 deletions(-) rename src/{ast => nodes}/Alias.ts (100%) rename src/{ast => nodes}/Collection.ts (100%) rename src/{ast => nodes}/Merge.ts (100%) rename src/{ast => nodes}/Node.ts (100%) rename src/{ast => nodes}/Pair.ts (100%) rename src/{ast => nodes}/Scalar.ts (100%) rename src/{ast => nodes}/YAMLMap.ts (100%) rename src/{ast => nodes}/YAMLSeq.ts (100%) rename src/{ast => nodes}/toJS.ts (100%) diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts index ec7f7cbc..c003b394 100644 --- a/src/compose/compose-collection.ts +++ b/src/compose/compose-collection.ts @@ -1,9 +1,9 @@ -import { isNode, ParsedNode } from '../ast/Node.js' -import { Scalar } from '../ast/Scalar.js' -import type { YAMLMap } from '../ast/YAMLMap.js' -import type { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' +import { isNode, ParsedNode } from '../nodes/Node.js' +import { Scalar } from '../nodes/Scalar.js' +import type { YAMLMap } from '../nodes/YAMLMap.js' +import type { YAMLSeq } from '../nodes/YAMLSeq.js' import type { BlockMap, BlockSequence, diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 84cfa841..d40ec0c6 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -1,6 +1,6 @@ -import { Alias } from '../ast/Alias.js' -import type { Node, ParsedNode } from '../ast/Node.js' import type { Document } from '../doc/Document.js' +import { Alias } from '../nodes/Alias.js' +import type { Node, ParsedNode } from '../nodes/Node.js' import type { FlowScalar, Token } from '../parse/tokens.js' import { composeCollection } from './compose-collection.js' import { composeScalar } from './compose-scalar.js' diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index 7a1c78fc..909c661c 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -1,7 +1,7 @@ -import { isScalar } from '../ast/Node.js' -import { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' import type { Schema } from '../doc/Schema.js' +import { isScalar } from '../nodes/Node.js' +import { Scalar } from '../nodes/Scalar.js' import type { BlockScalar, FlowScalar } from '../parse/tokens.js' import type { ScalarTag } from '../tags/types.js' import { resolveBlockScalar } from './resolve-block-scalar.js' diff --git a/src/compose/composer.ts b/src/compose/composer.ts index 1e60e6e1..8d25394f 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -1,7 +1,7 @@ -import { isCollection } from '../ast/Node.js' import { Directives } from '../doc/directives.js' import { Document } from '../doc/Document.js' import { YAMLParseError, YAMLWarning } from '../errors.js' +import { isCollection } from '../nodes/Node.js' import { defaultOptions, Options } from '../options.js' import type { Token } from '../parse/tokens.js' import { composeDoc } from './compose-doc.js' diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 02bc75ad..e3d27d8d 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -1,7 +1,7 @@ -import { Pair } from '../ast/Pair.js' -import { YAMLMap } from '../ast/YAMLMap.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' +import { Pair } from '../nodes/Pair.js' +import { YAMLMap } from '../nodes/YAMLMap.js' import type { BlockMap } from '../parse/tokens.js' import type { ComposeNode } from './compose-node.js' import { resolveMergePair } from './resolve-merge-pair.js' diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index 75f98465..5b7c6929 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -1,6 +1,6 @@ -import { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' +import { YAMLSeq } from '../nodes/YAMLSeq.js' import type { BlockSequence } from '../parse/tokens.js' import type { ComposeNode } from './compose-node.js' import { resolveProps } from './resolve-props.js' diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index 076bf8ff..27f2cba9 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -1,9 +1,9 @@ -import { isNode, isPair, ParsedNode } from '../ast/Node.js' -import { Pair } from '../ast/Pair.js' -import { YAMLMap } from '../ast/YAMLMap.js' -import { YAMLSeq } from '../ast/YAMLSeq.js' import { Type } from '../constants.js' import type { Document } from '../doc/Document.js' +import { isNode, isPair, ParsedNode } from '../nodes/Node.js' +import { Pair } from '../nodes/Pair.js' +import { YAMLMap } from '../nodes/YAMLMap.js' +import { YAMLSeq } from '../nodes/YAMLSeq.js' import type { FlowCollection, SourceToken, Token } from '../parse/tokens.js' import type { ComposeNode } from './compose-node.js' import { resolveEnd } from './resolve-end.js' diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts index e9a9a29d..6651595d 100644 --- a/src/compose/resolve-merge-pair.ts +++ b/src/compose/resolve-merge-pair.ts @@ -1,8 +1,8 @@ -import { Alias } from '../ast/Alias.js' -import { Merge } from '../ast/Merge.js' -import { isAlias, isMap, isScalar, ParsedNode } from '../ast/Node.js' -import type { Pair } from '../ast/Pair.js' -import type { Scalar } from '../ast/Scalar.js' +import { Alias } from '../nodes/Alias.js' +import { Merge } from '../nodes/Merge.js' +import { isAlias, isMap, isScalar, ParsedNode } from '../nodes/Node.js' +import type { Pair } from '../nodes/Pair.js' +import type { Scalar } from '../nodes/Scalar.js' export function resolveMergePair( pair: Pair, diff --git a/src/doc/Anchors.ts b/src/doc/Anchors.ts index 654c9308..0b030cc3 100644 --- a/src/doc/Anchors.ts +++ b/src/doc/Anchors.ts @@ -1,6 +1,6 @@ -import { Alias } from '../ast/Alias.js' -import { Merge } from '../ast/Merge.js' -import { isAlias, isCollection, isMap, isScalar, Node } from '../ast/Node.js' +import { Alias } from '../nodes/Alias.js' +import { Merge } from '../nodes/Merge.js' +import { isAlias, isCollection, isMap, isScalar, Node } from '../nodes/Node.js' export class Anchors { map: Record = Object.create(null) diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 2ab8654e..e37fc7d3 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -1,5 +1,7 @@ -import { Alias } from '../ast/Alias.js' -import { collectionFromPath, isEmptyPath } from '../ast/Collection.js' +import { Type } from '../constants.js' +import type { YAMLError, YAMLWarning } from '../errors.js' +import { Alias } from '../nodes/Alias.js' +import { collectionFromPath, isEmptyPath } from '../nodes/Collection.js' import { DOC, isCollection, @@ -8,13 +10,11 @@ import { Node, NODE_TYPE, ParsedNode -} from '../ast/Node.js' -import { Pair } from '../ast/Pair.js' -import { toJS, ToJSAnchorValue, ToJSContext } from '../ast/toJS.js' -import type { YAMLMap } from '../ast/YAMLMap.js' -import type { YAMLSeq } from '../ast/YAMLSeq.js' -import { Type } from '../constants.js' -import type { YAMLError, YAMLWarning } from '../errors.js' +} from '../nodes/Node.js' +import { Pair } from '../nodes/Pair.js' +import { toJS, ToJSAnchorValue, ToJSContext } from '../nodes/toJS.js' +import type { YAMLMap } from '../nodes/YAMLMap.js' +import type { YAMLSeq } from '../nodes/YAMLSeq.js' import { DocumentOptions, Options, diff --git a/src/doc/Schema.ts b/src/doc/Schema.ts index 9ed62134..d4f22e79 100644 --- a/src/doc/Schema.ts +++ b/src/doc/Schema.ts @@ -1,4 +1,4 @@ -import type { Pair } from '../ast/Pair.js' +import type { Pair } from '../nodes/Pair.js' import { schemas, tags } from '../tags/index.js' import type { CollectionTag, ScalarTag, TagId, TagObj } from '../tags/types.js' import { Directives } from './directives.js' diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts index b2f8e10d..060061c1 100644 --- a/src/doc/createNode.ts +++ b/src/doc/createNode.ts @@ -1,8 +1,8 @@ -import type { Alias } from '../ast/Alias.js' -import { isNode, isPair, Node } from '../ast/Node.js' -import { Scalar } from '../ast/Scalar.js' -import type { YAMLMap } from '../ast/YAMLMap.js' import { defaultTagPrefix } from '../constants.js' +import type { Alias } from '../nodes/Alias.js' +import { isNode, isPair, Node } from '../nodes/Node.js' +import { Scalar } from '../nodes/Scalar.js' +import type { YAMLMap } from '../nodes/YAMLMap.js' import type { TagObj } from '../tags/types.js' import type { Replacer } from './Document.js' import type { Schema } from './Schema.js' diff --git a/src/doc/directives.ts b/src/doc/directives.ts index fd12abd7..8c24628a 100644 --- a/src/doc/directives.ts +++ b/src/doc/directives.ts @@ -1,4 +1,4 @@ -import { isNode } from '../ast/Node.js' +import { isNode } from '../nodes/Node.js' import { visit } from '../visit.js' import type { Document } from './Document.js' diff --git a/src/ast/Alias.ts b/src/nodes/Alias.ts similarity index 100% rename from src/ast/Alias.ts rename to src/nodes/Alias.ts diff --git a/src/ast/Collection.ts b/src/nodes/Collection.ts similarity index 100% rename from src/ast/Collection.ts rename to src/nodes/Collection.ts diff --git a/src/ast/Merge.ts b/src/nodes/Merge.ts similarity index 100% rename from src/ast/Merge.ts rename to src/nodes/Merge.ts diff --git a/src/ast/Node.ts b/src/nodes/Node.ts similarity index 100% rename from src/ast/Node.ts rename to src/nodes/Node.ts diff --git a/src/ast/Pair.ts b/src/nodes/Pair.ts similarity index 100% rename from src/ast/Pair.ts rename to src/nodes/Pair.ts diff --git a/src/ast/Scalar.ts b/src/nodes/Scalar.ts similarity index 100% rename from src/ast/Scalar.ts rename to src/nodes/Scalar.ts diff --git a/src/ast/YAMLMap.ts b/src/nodes/YAMLMap.ts similarity index 100% rename from src/ast/YAMLMap.ts rename to src/nodes/YAMLMap.ts diff --git a/src/ast/YAMLSeq.ts b/src/nodes/YAMLSeq.ts similarity index 100% rename from src/ast/YAMLSeq.ts rename to src/nodes/YAMLSeq.ts diff --git a/src/ast/toJS.ts b/src/nodes/toJS.ts similarity index 100% rename from src/ast/toJS.ts rename to src/nodes/toJS.ts diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index 660adfba..8aebfd09 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -1,6 +1,6 @@ -import { isAlias, isNode, isPair, isScalar, Node } from '../ast/Node.js' -import type { Scalar } from '../ast/Scalar.js' import type { Document } from '../doc/Document.js' +import { isAlias, isNode, isPair, isScalar, Node } from '../nodes/Node.js' +import type { Scalar } from '../nodes/Scalar.js' import type { TagObj } from '../tags/types.js' import { stringifyString } from './stringifyString.js' diff --git a/src/stringify/stringifyNumber.ts b/src/stringify/stringifyNumber.ts index 15de27a4..b12d8459 100644 --- a/src/stringify/stringifyNumber.ts +++ b/src/stringify/stringifyNumber.ts @@ -1,4 +1,4 @@ -import type { Scalar } from '../ast/Scalar.js' +import type { Scalar } from '../nodes/Scalar.js' export function stringifyNumber({ format, diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts index 6a993cf9..20287cd4 100644 --- a/src/stringify/stringifyString.ts +++ b/src/stringify/stringifyString.ts @@ -1,5 +1,5 @@ -import type { Scalar } from '../ast/Scalar.js' import { Type } from '../constants.js' +import type { Scalar } from '../nodes/Scalar.js' import { strOptions } from '../tags/options.js' import { addCommentBefore } from './addComment.js' import { diff --git a/src/tags/core.ts b/src/tags/core.ts index 9824fa15..ee5d5fbb 100644 --- a/src/tags/core.ts +++ b/src/tags/core.ts @@ -1,4 +1,4 @@ -import { Scalar } from '../ast/Scalar.js' +import { Scalar } from '../nodes/Scalar.js' import { stringifyNumber } from '../stringify/stringifyNumber.js' import { failsafe } from './failsafe/index.js' import { boolOptions, intOptions, nullOptions } from './options.js' diff --git a/src/tags/failsafe/map.ts b/src/tags/failsafe/map.ts index c2efde00..ebb7e72d 100644 --- a/src/tags/failsafe/map.ts +++ b/src/tags/failsafe/map.ts @@ -1,6 +1,6 @@ -import { isMap } from '../../ast/Node.js' -import { createPair } from '../../ast/Pair.js' -import { YAMLMap } from '../../ast/YAMLMap.js' +import { isMap } from '../../nodes/Node.js' +import { createPair } from '../../nodes/Pair.js' +import { YAMLMap } from '../../nodes/YAMLMap.js' import type { CreateNodeContext } from '../../doc/createNode.js' import type { Schema } from '../../doc/Schema.js' import type { CollectionTag } from '../types.js' diff --git a/src/tags/failsafe/seq.ts b/src/tags/failsafe/seq.ts index f73fea56..422dc26a 100644 --- a/src/tags/failsafe/seq.ts +++ b/src/tags/failsafe/seq.ts @@ -1,5 +1,5 @@ -import { isSeq } from '../../ast/Node.js' -import { YAMLSeq } from '../../ast/YAMLSeq.js' +import { isSeq } from '../../nodes/Node.js' +import { YAMLSeq } from '../../nodes/YAMLSeq.js' import { CreateNodeContext, createNode } from '../../doc/createNode.js' import type { Schema } from '../../doc/Schema.js' import type { CollectionTag } from '../types.js' diff --git a/src/tags/json.ts b/src/tags/json.ts index 067c1ae1..bba74495 100644 --- a/src/tags/json.ts +++ b/src/tags/json.ts @@ -1,6 +1,6 @@ /* global BigInt */ -import { Scalar } from '../ast/Scalar.js' +import { Scalar } from '../nodes/Scalar.js' import { map } from './failsafe/map.js' import { seq } from './failsafe/seq.js' import { intOptions } from './options.js' diff --git a/src/tags/types.ts b/src/tags/types.ts index 017e4052..517ee0b6 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -1,9 +1,9 @@ -import type { Node } from '../ast/Node.js' -import type { Scalar } from '../ast/Scalar.js' -import type { YAMLMap } from '../ast/YAMLMap.js' -import type { YAMLSeq } from '../ast/YAMLSeq.js' import type { CreateNodeContext } from '../doc/createNode.js' import type { Schema } from '../doc/Schema.js' +import type { Node } from '../nodes/Node.js' +import type { Scalar } from '../nodes/Scalar.js' +import type { YAMLMap } from '../nodes/YAMLMap.js' +import type { YAMLSeq } from '../nodes/YAMLSeq.js' import type { StringifyContext } from '../stringify/stringify.js' export type SchemaId = 'core' | 'failsafe' | 'json' | 'yaml11' diff --git a/src/tags/yaml-1.1/binary.ts b/src/tags/yaml-1.1/binary.ts index 984cdc1c..f0ff90b9 100644 --- a/src/tags/yaml-1.1/binary.ts +++ b/src/tags/yaml-1.1/binary.ts @@ -1,7 +1,5 @@ -/* global atob, btoa, Buffer */ - -import type { Scalar } from '../../ast/Scalar.js' import { Type } from '../../constants.js' +import type { Scalar } from '../../nodes/Scalar.js' import { stringifyString } from '../../stringify/stringifyString.js' import { binaryOptions as options } from '../options.js' import type { ScalarTag } from '../types.js' diff --git a/src/tags/yaml-1.1/index.ts b/src/tags/yaml-1.1/index.ts index da610116..d0093d97 100644 --- a/src/tags/yaml-1.1/index.ts +++ b/src/tags/yaml-1.1/index.ts @@ -1,4 +1,4 @@ -import { Scalar } from '../../ast/Scalar.js' +import { Scalar } from '../../nodes/Scalar.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { failsafe } from '../failsafe/index.js' import { boolOptions, intOptions, nullOptions } from '../options.js' diff --git a/src/tags/yaml-1.1/omap.ts b/src/tags/yaml-1.1/omap.ts index ec962518..32e1e6ca 100644 --- a/src/tags/yaml-1.1/omap.ts +++ b/src/tags/yaml-1.1/omap.ts @@ -1,9 +1,9 @@ -import { YAMLSeq } from '../../ast/YAMLSeq.js' -import { toJS, ToJSContext } from '../../ast/toJS.js' -import { isPair, isScalar } from '../../ast/Node.js' -import { YAMLMap } from '../../ast/YAMLMap.js' -import { createPairs, resolvePairs } from './pairs.js' +import { YAMLSeq } from '../../nodes/YAMLSeq.js' +import { toJS, ToJSContext } from '../../nodes/toJS.js' +import { isPair, isScalar } from '../../nodes/Node.js' +import { YAMLMap } from '../../nodes/YAMLMap.js' import { CollectionTag } from '../types.js' +import { createPairs, resolvePairs } from './pairs.js' export class YAMLOMap extends YAMLSeq { static tag = 'tag:yaml.org,2002:omap' diff --git a/src/tags/yaml-1.1/pairs.ts b/src/tags/yaml-1.1/pairs.ts index eea8ac91..bced411d 100644 --- a/src/tags/yaml-1.1/pairs.ts +++ b/src/tags/yaml-1.1/pairs.ts @@ -1,9 +1,9 @@ -import { isMap, isPair, isSeq } from '../../ast/Node.js' -import { createPair, Pair } from '../../ast/Pair.js' -import { YAMLMap } from '../../ast/YAMLMap.js' -import { YAMLSeq } from '../../ast/YAMLSeq.js' import type { CreateNodeContext } from '../../doc/createNode.js' import type { Schema } from '../../doc/Schema.js' +import { isMap, isPair, isSeq } from '../../nodes/Node.js' +import { createPair, Pair } from '../../nodes/Pair.js' +import { YAMLMap } from '../../nodes/YAMLMap.js' +import { YAMLSeq } from '../../nodes/YAMLSeq.js' import type { CollectionTag } from '../types.js' export function resolvePairs( diff --git a/src/tags/yaml-1.1/set.ts b/src/tags/yaml-1.1/set.ts index c1671194..59a4379f 100644 --- a/src/tags/yaml-1.1/set.ts +++ b/src/tags/yaml-1.1/set.ts @@ -1,9 +1,9 @@ -import { isMap, isPair, isScalar } from '../../ast/Node.js' -import { createPair, Pair } from '../../ast/Pair.js' -import { Scalar } from '../../ast/Scalar.js' -import { ToJSContext } from '../../ast/toJS.js' -import { YAMLMap, findPair } from '../../ast/YAMLMap.js' import type { Schema } from '../../doc/Schema.js' +import { isMap, isPair, isScalar } from '../../nodes/Node.js' +import { createPair, Pair } from '../../nodes/Pair.js' +import { Scalar } from '../../nodes/Scalar.js' +import { ToJSContext } from '../../nodes/toJS.js' +import { YAMLMap, findPair } from '../../nodes/YAMLMap.js' import type { StringifyContext } from '../../stringify/stringify.js' import type { CollectionTag } from '../types.js' diff --git a/src/tags/yaml-1.1/timestamp.ts b/src/tags/yaml-1.1/timestamp.ts index ceda23b7..3e9ff0d7 100644 --- a/src/tags/yaml-1.1/timestamp.ts +++ b/src/tags/yaml-1.1/timestamp.ts @@ -1,6 +1,4 @@ -/* global BigInt */ - -import type { Scalar } from '../../ast/Scalar.js' +import type { Scalar } from '../../nodes/Scalar.js' import { stringifyNumber } from '../../stringify/stringifyNumber.js' import { intOptions } from '../options.js' import type { ScalarTag } from '../types.js' diff --git a/src/test-events.ts b/src/test-events.ts index 4e7efd3a..1b023f80 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -1,9 +1,9 @@ -import { isNode, ParsedNode } from './ast/Node.js' -import { Pair } from './ast/Pair.js' -import { Scalar } from './ast/Scalar.js' import { Document } from './doc/Document.js' -import { parseAllDocuments } from './index.js' +import { isNode, ParsedNode } from './nodes/Node.js' +import { Pair } from './nodes/Pair.js' +import { Scalar } from './nodes/Scalar.js' import type { Options } from './options.js' +import { parseAllDocuments } from './public-api.js' // test harness for yaml-test-suite event tests export function testEvents(src: string, options?: Options) { diff --git a/src/types.ts b/src/types.ts index ecf2727f..1331180f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ export * from './tags/options.js' export { Schema } from './doc/Schema.js' -export { Alias } from './ast/Alias.js' -export { Merge } from './ast/Merge.js' -export { Pair } from './ast/Pair.js' -export { Scalar } from './ast/Scalar.js' -export { YAMLMap } from './ast/YAMLMap.js' -export { YAMLSeq } from './ast/YAMLSeq.js' +export { Alias } from './nodes/Alias.js' +export { Merge } from './nodes/Merge.js' +export { Pair } from './nodes/Pair.js' +export { Scalar } from './nodes/Scalar.js' +export { YAMLMap } from './nodes/YAMLMap.js' +export { YAMLSeq } from './nodes/YAMLSeq.js' diff --git a/src/util.ts b/src/util.ts index 2e752883..a8c976a5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ -export { findPair } from './ast/YAMLMap.js' -export { toJS, ToJSContext } from './ast/toJS.js' +export { findPair } from './nodes/YAMLMap.js' +export { toJS, ToJSContext } from './nodes/toJS.js' export { stringifyNumber } from './stringify/stringifyNumber.js' export { stringifyString } from './stringify/stringifyString.js' export { Type } from './constants.js' diff --git a/src/visit.ts b/src/visit.ts index 7cea7790..c6b4acde 100644 --- a/src/visit.ts +++ b/src/visit.ts @@ -1,4 +1,5 @@ -import type { Alias } from './ast/Alias.js' +import type { Document } from './doc/Document.js' +import type { Alias } from './nodes/Alias.js' import { isAlias, isCollection, @@ -9,12 +10,11 @@ import { isScalar, isSeq, Node -} from './ast/Node.js' -import type { Pair } from './ast/Pair.js' -import type { Scalar } from './ast/Scalar.js' -import type { YAMLMap } from './ast/YAMLMap.js' -import type { YAMLSeq } from './ast/YAMLSeq.js' -import type { Document } from './doc/Document.js' +} from './nodes/Node.js' +import type { Pair } from './nodes/Pair.js' +import type { Scalar } from './nodes/Scalar.js' +import type { YAMLMap } from './nodes/YAMLMap.js' +import type { YAMLSeq } from './nodes/YAMLSeq.js' const BREAK = Symbol('break visit') const SKIP = Symbol('skip children') diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js index 5417aa0d..ba7b4969 100644 --- a/tests/doc/createNode.js +++ b/tests/doc/createNode.js @@ -1,8 +1,8 @@ import * as YAML from '../../src/index.js' -import { PairType } from '../../src/ast/Pair.js' -import { Scalar } from '../../src/ast/Scalar.js' -import { YAMLMap } from '../../src/ast/YAMLMap.js' -import { YAMLSeq } from '../../src/ast/YAMLSeq.js' +import { PairType } from '../../src/nodes/Pair.js' +import { Scalar } from '../../src/nodes/Scalar.js' +import { YAMLMap } from '../../src/nodes/YAMLMap.js' +import { YAMLSeq } from '../../src/nodes/YAMLSeq.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' let doc diff --git a/tests/doc/types.js b/tests/doc/types.js index d514b131..7322bb8a 100644 --- a/tests/doc/types.js +++ b/tests/doc/types.js @@ -1,6 +1,6 @@ import * as YAML from '../../src/index.js' -import { Scalar } from '../../src/ast/Scalar.js' -import { YAMLSeq } from '../../src/ast/YAMLSeq.js' +import { Scalar } from '../../src/nodes/Scalar.js' +import { YAMLSeq } from '../../src/nodes/YAMLSeq.js' import { binary } from '../../src/tags/yaml-1.1/binary.js' import { YAMLOMap } from '../../src/tags/yaml-1.1/omap.js' import { YAMLSet } from '../../src/tags/yaml-1.1/set.js' From 79e7da384c196f83339689c543b6e965236f0541 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 13:50:02 +0200 Subject: [PATCH 22/24] Enable ESLint for TypeScript files; fix issues --- .eslintrc.yaml | 26 +-- package-lock.json | 231 ++++++++++++++++++++----- package.json | 7 +- src/compose/resolve-flow-collection.ts | 3 +- src/doc/Document.ts | 2 +- src/log.ts | 3 +- src/nodes/Scalar.ts | 2 +- src/parse/lexer.ts | 2 +- src/parse/parser.ts | 9 +- src/tags/types.ts | 2 +- src/test-events.ts | 2 +- 11 files changed, 224 insertions(+), 65 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index ea82e48e..ee760b7d 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -1,34 +1,38 @@ root: true -parser: babel-eslint +parser: "@typescript-eslint/parser" env: node: true +plugins: + - "@typescript-eslint" + extends: - eslint:recommended + - plugin:@typescript-eslint/recommended - prettier rules: array-callback-return: error camelcase: error - consistent-return: error - eqeqeq: [error, always, 'null': ignore] + consistent-return: 0 + eqeqeq: [error, always, "null": ignore] no-constant-condition: [error, checkLoops: false] no-control-regex: 0 + no-fallthrough: [error, commentPattern: fallthrough] no-implicit-globals: error no-template-curly-in-string: warn - no-unused-labels: 0 no-var: error prefer-const: [warn, destructuring: all] + "@typescript-eslint/ban-ts-comment": off + "@typescript-eslint/explicit-module-boundary-types": off + "@typescript-eslint/no-explicit-any": off + "@typescript-eslint/no-namespace": off + "@typescript-eslint/no-unused-vars": off + "@typescript-eslint/no-unused-vars-experimental": warn overrides: - - files: src/**/*.js - env: - es6: true - node: false - - files: - - tests/**/*.js + - files: [tests/**] env: - es6: true jest: true rules: camelcase: 0 diff --git a/package-lock.json b/package-lock.json index ec68b3c0..f2446541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1838,9 +1838,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -2525,6 +2525,12 @@ "pretty-format": "^26.0.0" } }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -2570,6 +2576,139 @@ "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.2.tgz", + "integrity": "sha512-uiQQeu9tWl3f1+oK0yoAv9lt/KXO24iafxgQTkIYO/kitruILGx3uH+QtIAHqxFV+yIsdnJH+alel9KuE3J15Q==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.15.2", + "@typescript-eslint/scope-manager": "4.15.2", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.2.tgz", + "integrity": "sha512-Fxoshw8+R5X3/Vmqwsjc8nRO/7iTysRtDqx6rlfLZ7HbT8TZhPeQqbPjTyk2RheH3L8afumecTQnUc9EeXxohQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.15.2", + "@typescript-eslint/types": "4.15.2", + "@typescript-eslint/typescript-estree": "4.15.2", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.15.2.tgz", + "integrity": "sha512-SHeF8xbsC6z2FKXsaTb1tBCf0QZsjJ94H6Bo51Y1aVEZ4XAefaw5ZAilMoDPlGghe+qtq7XdTiDlGfVTOmvA+Q==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.15.2", + "@typescript-eslint/types": "4.15.2", + "@typescript-eslint/typescript-estree": "4.15.2", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.2.tgz", + "integrity": "sha512-Zm0tf/MSKuX6aeJmuXexgdVyxT9/oJJhaCkijv0DvJVT3ui4zY6XYd6iwIo/8GEZGy43cd7w1rFMiCLHbRzAPQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.2", + "@typescript-eslint/visitor-keys": "4.15.2" + } + }, + "@typescript-eslint/types": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.2.tgz", + "integrity": "sha512-r7lW7HFkAarfUylJ2tKndyO9njwSyoy6cpfDKWPX6/ctZA+QyaYscAHXVAfJqtnY6aaTwDYrOhp+ginlbc7HfQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.2.tgz", + "integrity": "sha512-cGR8C2g5SPtHTQvAymEODeqx90pJHadWsgTtx6GbnTWKqsg7yp6Eaya9nFzUd4KrKhxdYTTFBiYeTPQaz/l8bw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.2", + "@typescript-eslint/visitor-keys": "4.15.2", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.2.tgz", + "integrity": "sha512-TME1VgSb7wTwgENN5KVj4Nqg25hP8DisXxNBojM4Nn31rYaNDIocNm5cmjOFfh42n7NVERxWrDFoETO/76ePyg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.2", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -2754,20 +2893,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", @@ -3633,12 +3758,12 @@ } }, "eslint": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", - "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", + "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3650,7 +3775,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", @@ -3677,6 +3802,15 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3744,9 +3878,9 @@ "dev": true }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "path-key": { @@ -3800,9 +3934,9 @@ } }, "eslint-config-prettier": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz", - "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz", + "integrity": "sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==", "dev": true }, "eslint-scope": { @@ -3848,9 +3982,9 @@ "dev": true }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -4174,9 +4308,9 @@ } }, "file-entry-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", - "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { "flat-cache": "^3.0.4" @@ -7954,9 +8088,9 @@ }, "dependencies": { "ajv": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", - "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz", + "integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -7972,9 +8106,9 @@ "dev": true }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true } } @@ -8091,6 +8225,23 @@ "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", "dev": true }, + "tsutils": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", + "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index c800d4ec..be6b6bd9 100644 --- a/package.json +++ b/package.json @@ -86,12 +86,13 @@ "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-typescript": "^8.1.1", "@types/jest": "^26.0.20", - "babel-eslint": "^10.1.0", + "@typescript-eslint/eslint-plugin": "^4.15.2", + "@typescript-eslint/parser": "^4.15.2", "babel-jest": "^26.6.3", "common-tags": "^1.8.0", "cross-env": "^7.0.3", - "eslint": "^7.19.0", - "eslint-config-prettier": "^7.2.0", + "eslint": "^7.20.0", + "eslint-config-prettier": "^8.1.0", "fast-check": "^2.12.0", "jest": "^26.6.3", "jest-ts-webcompat-resolver": "^1.0.0", diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index 27f2cba9..193c903a 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -80,7 +80,7 @@ export function resolveFlowCollection( case 'space': hasSpace = true break - case 'comment': + case 'comment': { if (doc.options.strict && !hasSpace) onError( offset, @@ -93,6 +93,7 @@ export function resolveFlowCollection( hasComment = true newlines = '' break + } case 'newline': if (atLineStart && !hasComment) spaceBefore = true if (atValueEnd) { diff --git a/src/doc/Document.ts b/src/doc/Document.ts index e37fc7d3..0b0d05a2 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -96,7 +96,7 @@ export class Document { directives: Directives - directivesEndMarker: boolean = false + directivesEndMarker = false /** Errors encountered during parsing. */ errors: YAMLError[] = [] diff --git a/src/log.ts b/src/log.ts index 802cbdad..0d5f68ee 100644 --- a/src/log.ts +++ b/src/log.ts @@ -3,8 +3,7 @@ import { LogLevel, LogLevelId } from './constants.js' export function debug(logLevel: LogLevelId, ...messages: any[]) { - if (LogLevel.indexOf(logLevel) >= LogLevel.DEBUG) - console.log.apply(console, messages) + if (LogLevel.indexOf(logLevel) >= LogLevel.DEBUG) console.log(...messages) } export function warn(logLevel: LogLevelId, warning: string | Error) { diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 7176574f..576e84a0 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,4 +1,4 @@ -import { Type } from '../constants.js' +import type { Type } from '../constants.js' import { NodeBase, NODE_TYPE, SCALAR } from './Node.js' import { toJS, ToJSContext } from './toJS.js' diff --git a/src/parse/lexer.ts b/src/parse/lexer.ts index 27dd0b3d..22b72ccc 100644 --- a/src/parse/lexer.ts +++ b/src/parse/lexer.ts @@ -436,8 +436,8 @@ export class Lexer { this.pushSpaces(true) return 'flow' } - // fallthrough } + // fallthrough default: this.flowKey = false return this.parsePlainScalar() diff --git a/src/parse/parser.ts b/src/parse/parser.ts index 4a2aed49..e8e97006 100644 --- a/src/parse/parser.ts +++ b/src/parse/parser.ts @@ -110,7 +110,7 @@ function getFirstKeyStartProps(prev: SourceToken[]) { break loop } } - while (prev[++i]?.type === 'space') {} + while (prev[++i]?.type === 'space') { /* loop */ } return prev.splice(i, prev.length) } @@ -587,7 +587,10 @@ export class Parser { default: { const bv = this.startBlockValue(map) - if (bv) return this.stack.push(bv) + if (bv) { + this.stack.push(bv) + return + } } } } @@ -677,7 +680,7 @@ export class Parser { const parent = this.peek(2) if ( parent.type === 'block-map' && - (this.type == 'map-value-ind' || + (this.type === 'map-value-ind' || (this.type === 'newline' && !parent.items[parent.items.length - 1].sep)) ) { diff --git a/src/tags/types.ts b/src/tags/types.ts index 517ee0b6..e6b7ea0d 100644 --- a/src/tags/types.ts +++ b/src/tags/types.ts @@ -53,7 +53,7 @@ interface TagBase { /** * Used by some tags to configure their stringification, where applicable. */ - options?: object + options?: Record /** * The identifier for your data type, with which its stringified form will be diff --git a/src/test-events.ts b/src/test-events.ts index 1b023f80..607c8e80 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -18,7 +18,7 @@ export function testEvents(src: string, options?: Options) { let root = doc.contents if (Array.isArray(root)) root = root[0] const [rootStart, rootEnd] = doc.range || [0, 0] - let error = doc.errors[0] + const error = doc.errors[0] if (error && (!error.offset || error.offset < rootStart)) throw new Error() let docStart = '+DOC' From 4ce72b349a58965dc38e45693748b805f020d91d Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 15:24:46 +0200 Subject: [PATCH 23/24] Expose & test is* functions; add casting generic to documents --- src/doc/Document.ts | 26 +++--- src/index.ts | 24 +++++- src/nodes/Alias.ts | 13 +-- src/nodes/Collection.ts | 4 +- src/nodes/Node.ts | 73 +++++++++-------- src/nodes/Pair.ts | 5 +- src/nodes/Scalar.ts | 6 +- src/nodes/YAMLMap.ts | 15 ++-- src/nodes/YAMLSeq.ts | 11 ++- src/public-api.ts | 22 ++--- tests/is-node.ts | 172 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 285 insertions(+), 86 deletions(-) create mode 100644 tests/is-node.ts diff --git a/src/doc/Document.ts b/src/doc/Document.ts index 0b0d05a2..ec12a8ff 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -61,8 +61,7 @@ export interface ToJSOptions { } export declare namespace Document { - interface Parsed extends Document { - contents: ParsedNode | null + interface Parsed extends Document { range: [number, number] /** The schema used with the document. */ schema: Schema @@ -74,10 +73,10 @@ export declare namespace Document { } } -export class Document { +export class Document { static defaults = documentOptions; - [NODE_TYPE] = DOC + readonly [NODE_TYPE]: symbol /** * Anchors associated with the document's nodes; @@ -92,7 +91,7 @@ export class Document { comment: string | null = null /** The document contents. */ - contents: unknown + contents: T | null directives: Directives @@ -134,6 +133,7 @@ export class Document { replacer?: Replacer | Options | null, options?: Options ) { + Object.defineProperty(this, NODE_TYPE, { value: DOC }) let _replacer: Replacer | undefined = undefined if (typeof replacer === 'function' || Array.isArray(replacer)) { _replacer = replacer @@ -156,7 +156,7 @@ export class Document { this.contents = value === undefined ? null - : this.createNode(value, { replacer: _replacer }) + : ((this.createNode(value, { replacer: _replacer }) as unknown) as T) } /** Adds a value to the document. */ @@ -308,7 +308,11 @@ export class Document { */ set(key: any, value: unknown) { if (this.contents == null) { - this.contents = collectionFromPath(this.schema, [key], value) + this.contents = (collectionFromPath( + this.schema, + [key], + value + ) as unknown) as T } else if (assertCollection(this.contents)) { this.contents.set(key, value) } @@ -319,9 +323,13 @@ export class Document { * boolean to add/remove the item from the set. */ setIn(path: Iterable, value: unknown) { - if (isEmptyPath(path)) this.contents = value + if (isEmptyPath(path)) this.contents = (value as unknown) as T else if (this.contents == null) { - this.contents = collectionFromPath(this.schema, Array.from(path), value) + this.contents = (collectionFromPath( + this.schema, + Array.from(path), + value + ) as unknown) as T } else if (assertCollection(this.contents)) { this.contents.setIn(path, value) } diff --git a/src/index.ts b/src/index.ts index 7b089648..b4aa6f99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,31 @@ export { Composer } from './compose/composer.js' export { CreateNodeOptions, Document } from './doc/Document.js' export { Options, defaultOptions, scalarOptions } from './options.js' -export { visit, visitor, visitorFn } from './visit.js' + +export { + isAlias, + isCollection, + isDocument, + isMap, + isNode, + isPair, + isScalar, + isSeq, + Node, + ParsedNode +} from './nodes/Node.js' export { Lexer } from './parse/lexer.js' export { LineCounter } from './parse/line-counter.js' export { Parser } from './parse/parser.js' export * as tokens from './parse/tokens.js' -export * from './public-api.js' +export { + EmptyStream, + parse, + parseAllDocuments, + parseDocument, + stringify +} from './public-api.js' + +export { visit, visitor, visitorFn } from './visit.js' diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts index d1007e82..975eaca9 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -1,14 +1,6 @@ import { Type } from '../constants.js' import { StringifyContext } from '../stringify/stringify.js' -import { - ALIAS, - isAlias, - isCollection, - isPair, - Node, - NodeBase, - NODE_TYPE -} from './Node.js' +import { ALIAS, isAlias, isCollection, isPair, Node, NodeBase } from './Node.js' import { toJS, ToJSContext } from './toJS.js' export declare namespace Alias { @@ -18,12 +10,11 @@ export declare namespace Alias { } export class Alias extends NodeBase { - [NODE_TYPE] = ALIAS source: T type: Type.ALIAS = Type.ALIAS constructor(source: T) { - super() + super(ALIAS) this.source = source Object.defineProperty(this, 'tag', { set() { diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts index 6d01166f..87fe7bfe 100644 --- a/src/nodes/Collection.ts +++ b/src/nodes/Collection.ts @@ -71,8 +71,8 @@ export abstract class Collection extends NodeBase { | Type.FLOW_SEQ | Type.DOCUMENT - constructor(schema?: Schema) { - super() + constructor(type: symbol, schema?: Schema) { + super(type) Object.defineProperty(this, 'schema', { value: schema, configurable: true, diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index baead79d..6d33ea64 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -15,41 +15,6 @@ export type ParsedNode = | YAMLMap.Parsed | YAMLSeq.Parsed -export abstract class NodeBase { - /** A comment on or immediately after this */ - declare comment?: string | null - - /** A comment before this */ - declare commentBefore?: string | null - - /** Only available when `keepCstNodes` is set to `true` */ - // cstNode?: CST.Node - - /** - * The [start, end] range of characters of the source parsed - * into this node (undefined for pairs or if not parsed) - */ - declare range?: [number, number] | null - - /** A blank line before this node and its commentBefore */ - declare spaceBefore?: boolean - - /** A fully qualified tag, if required */ - declare tag?: string - - /** A plain JS representation of this node */ - abstract toJSON(): any - - abstract toString( - ctx?: StringifyContext, - onComment?: () => void, - onChompKeep?: () => void - ): string - - /** The type of this node */ - declare type?: Type | PairType -} - export const ALIAS = Symbol.for('yaml.alias') export const DOC = Symbol.for('yaml.document') export const MAP = Symbol.for('yaml.map') @@ -97,3 +62,41 @@ export function isNode(node: any): node is Node { } return false } + +export abstract class NodeBase { + readonly [NODE_TYPE]: symbol + + /** A comment on or immediately after this */ + declare comment?: string | null + + /** A comment before this */ + declare commentBefore?: string | null + + /** + * The [start, end] range of characters of the source parsed + * into this node (undefined for pairs or if not parsed) + */ + declare range?: [number, number] | null + + /** A blank line before this node and its commentBefore */ + declare spaceBefore?: boolean + + /** A fully qualified tag, if required */ + declare tag?: string + + /** A plain JS representation of this node */ + abstract toJSON(): any + + abstract toString( + ctx?: StringifyContext, + onComment?: () => void, + onChompKeep?: () => void + ): string + + /** The type of this node */ + declare type?: Type | PairType + + constructor(type: symbol) { + Object.defineProperty(this, NODE_TYPE, { value: type }) + } +} diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 034a83e3..d38978cb 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -13,7 +13,6 @@ import { isSeq, Node, NodeBase, - NODE_TYPE, PAIR } from './Node.js' @@ -33,8 +32,6 @@ export enum PairType { } export class Pair extends NodeBase { - [NODE_TYPE] = PAIR - /** Always Node or null when parsed, but can be set to anything. */ key: K @@ -44,7 +41,7 @@ export class Pair extends NodeBase { type: PairType constructor(key: K, value: V | null = null) { - super() + super(PAIR) this.key = key this.value = value this.type = PairType.PAIR diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 576e84a0..f8737274 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,5 +1,5 @@ import type { Type } from '../constants.js' -import { NodeBase, NODE_TYPE, SCALAR } from './Node.js' +import { NodeBase, SCALAR } from './Node.js' import { toJS, ToJSContext } from './toJS.js' export const isScalarValue = (value: unknown) => @@ -20,8 +20,6 @@ export declare namespace Scalar { } export class Scalar extends NodeBase { - [NODE_TYPE] = SCALAR - value: T declare type?: Scalar.Type @@ -39,7 +37,7 @@ export class Scalar extends NodeBase { declare source?: string constructor(value: T) { - super() + super(SCALAR) this.value = value } diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index 5ff834ce..ccffbec1 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -1,10 +1,11 @@ -import { Type } from '../constants.js' -import { StringifyContext } from '../stringify/stringify.js' +import type { Type } from '../constants.js' +import type { Schema } from '../doc/Schema.js' +import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { isPair, isScalar, MAP, NODE_TYPE, ParsedNode } from './Node.js' +import { isPair, isScalar, MAP, ParsedNode } from './Node.js' import { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' -import { ToJSContext } from './toJS.js' +import type { ToJSContext } from './toJS.js' export function findPair( items: Iterable>, @@ -35,12 +36,14 @@ export class YAMLMap extends Collection { return 'tag:yaml.org,2002:map' } - [NODE_TYPE] = MAP - items: Pair[] = [] type?: Type.FLOW_MAP | Type.MAP + constructor(schema?: Schema) { + super(MAP, schema) + } + /** * Adds a value to the collection. * diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index 94e680e9..a48a97c9 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -1,7 +1,8 @@ -import { Type } from '../constants.js' +import type { Type } from '../constants.js' +import type { Schema } from '../doc/Schema.js' import type { StringifyContext } from '../stringify/stringify.js' import { Collection } from './Collection.js' -import { NODE_TYPE, SEQ, isScalar, ParsedNode } from './Node.js' +import { isScalar, ParsedNode, SEQ } from './Node.js' import type { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' @@ -20,12 +21,14 @@ export class YAMLSeq extends Collection { return 'tag:yaml.org,2002:seq' } - [NODE_TYPE] = SEQ - items: T[] = [] type?: Type.FLOW_SEQ | Type.SEQ + constructor(schema?: Schema) { + super(SEQ, schema) + } + add(value: T) { this.items.push(value) } diff --git a/src/public-api.ts b/src/public-api.ts index 270b4770..d49801db 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -3,6 +3,7 @@ import { LogLevel } from './constants.js' import { Document, Replacer, Reviver } from './doc/Document.js' import { YAMLParseError } from './errors.js' import { warn } from './log.js' +import { ParsedNode } from './nodes/Node.js' import { Options } from './options.js' import { Parser } from './parse/parser.js' @@ -21,32 +22,35 @@ export interface EmptyStream * EmptyStream and contain additional stream information. In * TypeScript, you should use `'empty' in docs` as a type guard for it. */ -export function parseAllDocuments( +export function parseAllDocuments( source: string, options?: Options -): Document.Parsed[] | EmptyStream { - const docs: Document.Parsed[] = [] - const composer = new Composer(doc => docs.push(doc), options) +): Document.Parsed[] | EmptyStream { + const docs: Document.Parsed[] = [] + const composer = new Composer( + doc => docs.push(doc as Document.Parsed), + options + ) const parser = new Parser(composer.next, options?.lineCounter?.addNewLine) parser.parse(source) composer.end() if (docs.length > 0) return docs return Object.assign< - Document.Parsed[], + Document.Parsed[], { empty: true }, ReturnType >([], { empty: true }, composer.streamInfo()) } /** Parse an input string into a single YAML.Document */ -export function parseDocument( +export function parseDocument( source: string, options?: Options -): Document.Parsed | null { - let doc: Document.Parsed | null = null +): Document.Parsed | null { + let doc: Document.Parsed | null = null const composer = new Composer(_doc => { - if (!doc) doc = _doc + if (!doc) doc = _doc as Document.Parsed else if (LogLevel.indexOf(doc.options.logLevel) >= LogLevel.ERROR) { const errMsg = 'Source contains multiple documents; please use YAML.parseAllDocuments()' diff --git a/tests/is-node.ts b/tests/is-node.ts new file mode 100644 index 00000000..fda3e5d2 --- /dev/null +++ b/tests/is-node.ts @@ -0,0 +1,172 @@ +import { Alias, Pair, Scalar, YAMLMap, YAMLSeq } from '../dist/types.js' +import { + Document, + isAlias, + isCollection, + isDocument, + isMap, + isNode, + isPair, + isScalar, + isSeq, + parseDocument +} from '../index.js' + +for (const { fn, exp } of [ + { + fn: isAlias, + exp: { + doc: false, + scalar: false, + map: false, + seq: false, + alias: true, + pair: false + } + }, + { + fn: isCollection, + exp: { + doc: false, + scalar: false, + map: true, + seq: true, + alias: false, + pair: false + } + }, + { + fn: isDocument, + exp: { + doc: true, + scalar: false, + map: false, + seq: false, + alias: false, + pair: false + } + }, + { + fn: isMap, + exp: { + doc: false, + scalar: false, + map: true, + seq: false, + alias: false, + pair: false + } + }, + { + fn: isNode, + exp: { + doc: false, + scalar: true, + map: true, + seq: true, + alias: true, + pair: false + } + }, + { + fn: isPair, + exp: { + doc: false, + scalar: false, + map: false, + seq: false, + alias: false, + pair: true + } + }, + { + fn: isScalar, + exp: { + doc: false, + scalar: true, + map: false, + seq: false, + alias: false, + pair: false + } + }, + { + fn: isSeq, + exp: { + doc: false, + scalar: false, + map: false, + seq: true, + alias: false, + pair: false + } + } +]) { + describe(fn.name, () => { + test('parsed doc', () => { + const doc = parseDocument('foo') + expect(fn(doc)).toBe(exp.doc) + }) + + test('parsed scalar', () => { + const doc = parseDocument('foo') + expect(fn(doc.contents)).toBe(exp.scalar) + }) + + test('parsed map', () => { + const doc = parseDocument('foo: bar') + expect(fn(doc.contents)).toBe(exp.map) + }) + + test('parsed seq', () => { + const doc = parseDocument('[foo, bar]') + expect(fn(doc.contents)).toBe(exp.seq) + }) + + test('parsed alias', () => { + const doc = parseDocument('[ &a foo, *a ]') + expect(fn(doc.contents.items[1])).toBe(exp.alias) + }) + + test('parsed pair', () => { + const doc = parseDocument('foo: bar') + expect(fn(doc.contents.items[0])).toBe(exp.pair) + }) + + test('created doc', () => { + expect(fn(new Document())).toBe(exp.doc) + }) + + test('created scalar', () => { + expect(fn(new Scalar(42))).toBe(exp.scalar) + }) + + test('created map', () => { + expect(fn(new YAMLMap())).toBe(exp.map) + }) + + test('created seq', () => { + expect(fn(new YAMLSeq())).toBe(exp.seq) + }) + + test('created alias', () => { + expect(fn(new Alias(new Scalar(42)))).toBe(exp.alias) + }) + + test('created pair', () => { + expect(fn(new Pair(null))).toBe(exp.pair) + }) + + test('null', () => { + expect(fn(null)).toBe(false) + }) + + test('string', () => { + expect(fn('foo')).toBe(false) + }) + + test('object', () => { + expect(fn({ type: 'SCALAR', value: 42 })).toBe(false) + }) + }) +} From 26f19b74ad749af60c4337991e81598c8268b9c5 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sat, 27 Feb 2021 15:49:38 +0200 Subject: [PATCH 24/24] Fix test import paths --- jest.config.js | 6 +++--- tests/is-node.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jest.config.js b/jest.config.js index 27f644ab..104b2dc1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,10 +21,10 @@ switch (process.env.npm_lifecycle_event) { default: process.env.TRACE_LEVEL = 'log' moduleNameMapper = { - '^\\./dist$': '/src/index.js', - '^\\./dist/types(\\.js)?$': '/src/types.js', + '^\\./dist$': '/src/index.ts', + '^\\./dist/types(\\.js)?$': '/src/types.ts', '^\\./dist/(.+)$': '/src/$1', - '^\\.\\./dist/test-events.js$': '/src/test-events.js' + '^\\.\\./dist/test-events.js$': '/src/test-events.ts' } } diff --git a/tests/is-node.ts b/tests/is-node.ts index fda3e5d2..4307d9e6 100644 --- a/tests/is-node.ts +++ b/tests/is-node.ts @@ -1,4 +1,4 @@ -import { Alias, Pair, Scalar, YAMLMap, YAMLSeq } from '../dist/types.js' +import { Alias, Pair, Scalar, YAMLMap, YAMLSeq } from '../types.js' import { Document, isAlias,