diff --git a/docs/07_parsing_yaml.md b/docs/07_parsing_yaml.md index b8df5a2c..2176eb56 100644 --- a/docs/07_parsing_yaml.md +++ b/docs/07_parsing_yaml.md @@ -187,10 +187,17 @@ Some of the most common node properties include: | `offset` | `number` | The start index within the source string or character stream. | | `source` | `string` | A raw string representation of the node's value, including all newlines and indentation. | | `indent` | `number` | The indent level of the current line; mostly just for internal use. | -| `items` | `{ ... }[]` | The contents of a collection; shape depends on the collection type, and may include `key: Token` and `value: Token`. | +| `items` | `Item[]` | The contents of a collection; exact shape depends on the collection type. | | `start`, `sep`, `end` | `SourceToken[]` | Content before, within, and after "actual" values. Includes item and collection indicators, anchors, tags, comments, as well as other things. | -As an implementation detail, block and flow collections are parsed and presented rather differently due to their structural differences. +Collection items contain some subset of the following properties: + +| Item property | Type | Description | +| ------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `start` | `SourceToken[]` | Always defined. Content before the actual value. May include comments that are later assigned to the preceding item. | +| `key` | `Token ⎮ null` | Set for key/value pairs only, so never used in block sequences. | +| `sep` | `SourceToken[]` | Content between the key and the value. If defined, indicates that the `key` logically exists, even if its value is `null`. | +| `value` | `Token ⎮ null` | The value. Normally set, but may be left out for e.g. explicit keys with no matching value. | ### Counting Lines diff --git a/docs/08_errors.md b/docs/08_errors.md index 0a67920c..28e95920 100644 --- a/docs/08_errors.md +++ b/docs/08_errors.md @@ -28,6 +28,7 @@ To identify errors for special handling, you should primarily use `code` to diff | `BAD_DIRECTIVE` | Only the `%YAML` and `%TAG` directives are supported, and they need to follow the specified strucutre. | | `BAD_DQ_ESCAPE` | Double-quotes strings may include `\` escaped content, but that needs to be valid. | | `BAD_INDENT` | Indentation is important in YAML, and collection items need to all start at the same level. Block scalars are also picky about their leading content. | +| `BAD_PROP_ORDER` | Anchors and tags must be placed after the `?`, `:` and `-` indicators. | | `BAD_SCALAR_START` | Plain scalars cannot start with a block scalar indicator, or one of the two reserved characters: `@` and `. To fix, use a block or quoted scalar for the value. | | `BLOCK_AS_IMPLICIT_KEY` | There's probably something wrong with the indentation, or you're trying to parse something like `a: b: c`, where it's not clear what's the key and what's the value. | | `BLOCK_IN_FLOW` | YAML scalars and collections both have block and flow styles. Flow is allowed within block, but not the other way around. | @@ -40,7 +41,6 @@ To identify errors for special handling, you should primarily use `code` to diff | `MULTIPLE_ANCHORS` | A node is only allowed to have one anchor. | | `MULTIPLE_DOCS` | A YAML stream may include multiple documents. If yours does, you'll need to use `parseAllDocuments()` to work with it. | | `MULTIPLE_TAGS` | A node is only allowed to have one tag. | -| `PROP_BEFORE_SEP` | For an explicit key, anchors and tags must be after the `?` indicator | | `TAB_AS_INDENT` | Only spaces are allowed as indentation. | | `TAG_RESOLVE_FAILED` | Something went wrong when resolving a node's tag with the current schema. | | `UNEXPECTED_TOKEN` | A token was encountered in a place where it wasn't expected. | diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index bd142de1..aadaa299 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -1,11 +1,12 @@ -import { isNode, isPair, ParsedNode } from '../nodes/Node.js' +import { isPair } 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 { FlowCollection } from '../parse/tokens.js' import type { ComposeContext, ComposeNode } from './compose-node.js' import type { ComposeErrorHandler } from './composer.js' import { resolveEnd } from './resolve-end.js' +import { resolveProps } from './resolve-props.js' import { containsNewline } from './util-contains-newline.js' export function resolveFlowCollection( @@ -15,244 +16,198 @@ export function resolveFlowCollection( onError: ComposeErrorHandler ) { const isMap = fc.start.source === '{' - const coll = isMap ? new YAMLMap(ctx.schema) : new YAMLSeq(ctx.schema) + const fcName = isMap ? 'flow map' : 'flow sequence' + const coll = isMap + ? (new YAMLMap(ctx.schema) as YAMLMap.Parsed) + : (new YAMLSeq(ctx.schema) as YAMLSeq.Parsed) coll.flow = true - let key: ParsedNode | null = null - let value: ParsedNode | null = null - - let spaceBefore = false - let comment = '' - let hasSpace = false - let newlines = '' - let anchor = '' - let tagName = '' - - let offset = fc.offset + 1 - let atLineStart = false - let atExplicitKey = false - let atValueEnd = false - let nlAfterValueInSeq = false - let seqKeyToken: Token | null = null - - function getProps() { - const props = { spaceBefore, comment, anchor, tagName } - - spaceBefore = false - comment = '' - newlines = '' - anchor = '' - tagName = '' - - return props - } - - function addItem(pos: number) { - if (value) { - if (comment) value.comment = comment - } else { - value = composeEmptyNode(ctx, offset, fc.items, pos, getProps(), onError) - } - if (isMap || atExplicitKey) { - coll.items.push(key ? new Pair(key, value) : new Pair(value)) - } else { - const seq = coll as YAMLSeq - if (key) { - const map = new YAMLMap(ctx.schema) - map.flow = true - map.items.push(new Pair(key, value)) - seq.items.push(map) - } else seq.items.push(value) - } - } - + let offset = fc.offset for (let i = 0; i < fc.items.length; ++i) { - const token = fc.items[i] - let isSourceToken = true - switch (token.type) { - case 'space': - hasSpace = true - break - case 'comment': { - if (ctx.options.strict && !hasSpace) + const { start, key, sep, value } = fc.items[i] + const props = resolveProps(start, { + ctx, + flow: fcName, + indicator: 'explicit-key-ind', + offset, + onError, + startOnNewline: false + }) + if (!props.found) { + if (!props.anchor && !props.tagName && !sep && !value) { + if (i === 0 && props.comma) onError( - offset, - 'COMMENT_SPACE', - 'Comments must be separated from other tokens by white space characters' + props.comma.offset, + 'UNEXPECTED_TOKEN', + `Unexpected , in ${fcName}` ) - const cb = token.source.substring(1) - if (!comment) comment = cb - else comment += newlines + cb - atLineStart = false - newlines = '' - break - } - case 'newline': - if (atLineStart && !comment) spaceBefore = true - if (atValueEnd) { - if (comment) { - let node = coll.items[coll.items.length - 1] - if (isPair(node)) node = node.value || node.key - /* istanbul ignore else should not happen */ - if (isNode(node)) node.comment = comment - else - onError( - offset, - 'IMPOSSIBLE', - 'Error adding trailing comment to node' - ) - comment = '' - } - atValueEnd = false - } else { - newlines += token.source - if (!isMap && !key && value) nlAfterValueInSeq = true - } - atLineStart = true - hasSpace = true - break - case 'anchor': - if (anchor) + else if (i < fc.items.length - 1) onError( - offset, - 'MULTIPLE_ANCHORS', - 'A node can have at most one anchor' + props.start, + 'UNEXPECTED_TOKEN', + `Unexpected empty item in ${fcName}` ) - anchor = token.source.substring(1) - atLineStart = false - atValueEnd = false - hasSpace = false - break - case 'tag': { - if (tagName) - onError(offset, 'MULTIPLE_TAGS', 'A node can have at most one tag') - const tn = ctx.directives.tagName(token.source, m => - onError(offset, 'TAG_RESOLVE_FAILED', m) - ) - if (tn) tagName = tn - atLineStart = false - atValueEnd = false - hasSpace = false - break + if (props.comment) { + if (coll.comment) coll.comment += '\n' + props.comment + else coll.comment = props.comment + } + continue } - case 'explicit-key-ind': - if (anchor || tagName) - onError( - offset, - 'PROP_BEFORE_SEP', - 'Anchors and tags must be after the ? indicator' - ) - atExplicitKey = true - atLineStart = false - atValueEnd = false - hasSpace = false - break - case 'map-value-ind': { - if (key) { - if (value) { - onError( - offset, - 'BLOCK_AS_IMPLICIT_KEY', - 'Missing {} around pair used as mapping key' - ) - const map = new YAMLMap(ctx.schema) - map.flow = true - map.items.push(new Pair(key, value)) - map.range = [key.range[0], value.range[1]] - key = map as YAMLMap.Parsed - value = null - } // else explicit key - } else if (value) { - if (ctx.options.strict) { - const slMsg = - 'Implicit keys of flow sequence pairs need to be on a single line' - if (nlAfterValueInSeq) - onError(offset, 'MULTILINE_IMPLICIT_KEY', slMsg) - else if (seqKeyToken) { - if (containsNewline(seqKeyToken)) - onError(offset, 'MULTILINE_IMPLICIT_KEY', slMsg) - if (seqKeyToken.offset < offset - 1024) - onError( - offset, - 'KEY_OVER_1024_CHARS', - 'The : indicator must be at most 1024 chars after the start of an implicit flow sequence key' - ) - seqKeyToken = null - } + if (!isMap && ctx.options.strict && containsNewline(key)) + onError( + props.start, + 'MULTILINE_IMPLICIT_KEY', + 'Implicit keys of flow sequence pairs need to be on a single line' + ) + } + if (i === 0) { + if (props.comma) + onError( + props.comma.offset, + 'UNEXPECTED_TOKEN', + `Unexpected , in ${fcName}` + ) + } else { + if (!props.comma) + onError( + props.start, + 'MISSING_CHAR', + `Missing , between ${fcName} items` + ) + if (props.comment) { + let prevItemComment = '' + loop: for (const st of start) { + switch (st.type) { + case 'comma': + case 'space': + break + case 'comment': + prevItemComment = st.source.substring(1) + break loop + default: + break loop } - key = value - value = null - } else { - key = composeEmptyNode(ctx, offset, fc.items, i, getProps(), onError) } - if (comment) { - key.comment = comment - comment = '' + if (prevItemComment) { + let prev = coll.items[coll.items.length - 1] + if (isPair(prev)) prev = prev.value || prev.key + if (prev.comment) prev.comment += '\n' + prevItemComment + else prev.comment = prevItemComment + props.comment = props.comment.substring(prevItemComment.length + 1) } - atExplicitKey = false - atValueEnd = false - hasSpace = false - break } - case 'comma': - if (key || value || anchor || tagName || atExplicitKey) addItem(i) - else - onError( - offset, - 'UNEXPECTED_TOKEN', - `Unexpected , in flow ${isMap ? 'map' : 'sequence'}` - ) - key = null - value = null - atExplicitKey = false - atValueEnd = true - hasSpace = false - nlAfterValueInSeq = false - seqKeyToken = null - break - case 'block-map': - case 'block-seq': + } + + for (const token of [key, value]) + if (token && (token.type === 'block-map' || token.type === 'block-seq')) onError( - offset, + token.offset, 'BLOCK_IN_FLOW', 'Block collections are not allowed within flow collections' ) - // fallthrough - default: { - if (value) + + if (!isMap && !sep && !props.found) { + // item is a value in a seq + // → key & sep are empty, start does not include ? or : + const valueNode = value + ? composeNode(ctx, value, props, onError) + : composeEmptyNode(ctx, props.end, sep, null, props, onError) + ;(coll as YAMLSeq).items.push(valueNode) + offset = valueNode.range[1] + } else { + // item is a key+value pair + + // key value + const keyStart = props.end + const keyNode = key + ? composeNode(ctx, key, props, onError) + : composeEmptyNode(ctx, keyStart, start, null, props, onError) + + // value properties + const valueProps = resolveProps(sep || [], { + ctx, + flow: fcName, + indicator: 'map-value-ind', + offset: keyNode.range[1], + onError, + startOnNewline: false + }) + + if (valueProps.found) { + if (!isMap && !props.found && ctx.options.strict) { + if (sep) + for (const st of sep) { + if (st === valueProps.found) break + if (st.type === 'newline') { + onError( + st.offset, + 'MULTILINE_IMPLICIT_KEY', + 'Implicit keys of flow sequence pairs need to be on a single line' + ) + break + } + } + if (props.start < valueProps.found.offset - 1024) + onError( + valueProps.found.offset, + 'KEY_OVER_1024_CHARS', + 'The : indicator must be at most 1024 chars after the start of an implicit flow sequence key' + ) + } + } else if (value) { + if ('source' in value && value.source && value.source[0] === ':') + onError( + value.offset, + 'MISSING_CHAR', + `Missing space after : in ${fcName}` + ) + else onError( - offset, + valueProps.start, 'MISSING_CHAR', - 'Missing , between flow collection items' + `Missing , or : between ${fcName} items` ) - if (!isMap && !key && !atExplicitKey) seqKeyToken = token - value = composeNode(ctx, token, getProps(), onError) - offset = value.range[1] - atLineStart = false - isSourceToken = false - atValueEnd = false - hasSpace = false } + + // value value + const valueNode = value + ? composeNode(ctx, value, valueProps, onError) + : valueProps.found + ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError) + : null + if (!valueNode && valueProps.comment) { + if (keyNode.comment) keyNode.comment += '\n' + valueProps.comment + else keyNode.comment = valueProps.comment + } + + const pair = new Pair(keyNode, valueNode) + if (isMap) (coll as YAMLMap.Parsed).items.push(pair) + else { + const map = new YAMLMap(ctx.schema) + map.flow = true + map.items.push(pair) + ;(coll as YAMLSeq).items.push(map) + } + offset = valueNode ? valueNode.range[1] : valueProps.end } - if (isSourceToken) offset += (token as SourceToken).source.length } - if (key || value || anchor || tagName || atExplicitKey) - addItem(fc.items.length) const expectedEnd = isMap ? '}' : ']' const [ce, ...ee] = fc.end if (!ce || ce.source !== expectedEnd) { - const cs = isMap ? 'map' : 'sequence' onError( - offset, + offset + 1, 'MISSING_CHAR', - `Expected flow ${cs} to end with ${expectedEnd}` + `Expected ${fcName} to end with ${expectedEnd}` ) } if (ce) offset += ce.source.length if (ee.length > 0) { const end = resolveEnd(ee, offset, ctx.options.strict, onError) - if (end.comment) coll.comment = comment + if (end.comment) { + if (coll.comment) coll.comment += '\n' + end.comment + else coll.comment = end.comment + } offset = end.offset } diff --git a/src/compose/resolve-props.ts b/src/compose/resolve-props.ts index 5eb34441..419c27b4 100644 --- a/src/compose/resolve-props.ts +++ b/src/compose/resolve-props.ts @@ -4,6 +4,7 @@ import type { ComposeErrorHandler } from './composer.js' export interface ResolvePropsArg { ctx: ComposeContext + flow?: string indicator: 'doc-start' | 'explicit-key-ind' | 'map-value-ind' | 'seq-item-ind' offset: number onError: ComposeErrorHandler @@ -12,7 +13,7 @@ export interface ResolvePropsArg { export function resolveProps( tokens: SourceToken[], - { ctx, indicator, offset, onError, startOnNewline }: ResolvePropsArg + { ctx, flow, indicator, offset, onError, startOnNewline }: ResolvePropsArg ) { let spaceBefore = false let atNewline = startOnNewline @@ -22,14 +23,21 @@ export function resolveProps( let hasNewline = false let anchor = '' let tagName = '' + let comma: SourceToken | null = null let found: SourceToken | null = null let start: number | null = null for (const token of tokens) { switch (token.type) { case 'space': - // At the doc level, tabs at line start may be parsed as leading - // white space rather than indentation. - if (atNewline && indicator !== 'doc-start' && token.source[0] === '\t') + // At the doc level, tabs at line start may be parsed + // as leading white space rather than indentation. + // In a flow collection, only the parser handles indent. + if ( + !flow && + atNewline && + indicator !== 'doc-start' && + token.source[0] === '\t' + ) onError( token.offset, 'TAB_AS_INDENT', @@ -87,10 +95,26 @@ export function resolveProps( } case indicator: // Could here handle preceding comments differently + if (anchor || tagName) + onError( + token.offset, + 'BAD_PROP_ORDER', + `Anchors and tags must be after the ${token.source} indicator` + ) found = token atNewline = false hasSpace = false break + case 'comma': + if (flow) { + if (comma) + onError(token.offset, 'UNEXPECTED_TOKEN', `Unexpected , in ${flow}`) + comma = token + atNewline = false + hasSpace = false + break + } + // else fallthrough default: onError( token.offset, @@ -104,6 +128,7 @@ export function resolveProps( const last = tokens[tokens.length - 1] const end = last ? last.offset + last.source.length : offset return { + comma, found, spaceBefore, comment, diff --git a/src/compose/util-contains-newline.ts b/src/compose/util-contains-newline.ts index cd0c26ad..32cd8d3d 100644 --- a/src/compose/util-contains-newline.ts +++ b/src/compose/util-contains-newline.ts @@ -7,20 +7,16 @@ export function containsNewline(key: Token | null | undefined) { case 'scalar': case 'double-quoted-scalar': case 'single-quoted-scalar': - return key.source.includes('\n') + if (key.source.includes('\n')) return true + if (key.end) + for (const st of key.end) if (st.type === 'newline') return true + return false case 'flow-collection': - for (const token of key.items) { - switch (token.type) { - case 'newline': - return true - case 'alias': - case 'scalar': - case 'double-quoted-scalar': - case 'single-quoted-scalar': - case 'flow-collection': - if (containsNewline(token)) return true - break - } + for (const it of key.items) { + for (const st of it.start) if (st.type === 'newline') return true + if (it.sep) + for (const st of it.sep) if (st.type === 'newline') return true + if (containsNewline(it.key) || containsNewline(it.value)) return true } return false default: diff --git a/src/errors.ts b/src/errors.ts index 5bc3a7d9..97d34960 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -5,6 +5,7 @@ export type ErrorCode = | 'BAD_DIRECTIVE' | 'BAD_DQ_ESCAPE' | 'BAD_INDENT' + | 'BAD_PROP_ORDER' | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' @@ -17,7 +18,6 @@ export type ErrorCode = | 'MULTIPLE_ANCHORS' | 'MULTIPLE_DOCS' | 'MULTIPLE_TAGS' - | 'PROP_BEFORE_SEP' | 'TAB_AS_INDENT' | 'TAG_RESOLVE_FAILED' | 'UNEXPECTED_TOKEN' diff --git a/src/parse/parser.ts b/src/parse/parser.ts index e770dc18..b74220e3 100644 --- a/src/parse/parser.ts +++ b/src/parse/parser.ts @@ -66,7 +66,7 @@ function atFirstEmptyLineAfterComments(start: SourceToken[]) { } function isFlowToken( - token: Token | null + token: Token | null | undefined ): token is FlowScalar | FlowCollection { switch (token?.type) { case 'alias': @@ -116,6 +116,27 @@ function getFirstKeyStartProps(prev: SourceToken[]) { return prev.splice(i, prev.length) } +function fixFlowSeqItems(fc: FlowCollection) { + if (fc.start.type === 'flow-seq-start') { + for (const it of fc.items) { + if ( + it.sep && + !it.value && + !includesToken(it.start, 'explicit-key-ind') && + !includesToken(it.sep, 'map-value-ind') + ) { + if (it.key) it.value = it.key + delete it.key + if (isFlowToken(it.value)) { + if (it.value.end) Array.prototype.push.apply(it.value.end, it.sep) + else it.value.end = it.sep + } else Array.prototype.push.apply(it.start, it.sep) + delete it.sep + } + } + } +} + /** * A YAML concrete syntax tree (CST) parser * @@ -309,6 +330,7 @@ export class Parser { // For these, parent indent is needed instead of own if (token.type === 'block-scalar' || token.type === 'flow-collection') token.indent = 'indent' in top ? top.indent : -1 + if (token.type === 'flow-collection') fixFlowSeqItems(token) switch (top.type) { case 'document': top.value = token @@ -337,9 +359,14 @@ export class Parser { else it.value = token break } - case 'flow-collection': - top.items.push(token) - break + case 'flow-collection': { + const it = top.items[top.items.length - 1] + if (!it || it.value) + top.items.push({ start: [], key: token, sep: [] }) + else if (it.sep) it.value = token + else Object.assign(it, { key: token, sep: [] }) + return + } /* istanbul ignore next should not happen */ default: this.pop() @@ -652,31 +679,48 @@ export class Parser { } private flowCollection(fc: FlowCollection) { + const it = fc.items[fc.items.length - 1] if (this.type === 'flow-error-end') { - let top + let top: Token | undefined do { this.pop() top = this.peek(1) } while (top && top.type === 'flow-collection') } else if (fc.end.length === 0) { switch (this.type) { - case 'space': - case 'comment': - case 'newline': case 'comma': case 'explicit-key-ind': + if (!it || it.sep) fc.items.push({ start: [this.sourceToken] }) + else it.start.push(this.sourceToken) + return + case 'map-value-ind': + if (!it || it.value) + fc.items.push({ start: [], key: null, sep: [this.sourceToken] }) + else if (it.sep) it.sep.push(this.sourceToken) + else Object.assign(it, { key: null, sep: [this.sourceToken] }) + return + + case 'space': + case 'comment': + case 'newline': case 'anchor': case 'tag': - fc.items.push(this.sourceToken) + if (!it || it.value) fc.items.push({ start: [this.sourceToken] }) + else if (it.sep) it.sep.push(this.sourceToken) + else it.start.push(this.sourceToken) return case 'alias': case 'scalar': case 'single-quoted-scalar': - case 'double-quoted-scalar': - fc.items.push(this.flowScalar(this.type)) + case 'double-quoted-scalar': { + const fs = this.flowScalar(this.type) + if (!it || it.value) fc.items.push({ start: [], key: fs, sep: [] }) + else if (it.sep) this.stack.push(fs) + else Object.assign(it, { key: fs, sep: [] }) return + } case 'flow-map-end': case 'flow-seq-end': @@ -706,6 +750,7 @@ export class Parser { ) { const prev = getPrevProps(parent) const start = getFirstKeyStartProps(prev) + fixFlowSeqItems(fc) const sep = fc.end.splice(1, fc.end.length) sep.push(this.sourceToken) const map: BlockMap = { @@ -771,14 +816,18 @@ export class Parser { indent: this.indent, items: [{ start: [this.sourceToken] }] } as BlockSequence - case 'explicit-key-ind': + case 'explicit-key-ind': { this.onKeyLine = true + const prev = getPrevProps(parent) + const start = getFirstKeyStartProps(prev) + start.push(this.sourceToken) return { type: 'block-map', offset: this.offset, indent: this.indent, - items: [{ start: [this.sourceToken] }] + items: [{ start }] } as BlockMap + } case 'map-value-ind': { this.onKeyLine = true const prev = getPrevProps(parent) diff --git a/src/parse/tokens.ts b/src/parse/tokens.ts index 2703f787..8ee19939 100644 --- a/src/parse/tokens.ts +++ b/src/parse/tokens.ts @@ -87,7 +87,12 @@ export interface BlockSequence { type: 'block-seq' offset: number indent: number - items: Array<{ start: SourceToken[]; value?: Token; sep?: never }> + items: Array<{ + start: SourceToken[] + key?: never + sep?: never + value?: Token + }> } export interface FlowCollection { @@ -95,7 +100,12 @@ export interface FlowCollection { offset: number indent: number start: SourceToken - items: Array + items: Array<{ + start: SourceToken[] + key?: Token | null + sep?: SourceToken[] + value?: Token + }> end: SourceToken[] } diff --git a/tests/doc/comments.js b/tests/doc/comments.js index 05bbfb94..8ef642ee 100644 --- a/tests/doc/comments.js +++ b/tests/doc/comments.js @@ -261,6 +261,30 @@ describe('parse comments', () => { }) describe('flow collection commens', () => { + test('line comment after , in seq', () => { + const doc = YAML.parseDocument(source` + [ a, #c0 + b #c1 + ]`) + expect(doc.contents.items).toMatchObject([ + { value: 'a', comment: 'c0' }, + { value: 'b', comment: 'c1' } + ]) + }) + + test('line comment after , in map', () => { + const doc = YAML.parseDocument(source` + { a, #c0 + b: c, #c1 + d #c2 + }`) + expect(doc.contents.items).toMatchObject([ + { key: { value: 'a', comment: 'c0' } }, + { key: { value: 'b' }, value: { value: 'c', comment: 'c1' } }, + { key: { value: 'd', comment: 'c2' } } + ]) + }) + test('multi-line comments', () => { const doc = YAML.parseDocument('{ a,\n#c0\n#c1\nb }') expect(doc.contents.items).toMatchObject([ @@ -446,6 +470,36 @@ describe('stringify comments', () => { `) }) }) + + describe.skip('flow collection commens', () => { + test('line comment after , in seq', () => { + const doc = YAML.parseDocument(source` + [ a, #c0 + b #c1 + ]`) + expect(String(doc)).toBe(source` + [ + a, #c0 + b #c1 + ] + `) + }) + + test('line comment after , in map', () => { + const doc = YAML.parseDocument(source` + { a, #c0 + b: c, #c1 + d #c2 + }`) + expect(String(doc)).toBe(source` + { + ? a, #c0 + b: c, #c1 + ? d #c2 + } + `) + }) + }) }) describe('blank lines', () => { diff --git a/tests/doc/errors.js b/tests/doc/errors.js index 16f01acd..2d397aff 100644 --- a/tests/doc/errors.js +++ b/tests/doc/errors.js @@ -122,24 +122,17 @@ describe('block collections', () => { describe('flow collections', () => { test('start only of flow map (eemeli/yaml#8)', () => { const doc = YAML.parseDocument('{') - const message = expect.stringContaining('Expected flow map to end with }') - expect(doc.errors).toMatchObject([{ message, offset: 1 }]) + expect(doc.errors).toMatchObject([{ code: 'MISSING_CHAR', offset: 1 }]) }) test('start only of flow sequence (eemeli/yaml#8)', () => { const doc = YAML.parseDocument('[') - const message = expect.stringContaining( - 'Expected flow sequence to end with ]' - ) - expect(doc.errors).toMatchObject([{ message, offset: 1 }]) + expect(doc.errors).toMatchObject([{ code: 'MISSING_CHAR', offset: 1 }]) }) test('flow sequence without end', () => { const doc = YAML.parseDocument('[ foo, bar,') - const message = expect.stringContaining( - 'Expected flow sequence to end with ]' - ) - expect(doc.errors).toMatchObject([{ message, offset: 11 }]) + expect(doc.errors).toMatchObject([{ code: 'MISSING_CHAR', offset: 11 }]) }) test('doc-end within flow sequence', () => { @@ -147,7 +140,7 @@ describe('flow collections', () => { prettyErrors: false }) expect(doc.errors).toMatchObject([ - { message: 'Expected flow sequence to end with ]' }, + { code: 'MISSING_CHAR' }, { message: 'Unexpected flow-seq-end token in YAML document: "]"' }, { message: @@ -166,18 +159,17 @@ describe('flow collections', () => { test('block seq in flow collection', () => { const doc = YAML.parseDocument('{\n- foo\n}') - expect(doc.errors).toHaveLength(1) - expect(doc.errors[0].message).toMatch( - 'Block collections are not allowed within flow collections' - ) + expect(doc.errors).toMatchObject([{ code: 'BLOCK_IN_FLOW' }]) }) - test('anchor before explicit key indicator', () => { + test('anchor before explicit key indicator in block map', () => { + const doc = YAML.parseDocument('&a ? A') + expect(doc.errors).toMatchObject([{ code: 'BAD_PROP_ORDER' }]) + }) + + test('anchor before explicit key indicator in flow map', () => { const doc = YAML.parseDocument('{ &a ? A }') - expect(doc.errors).toHaveLength(1) - expect(doc.errors[0].message).toMatch( - 'Anchors and tags must be after the ? indicator' - ) + expect(doc.errors).toMatchObject([{ code: 'BAD_PROP_ORDER' }]) }) }) @@ -228,12 +220,14 @@ describe('pretty errors', () => { expect(docs[0].errors[0]).not.toHaveProperty('source') expect(docs[1].errors).toMatchObject([ { + code: 'UNEXPECTED_TOKEN', message: 'Unexpected , in flow map at line 3, column 7:\n\n{ 123,,, }\n ^\n', offset: 16, linePos: { line: 3, col: 7 } }, { + code: 'UNEXPECTED_TOKEN', message: 'Unexpected , in flow map at line 3, column 8:\n\n{ 123,,, }\n ^\n', offset: 17, diff --git a/tests/doc/parse.js b/tests/doc/parse.js index 2e7e468f..c07c0601 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -446,9 +446,9 @@ test('comment between key & : in flow collection (eemeli/yaml#149)', () => { expect(YAML.parse(src1)).toEqual({ a: 1 }) const src2 = '{a\n#c\n:1}' - expect(() => YAML.parse(src2)).toThrow( - 'Missing , between flow collection items' - ) + expect(async () => YAML.parse(src2)).rejects.toMatchObject({ + code: 'MISSING_CHAR' + }) }) describe('empty(ish) nodes', () => {