diff --git a/docs/04_documents.md b/docs/04_documents.md index d5b6e99b..ae038326 100644 --- a/docs/04_documents.md +++ b/docs/04_documents.md @@ -14,33 +14,30 @@ doc.contents // YAMLMap { // items: // [ Pair { -// key: Scalar { value: 'YAML', range: [ 0, 4 ] }, +// key: Scalar { value: 'YAML', range: [ 0, 4, 4 ] }, // value: // YAMLSeq { // items: // [ Scalar { // value: 'A human-readable data serialization language', -// range: [ 10, 55 ] }, +// range: [ 10, 54, 55 ] }, // Scalar { // value: 'https://en.wikipedia.org/wiki/YAML', -// range: [ 59, 94 ] } ], -// tag: 'tag:yaml.org,2002:seq', -// range: [ 8, 94 ] } }, +// range: [ 59, 93, 94 ] } ], +// range: [ 8, 94, 94 ] } }, // Pair { -// key: Scalar { value: 'yaml', range: [ 94, 98 ] }, +// key: Scalar { value: 'yaml', range: [ 94, 98, 98 ] }, // value: // YAMLSeq { // items: // [ Scalar { // value: 'A complete JavaScript implementation', -// range: [ 104, 141 ] }, +// range: [ 104, 140, 141 ] }, // Scalar { // value: 'https://www.npmjs.com/package/yaml', -// range: [ 145, 180 ] } ], -// tag: 'tag:yaml.org,2002:seq', -// range: [ 102, 180 ] } } ], -// tag: 'tag:yaml.org,2002:map', -// range: [ 0, 180 ] } +// range: [ 145, 180, 180 ] } ], +// range: [ 102, 180, 180 ] } } ], +// range: [ 0, 180, 180 ] } ``` #### `parseDocument(str, options = {}): Document` diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md index 9e7a89f6..6b75c916 100644 --- a/docs/05_content_nodes.md +++ b/docs/05_content_nodes.md @@ -12,9 +12,9 @@ It is valid to have an anchor associated with a node even if it has no aliases. class NodeBase { comment?: string // a comment on or immediately after this commentBefore?: string // a comment before this - range?: [number, number] - // the [start, end] range of characters of the source parsed - // into this node (undefined for pairs or if not parsed) + range?: [number, number, number] + // The [start, value-end, node-end] character offsets for the part + // of the source parsed into this node (undefined if not parsed). spaceBefore?: boolean // a blank line before this node and its commentBefore tag?: string // a fully qualified tag, if required diff --git a/docs/07_parsing_yaml.md b/docs/07_parsing_yaml.md index c359e092..7abcb39f 100644 --- a/docs/07_parsing_yaml.md +++ b/docs/07_parsing_yaml.md @@ -308,7 +308,7 @@ const item = doc.value.items[0].value } YAML.resolveAsScalar(item) -> { value: 'bar', type: 'QUOTE_DOUBLE', comment: 'comment', length: 14 } +> { value: 'bar', type: 'QUOTE_DOUBLE', comment: 'comment', range: [5, 9, 19] } ``` #### `CST.isCollection(token?: Token): boolean` diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index bb9ffc4e..9e0e2f04 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -52,8 +52,9 @@ export function composeDoc( ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError) - const re = resolveEnd(end, doc.contents.range[1], false, onError) + const contentEnd = doc.contents.range[2] + const re = resolveEnd(end, contentEnd, false, onError) if (re.comment) doc.comment = re.comment - doc.range = [offset, re.offset] + doc.range = [offset, contentEnd, re.offset] return doc } diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 24651d87..24d266bb 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -102,8 +102,9 @@ function composeAlias( onError: ComposeErrorHandler ) { const alias = new Alias(source.substring(1)) - const re = resolveEnd(end, offset + source.length, options.strict, onError) - alias.range = [offset, re.offset] + const valueEnd = offset + source.length + const re = resolveEnd(end, valueEnd, options.strict, onError) + alias.range = [offset, valueEnd, re.offset] if (re.comment) alias.comment = re.comment return alias as Alias.Parsed } diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts index dca620ac..009b5886 100644 --- a/src/compose/compose-scalar.ts +++ b/src/compose/compose-scalar.ts @@ -14,8 +14,7 @@ export function composeScalar( tagName: string | null, onError: ComposeErrorHandler ) { - const { offset } = token - const { value, type, comment, length } = + const { value, type, comment, range } = token.type === 'block-scalar' ? resolveBlockScalar(token, ctx.options.strict, onError) : resolveFlowScalar(token, ctx.options.strict, onError) @@ -28,15 +27,15 @@ export function composeScalar( try { const res = tag.resolve( value, - msg => onError(offset, 'TAG_RESOLVE_FAILED', msg), + msg => onError(token.offset, 'TAG_RESOLVE_FAILED', msg), ctx.options ) scalar = isScalar(res) ? res : new Scalar(res) } catch (error) { - onError(offset, 'TAG_RESOLVE_FAILED', error.message) + onError(token.offset, 'TAG_RESOLVE_FAILED', error.message) scalar = new Scalar(value) } - scalar.range = [offset, offset + length] + scalar.range = range scalar.source = value if (type) scalar.type = type if (tagName) scalar.tag = tagName diff --git a/src/compose/composer.ts b/src/compose/composer.ts index fa2c883c..450bcc3f 100644 --- a/src/compose/composer.ts +++ b/src/compose/composer.ts @@ -206,7 +206,7 @@ export class Composer { const dc = this.doc.comment this.doc.comment = dc ? `${dc}\n${end.comment}` : end.comment } - this.doc.range[1] = end.offset + this.doc.range[2] = end.offset break } default: @@ -240,7 +240,7 @@ export class Composer { 'MISSING_CHAR', 'Missing directives-end indicator line' ) - doc.range = [0, endOffset] + doc.range = [0, endOffset, endOffset] this.decorate(doc, false) yield doc } diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index 83fe741c..476fa291 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -65,7 +65,7 @@ export function resolveBlockMap( const valueProps = resolveProps(sep || [], { ctx, indicator: 'map-value-ind', - offset: keyNode.range[1], + offset: keyNode.range[2], onError, startOnNewline: !key || key.type === 'block-scalar' }) @@ -93,7 +93,7 @@ export function resolveBlockMap( const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep, null, valueProps, onError) - offset = valueNode.range[1] + offset = valueNode.range[2] map.items.push(new Pair(keyNode, valueNode)) } else { // key with no value @@ -111,6 +111,6 @@ export function resolveBlockMap( } } - map.range = [bm.offset, offset] + map.range = [bm.offset, offset, offset] return map as YAMLMap.Parsed } diff --git a/src/compose/resolve-block-scalar.ts b/src/compose/resolve-block-scalar.ts index b84fc3ef..fd8125c0 100644 --- a/src/compose/resolve-block-scalar.ts +++ b/src/compose/resolve-block-scalar.ts @@ -1,3 +1,4 @@ +import { Range } from '../nodes/Node.js' import { Scalar } from '../nodes/Scalar.js' import type { BlockScalar } from '../parse/cst.js' import type { ComposeErrorHandler } from './composer.js' @@ -10,10 +11,12 @@ export function resolveBlockScalar( value: string type: Scalar.BLOCK_FOLDED | Scalar.BLOCK_LITERAL | null comment: string - length: number + range: Range } { + const start = scalar.offset const header = parseBlockScalarHeader(scalar, strict, onError) - if (!header) return { value: '', type: null, comment: '', length: 0 } + if (!header) + return { value: '', type: null, comment: '', range: [start, start, start] } const type = header.mode === '>' ? Scalar.BLOCK_FOLDED : Scalar.BLOCK_LITERAL const lines = scalar.source ? splitLines(scalar.source) : [] @@ -29,9 +32,9 @@ export function resolveBlockScalar( if (!scalar.source || chompStart === 0) { const value = header.chomp === '+' ? lines.map(line => line[0]).join('\n') : '' - let length = header.length - if (scalar.source) length += scalar.source.length - return { value, type, comment: header.comment, length } + let end = start + header.length + if (scalar.source) end += scalar.source.length + return { value, type, comment: header.comment, range: [start, end, end] } } // find the indentation level to trim from start @@ -113,12 +116,8 @@ export function resolveBlockScalar( value += '\n' } - return { - value, - type, - comment: header.comment, - length: header.length + scalar.source.length - } + const end = start + header.length + scalar.source.length + return { value, type, comment: header.comment, range: [start, end, end] } } function parseBlockScalarHeader( diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts index a6211df9..be620dad 100644 --- a/src/compose/resolve-block-seq.ts +++ b/src/compose/resolve-block-seq.ts @@ -40,9 +40,9 @@ export function resolveBlockSeq( const node = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, offset, start, null, props, onError) - offset = node.range[1] + offset = node.range[2] seq.items.push(node) } - seq.range = [bs.offset, offset] + seq.range = [bs.offset, offset, offset] return seq as YAMLSeq.Parsed } diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts index 5e236072..3c5c9865 100644 --- a/src/compose/resolve-flow-collection.ts +++ b/src/compose/resolve-flow-collection.ts @@ -113,7 +113,7 @@ export function resolveFlowCollection( ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError) ;(coll as YAMLSeq).items.push(valueNode) - offset = valueNode.range[1] + offset = valueNode.range[2] } else { // item is a key+value pair @@ -128,7 +128,7 @@ export function resolveFlowCollection( ctx, flow: fcName, indicator: 'map-value-ind', - offset: keyNode.range[1], + offset: keyNode.range[2], onError, startOnNewline: false }) @@ -188,29 +188,32 @@ export function resolveFlowCollection( map.items.push(pair) ;(coll as YAMLSeq).items.push(map) } - offset = valueNode ? valueNode.range[1] : valueProps.end + offset = valueNode ? valueNode.range[2] : valueProps.end } } const expectedEnd = isMap ? '}' : ']' const [ce, ...ee] = fc.end - if (!ce || ce.source !== expectedEnd) { + let cePos = offset + if (ce && ce.source === expectedEnd) cePos += ce.source.length + else { onError( offset + 1, 'MISSING_CHAR', `Expected ${fcName} to end with ${expectedEnd}` ) + if (ce && ce.source.length !== 1) ee.unshift(ce) } - if (ce) offset += ce.source.length if (ee.length > 0) { - const end = resolveEnd(ee, offset, ctx.options.strict, onError) + 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 } - offset = end.offset + coll.range = [fc.offset, cePos, end.offset] + } else { + coll.range = [fc.offset, cePos, cePos] } - coll.range = [fc.offset, offset] return coll as YAMLMap.Parsed | YAMLSeq.Parsed } diff --git a/src/compose/resolve-flow-scalar.ts b/src/compose/resolve-flow-scalar.ts index d31e6c6a..15322faf 100644 --- a/src/compose/resolve-flow-scalar.ts +++ b/src/compose/resolve-flow-scalar.ts @@ -1,3 +1,4 @@ +import { Range } from '../nodes/Node.js' import { Scalar } from '../nodes/Scalar.js' import type { FlowScalar } from '../parse/cst.js' import type { ComposeErrorHandler } from './composer.js' @@ -11,7 +12,7 @@ export function resolveFlowScalar( value: string type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE | null comment: string - length: number + range: Range } { let _type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE let value: string @@ -44,7 +45,7 @@ export function resolveFlowScalar( value: '', type: null, comment: '', - length: source.length + range: [offset, offset + source.length, offset + source.length] } } @@ -53,7 +54,7 @@ export function resolveFlowScalar( value, type: _type, comment: re.comment, - length: source.length + re.offset + range: [offset, offset + source.length, offset + source.length + re.offset] } } diff --git a/src/doc/Document.ts b/src/doc/Document.ts index ed3cc238..5978ce29 100644 --- a/src/doc/Document.ts +++ b/src/doc/Document.ts @@ -7,7 +7,8 @@ import { isScalar, Node, NODE_TYPE, - ParsedNode + ParsedNode, + Range } from '../nodes/Node.js' import { Pair } from '../nodes/Pair.js' import type { Scalar } from '../nodes/Scalar.js' @@ -35,9 +36,7 @@ export type Replacer = any[] | ((key: any, value: any) => unknown) export declare namespace Document { interface Parsed extends Document { - range: [number, number] - /** The schema used with the document. */ - schema: Schema + range: Range } } @@ -65,6 +64,12 @@ export class Document { > > + /** + * The [start, value-end, node-end] character offsets for the part of the + * source parsed into this document (undefined if not parsed). + */ + declare range?: Range + // TS can't figure out that setSchema() will set this, or throw /** The schema used with the document. Use `setSchema()` to change. */ declare schema: Schema diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts index 62d9a030..3c7842b6 100644 --- a/src/nodes/Alias.ts +++ b/src/nodes/Alias.ts @@ -2,7 +2,15 @@ import { anchorIsValid } from '../doc/anchors' import type { Document } from '../doc/Document' import type { StringifyContext } from '../stringify/stringify.js' import { visit } from '../visit' -import { ALIAS, isAlias, isCollection, isPair, Node, NodeBase } from './Node.js' +import { + ALIAS, + isAlias, + isCollection, + isPair, + Node, + NodeBase, + Range +} from './Node.js' import type { Scalar } from './Scalar' import type { ToJSContext } from './toJS.js' import type { YAMLMap } from './YAMLMap' @@ -10,7 +18,7 @@ import type { YAMLSeq } from './YAMLSeq' export declare namespace Alias { interface Parsed extends Alias { - range: [number, number] + range: Range } } diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index e47d3677..7ae38d88 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -14,6 +14,8 @@ export type ParsedNode = | YAMLMap.Parsed | YAMLSeq.Parsed +export type Range = [number, number, number] + export const ALIAS = Symbol.for('yaml.alias') export const DOC = Symbol.for('yaml.document') export const MAP = Symbol.for('yaml.map') @@ -75,10 +77,10 @@ export abstract class NodeBase { declare commentBefore?: string | null /** - * The [start, end] range of characters of the source parsed - * into this node (undefined for pairs or if not parsed) + * The [start, value-end, node-end] character offsets for the part of the + * source parsed into this node (undefined if not parsed). */ - declare range?: [number, number] | null + declare range?: Range | null /** A blank line before this node and its commentBefore */ declare spaceBefore?: boolean diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts index 1f978018..c5f1331c 100644 --- a/src/nodes/Scalar.ts +++ b/src/nodes/Scalar.ts @@ -1,4 +1,4 @@ -import { NodeBase, SCALAR } from './Node.js' +import { NodeBase, Range, SCALAR } from './Node.js' import { toJS, ToJSContext } from './toJS.js' export const isScalarValue = (value: unknown) => @@ -6,7 +6,7 @@ export const isScalarValue = (value: unknown) => export declare namespace Scalar { interface Parsed extends Scalar { - range: [number, number] + range: Range source: string } diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts index d997f099..c8f4452c 100644 --- a/src/nodes/YAMLMap.ts +++ b/src/nodes/YAMLMap.ts @@ -3,7 +3,7 @@ import type { StringifyContext } from '../stringify/stringify.js' import { stringifyCollection } from '../stringify/stringifyCollection.js' import { addPairToJSMap } from './addPairToJSMap.js' import { Collection } from './Collection.js' -import { isPair, isScalar, MAP, ParsedNode } from './Node.js' +import { isPair, isScalar, MAP, ParsedNode, Range } from './Node.js' import { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import type { ToJSContext } from './toJS.js' @@ -28,7 +28,7 @@ export declare namespace YAMLMap { V extends ParsedNode | null = ParsedNode | null > extends YAMLMap { items: Pair[] - range: [number, number] + range: Range } } diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts index b0d7984c..21e49671 100644 --- a/src/nodes/YAMLSeq.ts +++ b/src/nodes/YAMLSeq.ts @@ -2,7 +2,7 @@ import type { Schema } from '../schema/Schema.js' import type { StringifyContext } from '../stringify/stringify.js' import { stringifyCollection } from '../stringify/stringifyCollection.js' import { Collection } from './Collection.js' -import { isScalar, ParsedNode, SEQ } from './Node.js' +import { isScalar, ParsedNode, Range, SEQ } from './Node.js' import type { Pair } from './Pair.js' import { isScalarValue } from './Scalar.js' import { toJS, ToJSContext } from './toJS.js' @@ -12,7 +12,7 @@ export declare namespace YAMLSeq { T extends ParsedNode | Pair = ParsedNode > extends YAMLSeq { items: T[] - range: [number, number] + range: Range } } diff --git a/src/parse/cst-scalar.ts b/src/parse/cst-scalar.ts index 4fcdd4c1..4f135a0b 100644 --- a/src/parse/cst-scalar.ts +++ b/src/parse/cst-scalar.ts @@ -2,6 +2,7 @@ import type { ComposeErrorHandler } from '../compose/composer.js' import { resolveBlockScalar } from '../compose/resolve-block-scalar.js' import { resolveFlowScalar } from '../compose/resolve-flow-scalar.js' import { YAMLParseError } from '../errors.js' +import { Range } from '../nodes/Node.js' import type { Scalar } from '../nodes/Scalar.js' import type { StringifyContext } from '../stringify/stringify.js' import { stringifyString } from '../stringify/stringifyString.js' @@ -19,7 +20,7 @@ export function resolveAsScalar( value: string type: Scalar.Type | null comment: string - length: number + range: Range } | null { if (token) { if (!onError) @@ -79,7 +80,9 @@ export function createScalarToken( options: { lineWidth: -1 } } as StringifyContext ) - const end = context.end ?? [{ type: 'newline', offset: -1, indent, source: '\n' }] + const end = context.end ?? [ + { type: 'newline', offset: -1, indent, source: '\n' } + ] switch (source[0]) { case '|': case '>': { @@ -185,7 +188,7 @@ function setBlockScalarValue(token: Token, source: string) { header.source = head token.source = body } else { - let offset = token.offset + const { offset } = token const indent = 'indent' in token ? token.indent : -1 const props: Token[] = [ { type: 'block-scalar-header', offset, indent, source: head } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index ddb2bfaf..6a696787 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -21,7 +21,7 @@ export class Schema { // Used by createNode() and composeScalar() [MAP]: CollectionTag; - [SCALAR]: ScalarTag + [SCALAR]: ScalarTag; [SEQ]: CollectionTag constructor({ diff --git a/src/test-events.ts b/src/test-events.ts index 4da8d7d0..bbd425aa 100644 --- a/src/test-events.ts +++ b/src/test-events.ts @@ -47,13 +47,14 @@ export function testEvents(src: string) { const doc = docs[i] let root = doc.contents if (Array.isArray(root)) root = root[0] - const [rootStart, rootEnd] = doc.range || [0, 0] + // eslint-disable-next-line no-sparse-arrays + const [rootStart, , rootEnd] = doc.range || [0, , 0] const error = doc.errors[0] if (error && (!error.offset || error.offset < rootStart)) throw new Error() let docStart = '+DOC' if (doc.directives.marker) docStart += ' ---' - else if (doc.contents && doc.contents.range[1] === doc.contents.range[0]) + else if (doc.contents && doc.contents.range[2] === doc.contents.range[0]) continue events.push(docStart) addEvents(events, doc, error?.offset ?? -1, root) diff --git a/tests/doc/comments.js b/tests/doc/comments.js index 208d0e88..2656590b 100644 --- a/tests/doc/comments.js +++ b/tests/doc/comments.js @@ -51,7 +51,7 @@ describe('parse comments', () => { expect(doc.contents.comment).toBe('c1') expect(doc.comment).toBe('c2') expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 14]) + expect(doc.contents.range).toMatchObject([4, 9, 14]) }) test('"quoted"', () => { @@ -61,7 +61,7 @@ describe('parse comments', () => { expect(doc.contents.comment).toBe('c1') expect(doc.comment).toBe('c2') expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 16]) + expect(doc.contents.range).toMatchObject([4, 11, 16]) }) test('block', () => { @@ -71,7 +71,7 @@ describe('parse comments', () => { expect(doc.contents.comment).toBe('c1') expect(doc.comment).toBe('c2') expect(doc.contents.value).toBe('value') - expect(doc.contents.range).toMatchObject([4, 18]) + expect(doc.contents.range).toMatchObject([4, 18, 18]) }) }) @@ -93,7 +93,7 @@ describe('parse comments', () => { { commentBefore: 'c0', value: 'value 1', comment: 'c1' }, { value: 'value 2' } ], - range: [4, 29] + range: [4, 29, 29] }, comment: 'c2' }) diff --git a/tests/doc/parse.js b/tests/doc/parse.js index 03adf423..e866968e 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -455,7 +455,7 @@ describe('empty(ish) nodes', () => { test('empty node position', () => { const doc = YAML.parseDocument('\r\na: # 123\r\n') const empty = doc.contents.items[0].value - expect(empty.range).toEqual([5, 5]) + expect(empty.range).toEqual([5, 5, 5]) }) test('parse an empty string as null', () => {