import { isPair } from '../nodes/identity.js'; import { Pair } from '../nodes/Pair.js'; import { YAMLMap } from '../nodes/YAMLMap.js'; import { YAMLSeq } from '../nodes/YAMLSeq.js'; import { resolveEnd } from './resolve-end.js'; import { resolveProps } from './resolve-props.js'; import { containsNewline } from './util-contains-newline.js'; import { mapIncludes } from './util-map-includes.js'; const blockMsg = 'Block collections are not allowed within flow collections'; const isBlock = (token) => token && (token.type === 'block-map' || token.type === 'block-seq'); function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) { const isMap = fc.start.source === '{'; const fcName = isMap ? 'flow map' : 'flow sequence'; const NodeClass = (tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq)); const coll = new NodeClass(ctx.schema); coll.flow = true; const atRoot = ctx.atRoot; if (atRoot) ctx.atRoot = false; if (ctx.atKey) ctx.atKey = false; let offset = fc.offset + fc.start.source.length; for (let i = 0; i < fc.items.length; ++i) { const collItem = fc.items[i]; const { start, key, sep, value } = collItem; const props = resolveProps(start, { flow: fcName, indicator: 'explicit-key-ind', next: key ?? sep?.[0], offset, onError, parentIndent: fc.indent, startOnNewline: false }); if (!props.found) { if (!props.anchor && !props.tag && !sep && !value) { if (i === 0 && props.comma) onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`); else if (i < fc.items.length - 1) onError(props.start, 'UNEXPECTED_TOKEN', `Unexpected empty item in ${fcName}`); if (props.comment) { if (coll.comment) coll.comment += '\n' + props.comment; else coll.comment = props.comment; } offset = props.end; continue; } if (!isMap && ctx.options.strict && containsNewline(key)) onError(key, // checked by containsNewline() '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, '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; } } 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); } } } 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.items.push(valueNode); offset = valueNode.range[2]; if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg); } else { // item is a key+value pair // key value ctx.atKey = true; const keyStart = props.end; const keyNode = key ? composeNode(ctx, key, props, onError) : composeEmptyNode(ctx, keyStart, start, null, props, onError); if (isBlock(key)) onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg); ctx.atKey = false; // value properties const valueProps = resolveProps(sep ?? [], { flow: fcName, indicator: 'map-value-ind', next: value, offset: keyNode.range[2], onError, parentIndent: fc.indent, 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, '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, '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, 'MISSING_CHAR', `Missing space after : in ${fcName}`); else onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`); } // value value const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError) : null; if (valueNode) { if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg); } else if (valueProps.comment) { if (keyNode.comment) keyNode.comment += '\n' + valueProps.comment; else keyNode.comment = valueProps.comment; } const pair = new Pair(keyNode, valueNode); if (ctx.options.keepSourceTokens) pair.srcToken = collItem; if (isMap) { const map = coll; if (mapIncludes(ctx, map.items, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique'); map.items.push(pair); } else { const map = new YAMLMap(ctx.schema); map.flow = true; map.items.push(pair); const endRange = (valueNode ?? keyNode).range; map.range = [keyNode.range[0], endRange[1], endRange[2]]; coll.items.push(map); } offset = valueNode ? valueNode.range[2] : valueProps.end; } } const expectedEnd = isMap ? '}' : ']'; const [ce, ...ee] = fc.end; let cePos = offset; if (ce && ce.source === expectedEnd) cePos = ce.offset + ce.source.length; else { const name = fcName[0].toUpperCase() + fcName.substring(1); const msg = atRoot ? `${name} must end with a ${expectedEnd}` : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`; onError(offset, atRoot ? 'MISSING_CHAR' : 'BAD_INDENT', msg); if (ce && ce.source.length !== 1) ee.unshift(ce); } if (ee.length > 0) { const end = resolveEnd(ee, cePos, ctx.options.strict, onError); if (end.comment) { if (coll.comment) coll.comment += '\n' + end.comment; else coll.comment = end.comment; } coll.range = [fc.offset, cePos, end.offset]; } else { coll.range = [fc.offset, cePos, cePos]; } return coll; } export { resolveFlowCollection };