diff --git a/docs/03_options.md b/docs/03_options.md index 393d4a15..3d1af9a9 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -24,6 +24,8 @@ The `version` option value (`'1.2'` by default) may be overridden by any documen | --------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | anchorPrefix | `string` | Default prefix for anchors. By default `'a'`, resulting in anchors `a1`, `a2`, etc. | | customTags | `Tag[] ⎮ function` | Array of [additional tags](#custom-data-types) to include in the schema | +| indent | `number` | The number of spaces to use when indenting code. By default `2`. | +| indentSeq | `boolean` | Whether block sequences should be indented. By default `true`. | | keepBlobsInJSON | `boolean` | Allow non-JSON JavaScript objects to remain in the `toJSON` output. Relevant with the YAML 1.1 `!!timestamp` and `!!binary` tags as well as BigInts. By default `true`. | | keepCstNodes | `boolean` | Include references in the AST to each node's corresponding CST node. By default `false`. | | keepNodeTypes | `boolean` | Store the original node type when parsing documents. By default `true`. | diff --git a/index.d.ts b/index.d.ts index 2c85042d..7bd17f20 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,6 +21,18 @@ export interface Options extends Schema.Options { * Default: `'a'`, resulting in anchors `a1`, `a2`, etc. */ anchorPrefix?: string + /** + * The number of spaces to use when indenting code. + * + * Default: `2` + */ + indent?: number + /** + * Whether block sequences should be indented. + * + * Default: `true` + */ + indentSeq?: boolean /** * Allow non-JSON JavaScript objects to remain in the `toJSON` output. * Relevant with the YAML 1.1 `!!timestamp` and `!!binary` tags as well as BigInts. diff --git a/playground b/playground index 9d69d93c..68d9d637 160000 --- a/playground +++ b/playground @@ -1 +1 @@ -Subproject commit 9d69d93ce153f9c8ff64ae4d0aae34a481f03b20 +Subproject commit 68d9d6376bea61b21b4e885910c151dca8b30dd1 diff --git a/src/Document.js b/src/Document.js index 8c1eb1fa..494f8cf4 100644 --- a/src/Document.js +++ b/src/Document.js @@ -543,7 +543,13 @@ export class Document { const keep = keepBlobsInJSON && (typeof arg !== 'string' || !(this.contents instanceof Scalar)) - const ctx = { doc: this, keep, mapAsMap: keep && !!mapAsMap, maxAliasCount } + const ctx = { + doc: this, + indentStep: ' ', + keep, + mapAsMap: keep && !!mapAsMap, + maxAliasCount + } const anchorNames = Object.keys(this.anchors.map) if (anchorNames.length > 0) ctx.anchors = anchorNames.map(name => ({ @@ -558,6 +564,11 @@ export class 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 @@ -585,7 +596,8 @@ export class Document { const ctx = { anchors: {}, doc: this, - indent: '' + indent: '', + indentStep: ' '.repeat(indentSize) } let chompKeep = false let contentComment = null diff --git a/src/index.js b/src/index.js index e7599d50..4835b33d 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,8 @@ import { warn } from './warnings' const defaultOptions = { anchorPrefix: 'a', customTags: null, + indent: 2, + indentSeq: true, keepCstNodes: false, keepNodeTypes: true, keepBlobsInJSON: true, diff --git a/src/schema/Collection.js b/src/schema/Collection.js index 1291cfa6..9ba1e169 100644 --- a/src/schema/Collection.js +++ b/src/schema/Collection.js @@ -1,6 +1,6 @@ import { addComment } from '../addComment' +import { Type } from '../constants' import { Node } from './Node' -import { Pair } from './Pair' import { Scalar } from './Scalar' function collectionFromPath(schema, path, value) { @@ -66,7 +66,7 @@ export class Collection extends Node { hasAllNullValues() { return this.items.every(node => { - if (!(node instanceof Pair)) return false + if (!node || node.type !== 'PAIR') return false const n = node.value return ( n == null || @@ -112,10 +112,10 @@ export class Collection extends Node { onComment, onChompKeep ) { - const { doc, indent } = ctx + const { doc, indent, indentStep } = ctx const inFlow = - (this.type && this.type.substr(0, 4) === 'FLOW') || ctx.inFlow - if (inFlow) itemIndent += ' ' + this.type === Type.FLOW_MAP || this.type === Type.FLOW_SEQ || ctx.inFlow + if (inFlow) itemIndent += indentStep const allNullValues = isMap && this.hasAllNullValues() ctx = Object.assign({}, ctx, { allNullValues, @@ -176,7 +176,7 @@ export class Collection extends Node { ) { str = start for (const s of strings) { - str += s ? `\n ${indent}${s}` : '\n' + str += s ? `\n${indentStep}${indent}${s}` : '\n' } str += `\n${indent}${end}` } else { diff --git a/src/schema/Pair.js b/src/schema/Pair.js index 64ffccb0..3d2aa576 100644 --- a/src/schema/Pair.js +++ b/src/schema/Pair.js @@ -6,6 +6,7 @@ import { toJSON } from '../toJSON' import { Collection } from './Collection' import { Node } from './Node' import { Scalar } from './Scalar' +import { YAMLSeq } from './Seq' const stringifyKey = (key, jsKey, ctx) => { if (jsKey === null) return '' @@ -15,6 +16,7 @@ const stringifyKey = (key, jsKey, ctx) => { anchors: {}, doc: ctx.doc, indent: '', + indentStep: ctx.indentStep, inFlow: true, inStringifyKey: true }) @@ -64,7 +66,7 @@ export class Pair extends Node { toString(ctx, onComment, onChompKeep) { if (!ctx || !ctx.doc) return JSON.stringify(this) - const { simpleKeys } = ctx.doc.options + const { indent: indentSize, indentSeq, simpleKeys } = ctx.doc.options let { key, value } = this let keyComment = key instanceof Node && key.comment if (simpleKeys) { @@ -83,10 +85,10 @@ export class Pair extends Node { key instanceof Collection || key.type === Type.BLOCK_FOLDED || key.type === Type.BLOCK_LITERAL) - const { doc, indent } = ctx + const { doc, indent, indentStep } = ctx ctx = Object.assign({}, ctx, { implicitKey: !explicitKey, - indent: indent + ' ' + indent: indent + indentStep }) let chompKeep = false let str = doc.schema.stringify( @@ -125,6 +127,19 @@ export class Pair extends Node { if (!explicitKey && !this.comment && value instanceof Scalar) ctx.indentAtStart = str.length + 1 chompKeep = false + if ( + !indentSeq && + indentSize >= 2 && + !ctx.inFlow && + !explicitKey && + value instanceof YAMLSeq && + value.type !== Type.FLOW_SEQ && + !value.tag && + !doc.anchors.getName(value) + ) { + // If indentSeq === false, consider '- ' as part of indentation where possible + ctx.indent = ctx.indent.substr(2) + } const valueStr = doc.schema.stringify( value, ctx, diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js index 82351a66..6eb5bd88 100644 --- a/tests/doc/stringify.js +++ b/tests/doc/stringify.js @@ -522,3 +522,103 @@ describe('sortMapEntries', () => { expect(String(doc)).toBe('a: 1\nb: 2\nbb: 4\nc: 3\n') }) }) + +describe('custom indent', () => { + let obj + beforeEach(() => { + const seq = YAML.createNode(['a']) + seq.commentBefore = 'sc' + const map = YAML.createNode({ foo: 'bar', seq }) + map.commentBefore = 'mc' + obj = { array: [{ a: 1, b: 2 }], map } + }) + + test('indent: 0', () => { + expect(() => YAML.stringify(obj, { indent: 0 })).toThrow( + /must be a positive integer/ + ) + }) + + test('indent: 1', () => { + expect(YAML.stringify(obj, { indent: 1 })).toBe( + source` + array: + - a: 1 + b: 2 + map: + #mc + foo: bar + seq: + #sc + - a + ` + '\n' + ) + }) + + test('indent: 4', () => { + expect(YAML.stringify(obj, { indent: 4 })).toBe( + source` + array: + - a: 1 + b: 2 + map: + #mc + foo: bar + seq: + #sc + - a + ` + '\n' + ) + }) +}) + +describe('indentSeq: false', () => { + let obj + beforeEach(() => { + const seq = YAML.createNode(['a']) + seq.commentBefore = 'sc' + obj = { array: [{ a: 1, b: 2 }], map: { seq } } + }) + + test('indent: 1', () => { + expect(YAML.stringify(obj, { indent: 1, indentSeq: false })).toBe( + source` + array: + - a: 1 + b: 2 + map: + seq: + #sc + - a + ` + '\n' + ) + }) + + test('indent: 2', () => { + expect(YAML.stringify(obj, { indent: 2, indentSeq: false })).toBe( + source` + array: + - a: 1 + b: 2 + map: + seq: + #sc + - a + ` + '\n' + ) + }) + + test('indent: 4', () => { + expect(YAML.stringify(obj, { indent: 4, indentSeq: false })).toBe( + source` + array: + - a: 1 + b: 2 + map: + seq: + #sc + - a + ` + '\n' + ) + }) +})