diff --git a/docs-slate b/docs-slate index 413d60f7..8cd36bfd 160000 --- a/docs-slate +++ b/docs-slate @@ -1 +1 @@ -Subproject commit 413d60f7fdbf95e379b7ff904bfee3a46e370716 +Subproject commit 8cd36bfde1cd8808e8b52b2b6ea913771ab2442a diff --git a/docs/03_options.md b/docs/03_options.md index 625c709a..1b930029 100644 --- a/docs/03_options.md +++ b/docs/03_options.md @@ -30,6 +30,7 @@ Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `new Composer()`, | prettyErrors | `boolean` | `true` | Include line/col position in errors, along with an extract of the source string. | | strict | `boolean` | `true` | When parsing, do not ignore errors [required](#silencing-errors-and-warnings) by the YAML 1.2 spec, but caused by unambiguous content. | | stringKeys | `boolean` | `false` | Parse all mapping keys as strings. Treat all non-scalar keys as errors. | +| keepIndentStep | `boolean` | `false` | Include an `indentStep` value on each parsed `Pair`, preserving the original indentation between keys and values in block maps. | | uniqueKeys | `boolean ⎮ (a, b) => boolean` | `true` | Whether key uniqueness is checked, or customised. If set to be a function, it will be passed two parsed nodes and should return a boolean value indicating their equality. | [bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index ddcb8db2..9c4ad9b5 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -52,6 +52,7 @@ On the other hand, `!!int` and `!!float` stringifiers will take `format` into ac class Pair { key: Node value: Node | null + indentStep?: number // set when parsed with keepIndentStep: true } class Collection implements NodeBase { diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 8cdb03ff..cb27f184 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -116,6 +116,12 @@ export function resolveBlockMap( offset = valueNode.range![2] const pair = new Pair(keyNode, valueNode) if (ctx.options.keepSourceTokens) pair.srcToken = collItem + if (ctx.options.keepIndentStep) { + pair.indentStep = + valueProps.hasNewline && value && 'indent' in value + ? value.indent - bm.indent + : 0 + } map.items.push(pair) } else { // key with no value diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index cecdc280..274e6c07 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -229,4 +229,4 @@ export function resolveFlowCollection( } return coll -} +} \ No newline at end of file diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts index 9296543a..f59e91a8 100644 --- a/src/nodes/Pair.ts +++ b/src/nodes/Pair.ts @@ -18,6 +18,14 @@ export class Pair< /** The CST token that was composed into this pair. */ declare srcToken?: CollectionItem + /** + * The number of spaces of indentation between the key and the value. + * + * If the value is in flow / on the same line as the key, + * the indentStep will be 0. + */ + indentStep?: number + constructor(key: NodeOf, value: NodeOf | null = null) { this.key = key this.value = value @@ -46,4 +54,4 @@ export class Pair< ? stringifyPair(this, ctx, onComment, onChompKeep) : JSON.stringify(this) } -} +} \ No newline at end of file diff --git a/src/options.ts b/src/options.ts index 4aeed603..e4d4dfe3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -68,6 +68,17 @@ export type ParseOptions = { * Default: `true` */ uniqueKeys?: boolean | ((a: Node, b: Node) => boolean) + + /** + * Include a `indentStep` value on each parsed `Pair`. + * + * This helps keep the original indentation and formatting after making + * transformations to the document. Currently, this only adds formatting + * information to Pairs found in block maps. + * + * Default: `false` + */ + keepIndentStep?: boolean } export type DocumentOptions = { diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts index bedae69f..21170d45 100644 --- a/src/stringify/stringify.ts +++ b/src/stringify/stringify.ts @@ -168,4 +168,4 @@ export function stringify( return node instanceof Scalar || str[0] === '{' || str[0] === '[' ? `${props} ${str}` : `${props}\n${ctx.indent}${str}` -} +} \ No newline at end of file diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts index 109cabbf..b996e315 100644 --- a/src/stringify/stringifyPair.ts +++ b/src/stringify/stringifyPair.ts @@ -7,17 +7,24 @@ import { stringify } from './stringify.ts' import { indentComment, lineComment } from './stringifyComment.ts' export function stringifyPair( - { key, value }: Readonly, + { key, value, indentStep: pairIndentStep }: Readonly, ctx: StringifyContext, onComment?: () => void, onChompKeep?: () => void ): string { - const { + let { indent, indentStep, noValues, options: { commentString, indentSeq, simpleKeys } } = ctx + + // Use the indentStep that is on the Pair by default, + // since that is preserved from the original document. + if (pairIndentStep !== undefined) { + indentStep = ' '.repeat(pairIndentStep) + } + if (simpleKeys) { if (key.comment) { throw new Error('With simple keys, key nodes cannot have comments') @@ -153,7 +160,8 @@ export function stringifyPair( } if (sp0 === -1 || nl0 < sp0) hasPropsLine = true } - if (!hasPropsLine) ws = `\n${ctx.indent}` + if (!hasPropsLine && (indentStep.length > 0 || !value.flow)) + ws = `\n${ctx.indent}` } } else if (valueStr === '' || valueStr[0] === '\n') { ws = '' @@ -169,4 +177,4 @@ export function stringifyPair( } return str -} +} \ No newline at end of file diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index 48d0710a..adce81fb 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -1036,6 +1036,179 @@ describe('custom indent', () => { }) }) +describe('keepIndentStep: true', () => { + test('keep object indentation', () => { + const src = source` + key: + super: indented + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('keep array indentation', () => { + const src = source` + key: + - name: foo + - name: bar + - name: bam + - name: baz + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('keep complex indentation', () => { + const src = source` + key: + super: + - name: foo + - name: bar + - name: bam + - name: baz + metadata: + foo: why + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('handles multiline seq flow on same line', () => { + const src = source` + key: [ + aaaaaaaa, + bbbbbbbb, + cccccccc, + dddddddd, + eeeeeeee, + ffffffff, + gggggggg, + hhhhhhhh + ] + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + test('handles multiline flow on same line', () => { + const src = source` + key: !tag { + one: aaaaaaaa, + two: bbbbbbbb, + three: cccccccc, + four: dddddddd, + five: eeeeeeee + } + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('handles multiline flow on next line', () => { + const src = source` + key: + !tag { + one: aaaaaaaa, + two: bbbbbbbb, + three: cccccccc, + four: dddddddd, + five: eeeeeeee + } + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('handles multiline flow on next line', () => { + const src = source` + key: + !tag { + one: aaaaaaaa, + two: bbbbbbbb, + three: cccccccc, + four: dddddddd, + five: eeeeeeee + } + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('handles explicit key map with no indent', () => { + const src = source` + foo: + ? [ key ] + : !tag + - foo + - bar + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('handles explicit key map with indent', () => { + const src = source` + foo: + ? [ key ] + : !tag + - foo + - bar + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('comment after key, value on next line', () => { + const src = source` + key: # comment + value: indented + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe( + source` + key: + # comment + value: indented + ` + ) + }) + + test('comment on value line', () => { + const src = source` + key: + value: indented # comment + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('comment before nested key at value indent level', () => { + const src = source` + key: + # comment + super: indented + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + expect(doc.toString()).toBe(src) + }) + + test('nesting parsed node under a new key preserves inner indentation', () => { + const src = source` + a: + b: 1 + c: 2 + ` + const doc = YAML.parseDocument(src, { keepIndentStep: true }) + const wrapper = YAML.parseDocument('outer: null') + wrapper.set('outer', doc.value) + expect(wrapper.toString()).toBe(source` + outer: + a: + b: 1 + c: 2 + `) + }) +}) + describe('indentSeq: false', () => { let obj: unknown beforeEach(() => {