Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setOrigRanges() to YAML.parseCST() output #31

Merged
merged 9 commits into from
Aug 23, 2018
14 changes: 14 additions & 0 deletions __tests__/cst/corner-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,17 @@ test('eemeli/yaml#l19', () => {
expect(items).toHaveLength(2)
expect(items[1].comment).toBe(' 123')
})

test('eemeli/yaml#20', () => {
const src = 'a:\r\n 123\r\nb:\r\n 456\r\n'
const docStream = parse(src)
const a = docStream[0].contents[0].items[1].node
expect(a.strValue).toBe('123')
expect(docStream.setOrigRanges()).toBe(true)
const { origStart: a0, origEnd: a1 } = a.valueRange
expect(src.slice(a0, a1)).toBe('123')
const b = docStream[0].contents[0].items[3].node
expect(b.strValue).toBe('456')
const { origStart: b0, origEnd: b1 } = b.valueRange
expect(src.slice(b0, b1)).toBe('456')
})
203 changes: 203 additions & 0 deletions __tests__/cst/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import parse from '../../src/cst/parse'

test('return value', () => {
const src = '- foo\n- bar\n'
const cst = parse(src)
expect(cst).toHaveLength(1)
expect(cst[0]).toMatchObject({
contents: [
{
error: null,
items: [
{
error: null,
node: {
error: null,
props: [],
range: { end: 6, start: 2 },
type: 'PLAIN',
value: null,
valueRange: { end: 5, start: 2 }
},
props: [],
range: { end: 6, start: 0 },
type: 'SEQ_ITEM',
value: null,
valueRange: { end: 5, start: 0 }
},
{
error: null,
node: {
error: null,
props: [],
range: { end: 12, start: 8 },
type: 'PLAIN',
value: null,
valueRange: { end: 11, start: 8 }
},
props: [],
range: { end: 12, start: 6 },
type: 'SEQ_ITEM',
value: null,
valueRange: { end: 11, start: 6 }
}
],
props: [],
range: { end: 12, start: 0 },
type: 'SEQ',
value: null,
valueRange: { end: 11, start: 0 }
}
],
directives: [],
error: null,
props: [],
range: null,
type: 'DOCUMENT',
value: null,
valueRange: { end: 12, start: 0 }
})
})

describe('toString()', () => {
test('plain document', () => {
const src = '- foo\n- bar\n'
const cst = parse(src)
expect(String(cst)).toBe(src)
})

test('stream of two documents', () => {
const src = 'foo\n...\nbar\n'
const cst = parse(src)
expect(cst).toHaveLength(2)
expect(String(cst)).toBe(src)
})

test('document with CRLF line separators', () => {
const src = '- foo\r\n- bar\r\n'
const cst = parse(src)
expect(cst.toString()).toBe('- foo\n- bar\n')
})
})

describe('setOrigRanges()', () => {
test('return false for no CRLF', () => {
const src = '- foo\n- bar\n'
const cst = parse(src)
expect(cst.setOrigRanges()).toBe(false)
expect(cst[0].valueRange).toMatchObject({ start: 0, end: 12 })
expect(cst[0].valueRange.origStart).toBeUndefined()
expect(cst[0].valueRange.origEnd).toBeUndefined()
})

test('single document', () => {
const src = '- foo\r\n- bar\r\n'
const cst = parse(src)
expect(cst.setOrigRanges()).toBe(true)
expect(cst).toHaveLength(1)
const { range, valueRange } = cst[0].contents[0].items[1].node
expect(src.slice(range.origStart, range.origEnd)).toBe('bar\r\n')
expect(src.slice(valueRange.origStart, valueRange.origEnd)).toBe('bar')
expect(cst[0]).toMatchObject({
contents: [
{
error: null,
items: [
{
error: null,
node: {
error: null,
props: [],
range: { end: 6, origEnd: 7, origStart: 2, start: 2 },
type: 'PLAIN',
value: null,
valueRange: { end: 5, origEnd: 5, origStart: 2, start: 2 }
},
props: [],
range: { end: 6, origEnd: 7, origStart: 0, start: 0 },
type: 'SEQ_ITEM',
value: null,
valueRange: { end: 5, origEnd: 5, origStart: 0, start: 0 }
},
{
error: null,
node: {
error: null,
props: [],
range: { end: 12, origEnd: 14, origStart: 9, start: 8 },
type: 'PLAIN',
value: null,
valueRange: { end: 11, origEnd: 12, origStart: 9, start: 8 }
},
props: [],
range: { end: 12, origEnd: 14, origStart: 7, start: 6 },
type: 'SEQ_ITEM',
value: null,
valueRange: { end: 11, origEnd: 12, origStart: 7, start: 6 }
}
],
props: [],
range: { end: 12, origEnd: 14, origStart: 0, start: 0 },
type: 'SEQ',
value: null,
valueRange: { end: 11, origEnd: 12, origStart: 0, start: 0 }
}
],
directives: [],
error: null,
props: [],
range: null,
type: 'DOCUMENT',
value: null,
valueRange: { end: 12, origEnd: 14, origStart: 0, start: 0 }
})
})

test('stream of two documents', () => {
const src = 'foo\r\n...\r\nbar\r\n'
const cst = parse(src)
expect(cst.setOrigRanges()).toBe(true)
expect(cst).toHaveLength(2)
const { range, valueRange } = cst[1].contents[0]
expect(src.slice(range.origStart, range.origEnd)).toBe('bar\r\n')
expect(src.slice(valueRange.origStart, valueRange.origEnd)).toBe('bar')
expect(cst[0]).toMatchObject({
contents: [
{
error: null,
props: [],
range: { end: 4, origEnd: 5, origStart: 0, start: 0 },
type: 'PLAIN',
value: null,
valueRange: { end: 3, origEnd: 3, origStart: 0, start: 0 }
}
],
directives: [],
error: null,
props: [],
range: null,
type: 'DOCUMENT',
value: null,
valueRange: { end: 4, origEnd: 5, origStart: 0, start: 0 }
})
expect(cst[1]).toMatchObject({
contents: [
{
error: null,
props: [],
range: { end: 12, origEnd: 15, origStart: 10, start: 8 },
type: 'PLAIN',
value: null,
valueRange: { end: 11, origEnd: 13, origStart: 10, start: 8 }
}
],
directives: [],
error: null,
props: [],
range: null,
type: 'DOCUMENT',
value: null,
valueRange: { end: 12, origEnd: 15, origStart: 10, start: 8 }
})
})
})
2 changes: 1 addition & 1 deletion docs
2 changes: 1 addition & 1 deletion src/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default class Document {
const comments = { before: [], after: [] }
const contentNodes = []
contents.forEach(node => {
if (node.valueRange && !node.valueRange.isEmpty) {
if (node.valueRange && !node.valueRange.isEmpty()) {
if (contentNodes.length === 1) {
const msg = 'Document is not valid YAML (bad indentation?)'
this.errors.push(new YAMLSyntaxError(node, msg))
Expand Down
7 changes: 6 additions & 1 deletion src/cst/BlockValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class BlockValue extends Node {
if (!this.valueRange || !this.context) return null
let { start, end } = this.valueRange
const { indent, src } = this.context
if (this.valueRange.isEmpty) return ''
if (this.valueRange.isEmpty()) return ''
let lastNewLine = null
let ch = src[end - 1]
while (ch === '\n' || ch === '\t' || ch === ' ') {
Expand Down Expand Up @@ -179,4 +179,9 @@ export default class BlockValue extends Node {
JSON.stringify(this.rawValue)
return offset
}

setOrigRanges(cr, offset) {
offset = super.setOrigRanges(cr, offset)
return this.header ? this.header.setOrigRange(cr, offset) : offset
}
}
8 changes: 8 additions & 0 deletions src/cst/Collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ export default class Collection extends Node {
return offset
}

setOrigRanges(cr, offset) {
offset = super.setOrigRanges(cr, offset)
this.items.forEach(node => {
offset = node.setOrigRanges(cr, offset)
})
return offset
}

toString() {
const {
context: { src },
Expand Down
5 changes: 5 additions & 0 deletions src/cst/CollectionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export default class CollectionItem extends Node {
return offset
}

setOrigRanges(cr, offset) {
offset = super.setOrigRanges(cr, offset)
return this.node ? this.node.setOrigRanges(cr, offset) : offset
}

toString() {
const {
context: { src },
Expand Down
11 changes: 11 additions & 0 deletions src/cst/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ export default class Document extends Node {
return offset
}

setOrigRanges(cr, offset) {
offset = super.setOrigRanges(cr, offset)
this.directives.forEach(node => {
offset = node.setOrigRanges(cr, offset)
})
this.contents.forEach(node => {
offset = node.setOrigRanges(cr, offset)
})
return offset
}

toString() {
const {
contents,
Expand Down
8 changes: 8 additions & 0 deletions src/cst/FlowCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ export default class FlowCollection extends Node {
return offset
}

setOrigRanges(cr, offset) {
offset = super.setOrigRanges(cr, offset)
this.items.forEach(node => {
offset = node.setOrigRanges(cr, offset)
})
return offset
}

toString() {
const {
context: { src },
Expand Down
15 changes: 15 additions & 0 deletions src/cst/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,21 @@ export default class Node {
return start
}

/**
* Populates the `origStart` and `origEnd` values of all ranges for this
* node. Extended by child classes to handle descendant nodes.
*
* @param {number[]} cr - Positions of dropped CR characters
* @param {number} offset - Starting index of `cr` from the last call
* @returns {number} - The next offset, matching the one found for `origStart`
*/
setOrigRanges(cr, offset) {
if (this.range) offset = this.range.setOrigRange(cr, offset)
this.valueRange.setOrigRange(cr, offset)
this.props.forEach(prop => prop.setOrigRange(cr, offset))
return offset
}

toString() {
const {
context: { src },
Expand Down
4 changes: 2 additions & 2 deletions src/cst/PlainValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default class PlainValue extends Node {
if (end === null || src[end] === '#') break
offset = PlainValue.endOfLine(src, end, inFlow)
}
if (this.valueRange.isEmpty) this.valueRange.start = start
if (this.valueRange.isEmpty()) this.valueRange.start = start
this.valueRange.end = offset
trace: this.valueRange, JSON.stringify(this.rawValue)
return offset
Expand Down Expand Up @@ -112,7 +112,7 @@ export default class PlainValue extends Node {
trace: 'first line',
{ valueRange: this.valueRange, comment: this.comment },
JSON.stringify(this.rawValue)
if (!this.hasComment || this.valueRange.isEmpty) {
if (!this.hasComment || this.valueRange.isEmpty()) {
offset = this.parseBlockValue(offset)
}
trace: this.type,
Expand Down
37 changes: 30 additions & 7 deletions src/cst/Range.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,38 @@ export default class Range {
this.end = end || start
}

get isEmpty() {
isEmpty() {
return typeof this.start !== 'number' || !this.end || this.end <= this.start
}

get length() {
return this.isEmpty ? 0 : this.end - this.start
}

apply(src) {
return this.isEmpty ? '' : src.slice(this.start, this.end)
/**
* Set `origStart` and `origEnd` to point to the original source range for
* this node, which may differ due to dropped CR characters.
*
* @param {number[]} cr - Positions of dropped CR characters
* @param {number} offset - Starting index of `cr` from the last call
* @returns {number} - The next offset, matching the one found for `origStart`
*/
setOrigRange(cr, offset) {
const { start, end } = this
if (cr.length === 0 || end <= cr[0]) {
this.origStart = start
this.origEnd = end
return offset
}
let i = offset
while (i < cr.length) {
if (cr[i] > start) break
else ++i
}
this.origStart = start + i
const nextOffset = i
while (i < cr.length) {
// if end was at \n, it should now be at \r
if (cr[i] >= end) break
else ++i
}
this.origEnd = end + i
return nextOffset
}
}
Loading