setScalarValue reconstructed a block scalar's content indent as token.indent plus one step, but token.indent is the indent of the node, not of its contents. Contents sitting further in than one step were pulled back, and without afterKey they lost their indentation entirely, which for a block scalar in a collection changes what the document means rather than just how it looks. resolveBlockScalar already measures the real content indent when reading: an explicit indentation indicator counts from the node indent, and otherwise the indent is that of the first non-empty line. Measure the same way before stringifying, and fall back to the previous behaviour when a block scalar has no contents to measure. Fixes #349
276 lines
6.8 KiB
TypeScript
276 lines
6.8 KiB
TypeScript
import { CST, Parser } from 'yaml'
|
|
import { source } from './_utils.ts'
|
|
|
|
function cstDoc(src: string) {
|
|
const tokens = Array.from(new Parser().parse(src))
|
|
expect(tokens).toHaveLength(1)
|
|
expect(tokens[0].type).toBe('document')
|
|
return tokens[0] as CST.Document
|
|
}
|
|
|
|
describe('CST.visit', () => {
|
|
test('Visit paths in order', () => {
|
|
const doc = cstDoc(source`
|
|
foo:
|
|
- baz
|
|
- [ bar: 42 ]
|
|
? { fuzz, ball }
|
|
: fii
|
|
`)
|
|
const paths: CST.VisitPath[] = []
|
|
CST.visit(doc, (_item, path) => {
|
|
paths.push(path)
|
|
})
|
|
expect(paths).toMatchObject([
|
|
[],
|
|
[['value', 0]],
|
|
[
|
|
['value', 0],
|
|
['value', 0]
|
|
],
|
|
[
|
|
['value', 0],
|
|
['value', 1]
|
|
],
|
|
[
|
|
['value', 0],
|
|
['value', 1],
|
|
['value', 0]
|
|
],
|
|
[['value', 1]],
|
|
[
|
|
['value', 1],
|
|
['key', 0]
|
|
],
|
|
[
|
|
['value', 1],
|
|
['key', 1]
|
|
]
|
|
])
|
|
})
|
|
|
|
test('break visit', () => {
|
|
const doc = cstDoc(source`
|
|
- " foo"
|
|
- "foo"
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'foo') return CST.visit.BREAK
|
|
})
|
|
expect(visits).toBe(3)
|
|
})
|
|
|
|
test('remove item', () => {
|
|
const doc = cstDoc(source`
|
|
- " foo"
|
|
- "foo"
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'foo') return CST.visit.REMOVE
|
|
})
|
|
expect(visits).toBe(4)
|
|
expect(CST.stringify(doc)).toBe(source`
|
|
- " foo"
|
|
- "bar"
|
|
`)
|
|
})
|
|
|
|
test('replace value with block scalar in seq', () => {
|
|
const doc = cstDoc(source`
|
|
- " foo"
|
|
- "foo"
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
if (item.value) {
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'foo')
|
|
CST.setScalarValue(item.value, 'foo', { type: 'BLOCK_LITERAL' })
|
|
}
|
|
})
|
|
expect(visits).toBe(4)
|
|
expect(CST.stringify(doc)).toBe(source`
|
|
- " foo"
|
|
- |-
|
|
foo
|
|
- "bar"
|
|
`)
|
|
})
|
|
|
|
test('add item', () => {
|
|
const doc = cstDoc(source`
|
|
- " foo"
|
|
- "foo"
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, (item, path) => {
|
|
visits += 1
|
|
if (CST.isScalar(item.value)) {
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'foo') {
|
|
const parent = CST.visit.parentCollection(doc, path)
|
|
const idx = path[path.length - 1][1]
|
|
const { indent } = item.value
|
|
parent.items.splice(idx, 0, {
|
|
start: item.start.slice(),
|
|
value: CST.createScalarToken('hip', { indent })
|
|
})
|
|
return idx + 2
|
|
}
|
|
}
|
|
})
|
|
expect(visits).toBe(4)
|
|
expect(CST.stringify(doc)).toBe(source`
|
|
- " foo"
|
|
- hip
|
|
- "foo"
|
|
- "bar"
|
|
`)
|
|
})
|
|
|
|
test('replace value with flow scalar in map', () => {
|
|
const doc = cstDoc(source`
|
|
- a: A
|
|
b: B\t#comment
|
|
c: C
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
if (item.value) {
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'B') {
|
|
CST.setScalarValue(item.value, 'foo\n\nbar', { afterKey: !!item.key })
|
|
}
|
|
}
|
|
})
|
|
expect(visits).toBe(5)
|
|
expect(CST.stringify(doc)).toBe(source`
|
|
- a: A
|
|
b: foo
|
|
|
|
|
|
bar\t#comment
|
|
c: C
|
|
`)
|
|
})
|
|
|
|
test('skip children', () => {
|
|
const doc = cstDoc(source`
|
|
- " foo"
|
|
- [ foo, 13, 42 ]
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
if (item.value?.type === 'flow-collection') return CST.visit.SKIP
|
|
})
|
|
expect(visits).toBe(4)
|
|
})
|
|
|
|
test('set next index', () => {
|
|
const doc = cstDoc(source`
|
|
- "foo"
|
|
- [ foo, 13, 42 ]
|
|
- "bar"
|
|
`)
|
|
let visits = 0
|
|
CST.visit(doc, item => {
|
|
visits += 1
|
|
const scalar = CST.resolveAsScalar(item.value)
|
|
if (scalar?.value === 'foo') return 2
|
|
})
|
|
expect(visits).toBe(3)
|
|
})
|
|
})
|
|
|
|
test('Line comment before unindented block-seq in block-map (#525)', () => {
|
|
const src = 'a:\n#\n- b'
|
|
const [doc] = Array.from(new Parser().parse(src))
|
|
expect(CST.stringify(doc)).toBe(src)
|
|
})
|
|
|
|
describe('CST.setScalarValue block scalar indent (#349)', () => {
|
|
function setBlockScalar(src: string, value: string, afterKey = true) {
|
|
const doc = cstDoc(src)
|
|
let found: CST.BlockScalar | null = null
|
|
CST.visit(doc, item => {
|
|
if (item.value?.type === 'block-scalar') {
|
|
found = item.value
|
|
CST.setScalarValue(item.value, value, {
|
|
afterKey: afterKey && !!item.key
|
|
})
|
|
}
|
|
})
|
|
expect(found).not.toBe(null)
|
|
return {
|
|
src: CST.stringify(doc),
|
|
token: found as unknown as CST.BlockScalar
|
|
}
|
|
}
|
|
|
|
test('keeps content indented by more than one step', () => {
|
|
const src = 'key:\n |\n line one\n line two\n'
|
|
expect(setBlockScalar(src, 'new one\nnew two\n').src).toBe(
|
|
'key:\n |\n new one\n new two\n'
|
|
)
|
|
})
|
|
|
|
test('keeps content indent when not marked as being after a key', () => {
|
|
const src = 'key: |\n line one\n'
|
|
expect(setBlockScalar(src, 'new one\n', false).src).toBe(
|
|
'key: |\n new one\n'
|
|
)
|
|
})
|
|
|
|
test('keeps the indent of a nested block scalar', () => {
|
|
const src = 'a:\n b: |\n deep\n'
|
|
expect(setBlockScalar(src, 'new\n').src).toBe('a:\n b: |\n new\n')
|
|
})
|
|
|
|
test('keeps the indent given by an explicit indentation indicator', () => {
|
|
const src = 'key: |2\n line one\n'
|
|
expect(setBlockScalar(src, 'new one\n').src).toBe('key: |\n new one\n')
|
|
})
|
|
|
|
test('keeps the indent when the first content line is empty', () => {
|
|
const src = 'key: |\n\n line one\n'
|
|
expect(setBlockScalar(src, 'new one\n').src).toBe('key: |\n new one\n')
|
|
})
|
|
|
|
test('leaves an indent of one step alone', () => {
|
|
const src = 'key: |\n line one\n'
|
|
expect(setBlockScalar(src, 'new one\n').src).toBe('key: |\n new one\n')
|
|
})
|
|
|
|
test('resolves back to the value that was set', () => {
|
|
const src = 'key:\n |\n line one\n line two\n'
|
|
const { token } = setBlockScalar(src, 'new one\nnew two\n')
|
|
expect(CST.resolveAsScalar(token).value).toBe('new one\nnew two\n')
|
|
})
|
|
|
|
test('still indents a block scalar replacing a flow scalar', () => {
|
|
const doc = cstDoc('key: "old"\n')
|
|
CST.visit(doc, item => {
|
|
if (item.value && CST.isScalar(item.value))
|
|
CST.setScalarValue(item.value, 'new one\nnew two\n', {
|
|
afterKey: !!item.key,
|
|
type: 'BLOCK_LITERAL'
|
|
})
|
|
})
|
|
expect(CST.stringify(doc)).toBe('key: |\n new one\n new two\n')
|
|
})
|
|
})
|