Skip to content

Commit

Permalink
Merge pull request #68 from MikeRalphson/lineOffsets
Browse files Browse the repository at this point in the history
Optionally compute lineOffsets, fixes #67
  • Loading branch information
eemeli authored Jan 27, 2019
2 parents c286abe + 2a244a0 commit 8dcc118
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/cst/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export default class Document extends Node {
* @returns {number} - Index of the character after this
*/
parse(context, start) {
context.root = this
this.context = context
const { src } = context
trace: 'DOC START', JSON.stringify(src.slice(start))
Expand Down
9 changes: 9 additions & 0 deletions src/cst/Node.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getLinePos from './getLinePos'
import Range from './Range'

export const Type = {
Expand Down Expand Up @@ -248,6 +249,14 @@ export default class Node {
return jsonLikeTypes.indexOf(this.type) !== -1
}

get rangeAsLinePos() {
if (!this.range || !this.context) return undefined
const start = getLinePos(this.range.start, this.context.root)
if (!start) return undefined
const end = getLinePos(this.range.end, this.context.root)
return { start, end }
}

get rawValue() {
if (!this.valueRange || !this.context) return null
const { start, end } = this.valueRange
Expand Down
1 change: 1 addition & 0 deletions src/cst/ParseContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class ParseContext {
this.indent = indent != null ? indent : orig.indent
this.lineStart = lineStart != null ? lineStart : orig.lineStart
this.parent = parent != null ? parent : orig.parent || {}
this.root = orig.root
this.src = orig.src
}

Expand Down
54 changes: 54 additions & 0 deletions src/cst/getLinePos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function findLineStarts(src) {
const ls = [0]
let offset = src.indexOf('\n')
while (offset !== -1) {
offset += 1
ls.push(offset)
offset = src.indexOf('\n', offset)
}
return ls
}

/**
* Determine the line/col position matching a character offset.
*
* Accepts a source string or a CST document as the second parameter. With
* the latter, starting indices for lines are cached in the document as
* `lineStarts: number[]`.
*
* Returns a zero-indexed `{ line, col }` location if found, or
* `undefined` otherwise.
*
* @param {number} offset
* @param {string|Document|Document[]} cst
* @returns {{ line: number, col: number }|undefined}
*/
export default function getLinePos(offset, cst) {
if (typeof offset === 'number' && offset >= 0) {
let lineStarts, srcLength
if (typeof cst === 'string') {
lineStarts = findLineStarts(cst)
srcLength = cst.length
} else {
if (Array.isArray(cst)) cst = cst[0]
if (cst) {
if (!cst.lineStarts) cst.lineStarts = findLineStarts(cst.context.src)
lineStarts = cst.lineStarts
srcLength = cst.context.src.length
}
}
if (lineStarts && offset <= srcLength) {
for (let i = 0; i < lineStarts.length; ++i) {
const start = lineStarts[i]
if (offset < start) {
const line = i - 1
return { line, col: offset - lineStarts[line] }
}
if (offset === start) return { line: i, col: 0 }
}
const line = lineStarts.length - 1
return { line, col: offset - lineStarts[line] }
}
}
return undefined
}
2 changes: 1 addition & 1 deletion src/cst/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export default function parse(src) {
return '\n'
})
}
const context = new ParseContext({ src })
const documents = []
let offset = 0
do {
const doc = new Document()
const context = new ParseContext({ src })
offset = doc.parse(context, offset)
documents.push(doc)
} while (offset < src.length)
Expand Down
49 changes: 49 additions & 0 deletions tests/cst/getLinePos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import getLinePos from '../../src/cst/getLinePos'
import parse from '../../src/cst/parse'

test('lineStarts for empty document', () => {
const src = ''
const cst = parse(src)
expect(() => getLinePos(0, cst)).not.toThrow()
expect(cst[0].lineStarts).toMatchObject([0])
})

test('lineStarts for multiple documents', () => {
const src = 'foo\n...\nbar\n'
const cst = parse(src)
expect(() => getLinePos(0, cst)).not.toThrow()
expect(cst[0].lineStarts).toMatchObject([0, 4, 8, 12])
})

test('lineStarts for malformed document', () => {
const src = '- foo\n\t- bar\n'
const cst = parse(src)
expect(() => getLinePos(0, cst)).not.toThrow()
expect(cst[0].lineStarts).toMatchObject([0, 6, 13])
})

test('getLinePos()', () => {
const src = '- foo\n- bar\n'
const cst = parse(src)
expect(cst[0].lineStarts).toBeUndefined()
expect(getLinePos(0, cst)).toMatchObject({ line: 0, col: 0 })
expect(getLinePos(1, cst)).toMatchObject({ line: 0, col: 1 })
expect(getLinePos(2, cst)).toMatchObject({ line: 0, col: 2 })
expect(getLinePos(5, cst)).toMatchObject({ line: 0, col: 5 })
expect(getLinePos(6, cst)).toMatchObject({ line: 1, col: 0 })
expect(getLinePos(7, cst)).toMatchObject({ line: 1, col: 1 })
expect(getLinePos(11, cst)).toMatchObject({ line: 1, col: 5 })
expect(getLinePos(12, cst)).toMatchObject({ line: 2, col: 0 })
expect(cst[0].lineStarts).toMatchObject([0, 6, 12])
})

test('invalid args for getLinePos()', () => {
const src = '- foo\n- bar\n'
const cst = parse(src)
expect(getLinePos()).toBeUndefined()
expect(getLinePos(0)).toBeUndefined()
expect(getLinePos(1)).toBeUndefined()
expect(getLinePos(-1, cst)).toBeUndefined()
expect(getLinePos(13, cst)).toBeUndefined()
expect(getLinePos(Math.MAXINT, cst)).toBeUndefined()
})
6 changes: 6 additions & 0 deletions tests/cst/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ describe('setOrigRanges()', () => {
value: null,
valueRange: { end: 12, origEnd: 14, origStart: 0, start: 0 }
})
expect(cst[0].context.root).toBe(cst[0])
expect(cst[0].contents[0].items[1].node.context.root).toBe(cst[0])
})

test('stream of two documents', () => {
Expand Down Expand Up @@ -204,6 +206,8 @@ describe('setOrigRanges()', () => {
value: null,
valueRange: { end: 12, origEnd: 15, origStart: 10, start: 8 }
})
expect(cst[0].context.root).toBe(cst[0])
expect(cst[1].context.root).toBe(cst[1])
})

test('flow collections', () => {
Expand Down Expand Up @@ -242,5 +246,7 @@ describe('setOrigRanges()', () => {
value: null,
valueRange: { end: 7, origEnd: 9, origStart: 2, start: 1 }
})
expect(cst[0].context.root).toBe(cst[0])
expect(cst[0].contents[1].context.root).toBe(cst[0])
})
})
31 changes: 0 additions & 31 deletions tests/doc/corner-cases.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'fs'
import path from 'path'
import Node from '../../src/cst/Node'
import YAML from '../../src/index'

test('eemeli/yaml#2', () => {
Expand All @@ -25,36 +24,6 @@ test('eemeli/yaml#3', () => {
expect(doc.contents.items[0].value.value).toBe(123)
})

test('eemeli/yaml#6', () => {
const src = 'abc: 123\ndef'
const doc = YAML.parseDocument(src)
expect(doc.errors).toHaveLength(1)
expect(doc.errors[0].name).toBe('YAMLSemanticError')
expect(doc.errors[0].source).toBeInstanceOf(Node)
})

describe('eemeli/yaml#7', () => {
test('map', () => {
const src = '{ , }\n---\n{ 123,,, }\n'
const docs = YAML.parseAllDocuments(src)
expect(docs[0].errors).toHaveLength(1)
expect(docs[1].errors).toHaveLength(2)
})
test('seq', () => {
const src = '[ , ]\n---\n[ 123,,, ]\n'
const docs = YAML.parseAllDocuments(src)
expect(docs[0].errors).toHaveLength(1)
expect(docs[1].errors).toHaveLength(2)
})
})

test('eemeli/yaml#8', () => {
const src = '{'
const doc = YAML.parseDocument(src)
expect(doc.errors).toHaveLength(1)
expect(doc.errors[0].name).toBe('YAMLSemanticError')
})

describe('eemeli/yaml#10', () => {
test('reported', () => {
const src = `
Expand Down
59 changes: 59 additions & 0 deletions tests/doc/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Node from '../../src/cst/Node'
import YAML from '../../src/index'

test('eemeli/yaml#6', () => {
const src = 'abc: 123\ndef'
const doc = YAML.parseDocument(src)
expect(doc.errors).toMatchObject([{ name: 'YAMLSemanticError' }])
const node = doc.errors[0].source
expect(node).toBeInstanceOf(Node)
expect(node.rangeAsLinePos).toMatchObject({
start: { line: 1, col: 0 },
end: { line: 1, col: 3 }
})
})

describe('eemeli/yaml#7', () => {
test('map', () => {
const src = '{ , }\n---\n{ 123,,, }\n'
const docs = YAML.parseAllDocuments(src)
expect(docs[0].errors).toMatchObject([{ name: 'YAMLSyntaxError' }])
expect(docs[1].errors).toMatchObject([
{ name: 'YAMLSyntaxError' },
{ name: 'YAMLSyntaxError' }
])
const node = docs[0].errors[0].source
expect(node).toBeInstanceOf(Node)
expect(node.rangeAsLinePos).toMatchObject({
start: { line: 0, col: 0 },
end: { line: 0, col: 5 }
})
})
test('seq', () => {
const src = '[ , ]\n---\n[ 123,,, ]\n'
const docs = YAML.parseAllDocuments(src)
expect(docs[0].errors).toMatchObject([{ name: 'YAMLSyntaxError' }])
expect(docs[1].errors).toMatchObject([
{ name: 'YAMLSyntaxError' },
{ name: 'YAMLSyntaxError' }
])
const node = docs[1].errors[0].source
expect(node).toBeInstanceOf(Node)
expect(node.rangeAsLinePos).toMatchObject({
start: { line: 2, col: 0 },
end: { line: 2, col: 10 }
})
})
})

test('eemeli/yaml#8', () => {
const src = '{'
const doc = YAML.parseDocument(src)
expect(doc.errors).toMatchObject([{ name: 'YAMLSemanticError' }])
const node = doc.errors[0].source
expect(node).toBeInstanceOf(Node)
expect(node.rangeAsLinePos).toMatchObject({
start: { line: 0, col: 0 },
end: { line: 0, col: 1 }
})
})

0 comments on commit 8dcc118

Please sign in to comment.