Skip to content

Commit

Permalink
Fix line folding for quoted strings + check for sufficient indentatio…
Browse files Browse the repository at this point in the history
…n (QB6E)
  • Loading branch information
eemeli committed Feb 22, 2018
1 parent c604a79 commit 22e4f65
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 39 deletions.
4 changes: 2 additions & 2 deletions __tests__/YAML-1.2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,10 @@ tie-fighter: '|\\-*-/|'`,
spans many lines.
quoted: "So does this
quoted scalar.\n"`,
quoted scalar.\\n"`,
tgt: [ {
plain: 'This unquoted scalar spans many lines.',
quoted: 'So does this quoted scalar. ' } ]
quoted: 'So does this quoted scalar.\n' } ]
},
},

Expand Down
4 changes: 2 additions & 2 deletions __tests__/ast/YAML-1.2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,10 @@ tie-fighter: '|\\-*-/|'`,
spans many lines.
quoted: "So does this
quoted scalar.\n"`,
quoted scalar.\\n"`,
tgt: [ { contents: [ { items: [
'plain', { type: Type.MAP_VALUE, node: 'This unquoted scalar spans many lines.' },
'quoted', { type: Type.MAP_VALUE, node: 'So does this quoted scalar. ' }
'quoted', { type: Type.MAP_VALUE, node: 'So does this quoted scalar.\n' }
] } ] } ]
},
},
Expand Down
27 changes: 23 additions & 4 deletions __tests__/ast/corner-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ describe('folded block with chomp: keep', () => {
})
})

test('multiple linebreaks in plain scalar', () => {
const src = `trimmed\n\n\n\nlines\n`
const doc = parse(src)[0]
expect(doc.contents[0].strValue).toBe('trimmed\n\n\nlines')
describe('multiple linebreaks in scalars', () => {
test('plain', () => {
const src = `trimmed\n\n\n\nlines\n`
const doc = parse(src)[0]
expect(doc.contents[0].strValue).toBe('trimmed\n\n\nlines')
})

test('single-quoted', () => {
const src = `'trimmed\n\n\n\nlines'\n`
const doc = parse(src)[0]
expect(doc.contents[0].strValue).toBe('trimmed\n\n\nlines')
})
})

test('no null document for document-end marker', () => {
Expand All @@ -44,3 +52,14 @@ test('seq with anchor as explicit key', () => {
expect(doc.contents).toHaveLength(1)
expect(doc.contents[0].items[0].node.rawValue).toBe('- a')
})

test('unindented single-quoted string', () => {
const src = `key: 'two\nlines'\n`
const doc = parse(src)[0]
const { node } = doc.contents[0].items[1]
expect(node.error).toBeNull()
expect(node.strValue).toMatchObject({
str: 'two lines',
errors: [new SyntaxError('Multi-line single-quoted string needs to be sufficiently indented')]
})
})
1 change: 0 additions & 1 deletion __tests__/yaml-test-suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const skipErrors = [
'9C9N',
'9KBC',
'B63P',
'QB6E',
'SY6V',
'ZCZ6',
'ZL4Z',
Expand Down
30 changes: 30 additions & 0 deletions src/ast/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,36 @@ export default class Node {
: Node.endOfWhiteSpace(src, offset)
}

// fold single newline into space, multiple newlines to N - 1 newlines
// presumes src[offset] === '\n'
static foldNewline (src, offset, indent) {
let inCount = 0
let error = false
let fold = ''
let ch = src[offset + 1]
while (ch === ' ' || ch === '\t' || ch === '\n') {
switch (ch) {
case '\n':
inCount = 0
offset += 1
fold += '\n'
break
case '\t':
if (inCount <= indent) error = true
offset = Node.endOfWhiteSpace(src, offset + 2) - 1
break
case ' ':
inCount += 1
offset += 1
break
}
ch = src[offset + 1]
}
if (!fold) fold = ' '
if (ch && inCount <= indent) error = true
return { fold, offset, error }
}

constructor (type, props, context) {
this.context = context || null
this.error = null
Expand Down
15 changes: 3 additions & 12 deletions src/ast/PlainValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,9 @@ export default class PlainValue extends Node {
for (let i = start; i < end; ++i) {
let ch = src[i]
if (ch === '\n') {
// fold single newline into space, multiple newlines to N - 1 newlines
let nlCount = 1
ch = src[i + 1]
while (ch === ' ' || ch === '\t' || ch === '\n') {
if (ch === '\n') {
++nlCount
str += '\n'
}
i += 1
ch = src[i + 1]
}
if (nlCount === 1) str += ' '
const { fold, offset } = Node.foldNewline(src, i, -1)
str += fold
i = offset
} else if (ch === ' ' || ch === '\t') {
// trim trailing whitespace
const wsStart = i
Expand Down
16 changes: 6 additions & 10 deletions src/ast/QuoteDouble.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default class QuoteDouble extends Node {
if (!this.valueRange || !this.context) return null
const errors = []
const { start, end } = this.valueRange
const { src } = this.context
const { indent, src } = this.context
if (src[end - 1] !== '"') errors.push(new SyntaxError('Missing closing "quote'))
// Using String#replace is too painful with escaped newlines preceded by
// escaped backslashes; also, this should be faster.
Expand All @@ -40,15 +40,11 @@ export default class QuoteDouble extends Node {
if (ch === '\n') {
if (Node.atDocumentBoundary(src, i + 1)) errors.push(new SyntaxError(
'Document boundary indicators are not allowed within string values'))
// fold single newline into space, multiple newlines to just one
let nlCount = 1
ch = src[i + 1]
while (ch === ' ' || ch === '\t' || ch === '\n') {
if (ch === '\n') ++nlCount
i += 1
ch = src[i + 1]
}
str += nlCount > 1 ? '\n' : ' '
const { fold, offset, error } = Node.foldNewline(src, i, indent)
str += fold
i = offset
if (error) errors.push(new SyntaxError(
'Multi-line double-quoted string needs to be sufficiently indented'))
} else if (ch === '\\') {
i += 1
switch (src[i]) {
Expand Down
37 changes: 29 additions & 8 deletions src/ast/QuoteSingle.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,36 @@ export default class QuoteSingle extends Node {
if (!this.valueRange || !this.context) return null
const errors = []
const { start, end } = this.valueRange
const { src } = this.context
const { indent, src } = this.context
if (src[end - 1] !== "'") errors.push(new SyntaxError('Missing closing \'quote'))
const raw = src.slice(start + 1, end - 1)
if (/\n(?:---|\.\.\.)(?:[\n\t ]|$)/.test(raw)) errors.push(new SyntaxError(
'Document boundary indicators are not allowed within string values'))
const str = raw
.replace(/''/g, "'")
.replace(/[ \t]*\n[ \t]*/g, '\n')
.replace(/\n+/g, nl => nl.length === 1 ? ' ' : '\n')
let str = ''
for (let i = start + 1; i < end - 1; ++i) {
let ch = src[i]
if (ch === '\n') {
if (Node.atDocumentBoundary(src, i + 1)) errors.push(new SyntaxError(
'Document boundary indicators are not allowed within string values'))
const { fold, offset, error } = Node.foldNewline(src, i, indent)
str += fold
i = offset
if (error) errors.push(new SyntaxError(
'Multi-line single-quoted string needs to be sufficiently indented'))
} else if (ch === "'") {
str += ch
i += 1
if (src[i] !== "'") errors.push(new SyntaxError('Unescaped single quote? This should not happen.'))
} else if (ch === ' ' || ch === '\t') {
// trim trailing whitespace
const wsStart = i
let next = src[i + 1]
while (next === ' ' || next === '\t') {
i += 1
next = src[i + 1]
}
if (next !== '\n') str += i > wsStart ? src.slice(wsStart, i + 1) : ch
} else {
str += ch
}
}
return errors.length > 0 ? { errors, str } : str
}

Expand Down

0 comments on commit 22e4f65

Please sign in to comment.