Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: eemeli/yaml
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.6.0
Choose a base ref
...
head repository: eemeli/yaml
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.7.0
Choose a head ref
  • 18 commits
  • 37 files changed
  • 1 contributor

Commits on Jul 11, 2019

  1. Update dependencies

    eemeli committed Jul 11, 2019
    Copy the full SHA
    ee10b83 View commit details

Commits on Jul 29, 2019

  1. Copy the full SHA
    2c60bff View commit details
  2. Verified

    This commit was signed with the committer’s verified signature.
    BobbyMcWho Bobby McDonald
    Copy the full SHA
    d381e6e View commit details

Commits on Aug 19, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5144152 View commit details
  2. Copy the full SHA
    c8a5c8f View commit details
  3. Copy the full SHA
    a4df32e View commit details

Commits on Aug 28, 2019

  1. Copy the full SHA
    739fa4d View commit details
  2. Update dependencies

    eemeli committed Aug 28, 2019
    Copy the full SHA
    621e544 View commit details

Commits on Sep 9, 2019

  1. Copy the full SHA
    884fe07 View commit details

Commits on Sep 10, 2019

  1. Copy the full SHA
    56dfc5c View commit details

Commits on Sep 21, 2019

  1. Copy the full SHA
    c6adfa8 View commit details
  2. Copy the full SHA
    4fa45d0 View commit details
  3. Copy the full SHA
    44e27d8 View commit details
  4. Copy the full SHA
    499f97d View commit details

Commits on Sep 22, 2019

  1. Copy the full SHA
    75e0841 View commit details
  2. Copy the full SHA
    ec91de9 View commit details

Commits on Sep 25, 2019

  1. Add simpleKeys option (#122)

    eemeli authored Sep 25, 2019
    Copy the full SHA
    440aadc View commit details
  2. 1.7.0

    eemeli committed Sep 25, 2019
    Copy the full SHA
    40acaa7 View commit details
2 changes: 1 addition & 1 deletion docs
2 changes: 1 addition & 1 deletion map.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('./dist/schema/Map').default
require('./dist/deprecation').warnFileDeprecation(__filename)
require('./dist/warnings').warnFileDeprecation(__filename)
3,045 changes: 1,074 additions & 1,971 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "yaml",
"version": "1.6.0",
"version": "1.7.0",
"license": "ISC",
"author": "Eemeli Aro <eemeli@gmail.com>",
"repository": "github:eemeli/yaml",
@@ -66,25 +66,25 @@
"singleQuote": true
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.8.0",
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-plugin-trace": "^1.1.0",
"common-tags": "^1.8.0",
"cpy-cli": "^2.0.0",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.3.0",
"eslint": "^6.2.2",
"eslint-config-prettier": "^6.1.0",
"eslint-plugin-prettier": "^3.1.0",
"fast-check": "^1.15.1",
"jest": "^24.8.0",
"prettier": "^1.17.1"
"fast-check": "^1.16.2",
"jest": "^24.9.0",
"prettier": "^1.18.2"
},
"dependencies": {
"@babel/runtime": "^7.4.5"
"@babel/runtime": "^7.5.5"
},
"engines": {
"node": ">= 6"
2 changes: 1 addition & 1 deletion pair.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('./dist/schema/Pair').default
require('./dist/deprecation').warnFileDeprecation(__filename)
require('./dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion scalar.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('./dist/schema/Scalar').default
require('./dist/deprecation').warnFileDeprecation(__filename)
require('./dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion schema.js
Original file line number Diff line number Diff line change
@@ -4,4 +4,4 @@ module.exports.nullOptions = opt.nullOptions
module.exports.strOptions = opt.strOptions
module.exports.stringify = require('./dist/stringify').stringifyString

require('./dist/deprecation').warnFileDeprecation(__filename)
require('./dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion seq.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('./dist/schema/Seq').default
require('./dist/deprecation').warnFileDeprecation(__filename)
require('./dist/warnings').warnFileDeprecation(__filename)
18 changes: 13 additions & 5 deletions src/Document.js
Original file line number Diff line number Diff line change
@@ -139,11 +139,19 @@ export default class Document {
}
}

setSchema() {
if (!this.schema)
this.schema = new Schema(
Object.assign({}, this.getDefaults(), this.options)
)
setSchema(id, customTags) {
if (!id && !customTags && this.schema) return
if (typeof id === 'number') id = id.toFixed(1)
if (id === '1.0' || id === '1.1' || id === '1.2') {
if (this.version) this.version = id
else this.options.version = id
delete this.options.schema
} else if (id && typeof id === 'string') {
this.options.schema = id
}
if (Array.isArray(customTags)) this.options.customTags = customTags
const opt = Object.assign({}, this.getDefaults(), this.options)
this.schema = new Schema(opt)
}

parse(node, prevDoc) {
2 changes: 1 addition & 1 deletion src/cst/Node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Char, Type } from '../constants'
import getLinePos from './getLinePos'
import { getLinePos } from './source-utils'
import Range from './Range'

/** Root class of all nodes */
3 changes: 2 additions & 1 deletion src/cst/PlainValue.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,8 @@ export default class PlainValue extends Node {
const next = src[offset + 1]
if (
ch === ':' &&
(next === '\n' ||
(!next ||
next === '\n' ||
next === '\t' ||
next === ' ' ||
(inFlow && next === ','))
53 changes: 0 additions & 53 deletions src/cst/getLinePos.js

This file was deleted.

132 changes: 132 additions & 0 deletions src/cst/source-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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
}

function getSrcInfo(cst) {
let lineStarts, src
if (typeof cst === 'string') {
lineStarts = findLineStarts(cst)
src = cst
} else {
if (Array.isArray(cst)) cst = cst[0]
if (cst && cst.context) {
if (!cst.lineStarts) cst.lineStarts = findLineStarts(cst.context.src)
lineStarts = cst.lineStarts
src = cst.context.src
}
}
return { lineStarts, src }
}

/**
* @typedef {Object} LinePos - One-indexed position in the source
* @property {number} line
* @property {number} col
*/

/**
* 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 one-indexed `{ line, col }` location if found, or
* `undefined` otherwise.
*
* @param {number} offset
* @param {string|Document|Document[]} cst
* @returns {?LinePos}
*/
export function getLinePos(offset, cst) {
if (typeof offset !== 'number' || offset < 0) return null
const { lineStarts, src } = getSrcInfo(cst)
if (!lineStarts || !src || offset > src.length) return null
for (let i = 0; i < lineStarts.length; ++i) {
const start = lineStarts[i]
if (offset < start) {
return { line: i, col: offset - lineStarts[i - 1] + 1 }
}
if (offset === start) return { line: i + 1, col: 1 }
}
const line = lineStarts.length
return { line, col: offset - lineStarts[line - 1] + 1 }
}

/**
* Get a specified line from the source.
*
* 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 the line as a string if found, or `null` otherwise.
*
* @param {number} line One-indexed line number
* @param {string|Document|Document[]} cst
* @returns {?string}
*/
export function getLine(line, cst) {
const { lineStarts, src } = getSrcInfo(cst)
if (!lineStarts || !(line >= 1) || line > lineStarts.length) return null
const start = lineStarts[line - 1]
let end = lineStarts[line] // undefined for last line; that's ok for slice()
while (end && end > start && src[end - 1] === '\n') --end
return src.slice(start, end)
}

/**
* Pretty-print the starting line from the source indicated by the range `pos`
*
* Trims output to `maxWidth` chars while keeping the starting column visible,
* using `…` at either end to indicate dropped characters.
*
* Returns a two-line string (or `null`) with `\n` as separator; the second line
* will hold appropriately indented `^` marks indicating the column range.
*
* @param {Object} pos
* @param {LinePos} pos.start
* @param {LinePos} [pos.end]
* @param {string|Document|Document[]*} cst
* @param {number} [maxWidth=80]
* @returns {?string}
*/
export function getPrettyContext({ start, end }, cst, maxWidth = 80) {
let src = getLine(start.line, cst)
if (!src) return null
let { col } = start
if (src.length > maxWidth) {
if (col <= maxWidth - 10) {
src = src.substr(0, maxWidth - 1) + '…'
} else {
const halfWidth = Math.round(maxWidth / 2)
if (src.length > col + halfWidth)
src = src.substr(0, col + halfWidth - 1) + '…'
col -= src.length - maxWidth
src = '…' + src.substr(1 - maxWidth)
}
}
let errLen = 1
let errEnd = ''
if (end) {
if (
end.line === start.line &&
col + (end.col - start.col) <= maxWidth + 1
) {
errLen = end.col - start.col
} else {
errLen = Math.min(src.length + 1, maxWidth) - col
errEnd = '…'
}
}
const offset = col > 1 ? ' '.repeat(col - 1) : ''
const err = '^'.repeat(errLen)
return `${src}\n${offset}${err}${errEnd}`
}
24 changes: 21 additions & 3 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Node from './cst/Node'
import { getLinePos, getPrettyContext } from './cst/source-utils'
import Range from './cst/Range'

export class YAMLError extends Error {
constructor(name, source, message) {
@@ -11,12 +13,28 @@ export class YAMLError extends Error {
}

makePretty() {
if (this.source) {
this.nodeType = this.source.type
if (!this.source) return
this.nodeType = this.source.type
const cst = this.source.context && this.source.context.root
if (typeof this.offset === 'number') {
this.range = new Range(this.offset, this.offset + 1)
const start = cst && getLinePos(this.offset, cst)
if (start) {
const end = { line: start.line, col: start.col + 1 }
this.linePos = { start, end }
}
delete this.offset
} else {
this.range = this.source.range
this.linePos = this.source.rangeAsLinePos
delete this.source
}
if (this.linePos) {
const { line, col } = this.linePos.start
this.message += ` at line ${line}, column ${col}`
const ctx = cst && getPrettyContext(this.linePos, cst)
if (ctx) this.message += `:\n\n${ctx}\n`
}
delete this.source
}
}

7 changes: 3 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* global console */

import parseCST from './cst/parse'
import YAMLDocument from './Document'
import { YAMLSemanticError } from './errors'
import Schema from './schema'
import { warn } from './warnings'

const defaultOptions = {
anchorPrefix: 'a',
@@ -14,6 +13,7 @@ const defaultOptions = {
mapAsMap: false,
maxAliasCount: 100,
prettyErrors: false, // TODO Set true in v2
simpleKeys: false,
version: '1.2'
}

@@ -62,8 +62,7 @@ function parseDocument(src, options) {

function parse(src, options) {
const doc = parseDocument(src, options)
// eslint-disable-next-line no-console
doc.warnings.forEach(warning => console.warn(warning))
doc.warnings.forEach(warning => warn(warning))
if (doc.errors.length > 0) throw doc.errors[0]
return doc.toJSON()
}
2 changes: 1 addition & 1 deletion src/schema/Alias.js
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ export default class Alias extends Node {
if (!ctx) return toJSON(this.source, arg, ctx)
const { anchors, maxAliasCount } = ctx
const anchor = anchors.find(a => a.node === this.source)
if (!anchor || !anchor.res) {
if (!anchor || anchor.res === undefined) {
const msg = 'This should not happen: Alias anchor was not resolved?'
if (this.cstNode) throw new YAMLReferenceError(this.cstNode, msg)
else throw new ReferenceError(msg)
2 changes: 1 addition & 1 deletion src/schema/Merge.js
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ export default class Merge extends Pair {
} else if (map instanceof Set) {
map.add(key)
} else {
if (!map.hasOwnProperty(key)) map[key] = value
if (!Object.prototype.hasOwnProperty.call(map, key)) map[key] = value
}
}
}
21 changes: 19 additions & 2 deletions src/schema/Pair.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Published as 'yaml/pair'

import addComment from '../addComment'
import { Type } from '../constants'
import toJSON from '../toJSON'
import Collection from './Collection'
import Node from './Node'
@@ -58,9 +59,25 @@ export default class Pair extends Node {

toString(ctx, onComment, onChompKeep) {
if (!ctx || !ctx.doc) return JSON.stringify(this)
const { simpleKeys } = ctx.doc.options
let { key, value } = this
let keyComment = key instanceof Node && key.comment
const explicitKey = !key || keyComment || key instanceof Collection
if (simpleKeys) {
if (keyComment) {
throw new Error('With simple keys, key nodes cannot have comments')
}
if (key instanceof Collection) {
const msg = 'With simple keys, collection cannot be used as a key value'
throw new Error(msg)
}
}
const explicitKey =
!simpleKeys &&
(!key ||
keyComment ||
key instanceof Collection ||
key.type === Type.BLOCK_FOLDED ||
key.type === Type.BLOCK_LITERAL)
const { doc, indent } = ctx
ctx = Object.assign({}, ctx, {
implicitKey: !explicitKey,
@@ -74,7 +91,7 @@ export default class Pair extends Node {
() => (chompKeep = true)
)
str = addComment(str, ctx.indent, keyComment)
if (ctx.allNullValues) {
if (ctx.allNullValues && !simpleKeys) {
if (this.comment) {
str = addComment(str, ctx.indent, this.comment)
if (onComment) onComment()
4 changes: 2 additions & 2 deletions src/schema/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { warnOptionDeprecation } from '../deprecation'
import { warnOptionDeprecation } from '../warnings'
import { Type } from '../constants'
import { YAMLReferenceError, YAMLWarning } from '../errors'
import { stringifyString } from '../stringify'
@@ -162,7 +162,7 @@ export default class Schema {

resolveNodeWithFallback(doc, node, tagName) {
const res = this.resolveNode(doc, node, tagName)
if (node.hasOwnProperty('resolved')) return res
if (Object.prototype.hasOwnProperty.call(node, 'resolved')) return res
const fallback = isMap(node)
? Schema.defaultTags.MAP
: isSeq(node)
32 changes: 21 additions & 11 deletions src/schema/parseMap.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Char, Type } from '../constants'
import PlainValue from '../cst/PlainValue'
import { YAMLSemanticError, YAMLSyntaxError } from '../errors'
import { YAMLSemanticError, YAMLSyntaxError, YAMLWarning } from '../errors'
import Map from './Map'
import Merge, { MERGE_KEY } from './Merge'
import Pair from './Pair'
import { checkKeyLength, resolveComments } from './parseUtils'
import {
checkFlowCollectionEnd,
checkKeyLength,
resolveComments
} from './parseUtils'
import Alias from './Alias'
import Collection from './Collection'

export default function parseMap(doc, cst) {
if (cst.type !== Type.MAP && cst.type !== Type.FLOW_MAP) {
@@ -20,8 +25,10 @@ export default function parseMap(doc, cst) {
const map = new Map()
map.items = items
resolveComments(map, comments)
let hasCollectionKey = false
for (let i = 0; i < items.length; ++i) {
const { key: iKey } = items[i]
if (iKey instanceof Collection) hasCollectionKey = true
if (doc.schema.merge && iKey && iKey.value === MERGE_KEY) {
items[i] = new Merge(items[i])
const sources = items[i].value.items
@@ -44,7 +51,7 @@ export default function parseMap(doc, cst) {
iKey === jKey ||
(iKey &&
jKey &&
iKey.hasOwnProperty('value') &&
Object.prototype.hasOwnProperty.call(iKey, 'value') &&
iKey.value === jKey.value)
) {
const msg = `Map keys must be unique; "${iKey}" is repeated`
@@ -54,6 +61,11 @@ export default function parseMap(doc, cst) {
}
}
}
if (hasCollectionKey && !doc.options.mapAsMap) {
const warn =
'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'
doc.warnings.push(new YAMLWarning(cst, warn))
}
cst.resolved = map
return map
}
@@ -190,7 +202,7 @@ function resolveFlowMapItems(doc, cst) {
checkKeyLength(doc.errors, cst, i, key, keyStart)
const item = cst.items[i]
if (typeof item.char === 'string') {
const { char } = item
const { char, offset } = item
if (char === '?' && key === undefined && !explicitKey) {
explicitKey = true
next = ':'
@@ -223,9 +235,10 @@ function resolveFlowMapItems(doc, cst) {
next = ':'
continue
}
doc.errors.push(
new YAMLSyntaxError(cst, `Flow map contains an unexpected ${char}`)
)
const msg = `Flow map contains an unexpected ${char}`
const err = new YAMLSyntaxError(cst, msg)
err.offset = offset
doc.errors.push(err)
} else if (item.type === Type.BLANK_LINE) {
comments.push({ afterKey: !!key, before: items.length })
} else if (item.type === Type.COMMENT) {
@@ -252,10 +265,7 @@ function resolveFlowMapItems(doc, cst) {
explicitKey = false
}
}
if (cst.items[cst.items.length - 1].char !== '}')
doc.errors.push(
new YAMLSemanticError(cst, 'Expected flow map to end with }')
)
checkFlowCollectionEnd(doc.errors, cst)
if (key !== undefined) items.push(new Pair(key))
return { comments, items }
}
37 changes: 25 additions & 12 deletions src/schema/parseSeq.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Type } from '../constants'
import { YAMLSemanticError, YAMLSyntaxError } from '../errors'
import { YAMLSemanticError, YAMLSyntaxError, YAMLWarning } from '../errors'
import Pair from './Pair'
import { checkKeyLength, resolveComments } from './parseUtils'
import {
checkFlowCollectionEnd,
checkKeyLength,
resolveComments
} from './parseUtils'
import Seq from './Seq'
import Collection from './Collection'

export default function parseSeq(doc, cst) {
if (cst.type !== Type.SEQ && cst.type !== Type.FLOW_SEQ) {
@@ -17,6 +22,14 @@ export default function parseSeq(doc, cst) {
const seq = new Seq()
seq.items = items
resolveComments(seq, comments)
if (
!doc.options.mapAsMap &&
items.some(it => it instanceof Pair && it.key instanceof Collection)
) {
const warn =
'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'
doc.warnings.push(new YAMLWarning(cst, warn))
}
cst.resolved = seq
return seq
}
@@ -62,7 +75,7 @@ function resolveFlowSeqItems(doc, cst) {
for (let i = 0; i < cst.items.length; ++i) {
const item = cst.items[i]
if (typeof item.char === 'string') {
const { char } = item
const { char, offset } = item
if (char !== ':' && (explicitKey || key !== undefined)) {
if (explicitKey && key === undefined) key = next ? items.pop() : null
items.push(new Pair(key))
@@ -78,9 +91,10 @@ function resolveFlowSeqItems(doc, cst) {
if (next === ',') {
key = items.pop()
if (key instanceof Pair) {
const msg =
'Chaining flow sequence pairs is invalid (e.g. [ a : b : c ])'
doc.errors.push(new YAMLSemanticError(char, msg))
const msg = 'Chaining flow sequence pairs is invalid'
const err = new YAMLSemanticError(cst, msg)
err.offset = offset
doc.errors.push(err)
}
if (!explicitKey) checkKeyLength(doc.errors, cst, i, key, keyStart)
} else {
@@ -91,15 +105,17 @@ function resolveFlowSeqItems(doc, cst) {
next = null
} else if (next === '[' || char !== ']' || i < cst.items.length - 1) {
const msg = `Flow sequence contains an unexpected ${char}`
doc.errors.push(new YAMLSyntaxError(cst, msg))
const err = new YAMLSyntaxError(cst, msg)
err.offset = offset
doc.errors.push(err)
}
} else if (item.type === Type.BLANK_LINE) {
comments.push({ before: items.length })
} else if (item.type === Type.COMMENT) {
comments.push({ comment: item.comment, before: items.length })
} else {
if (next) {
const msg = `Expected a ${next} here in flow sequence`
const msg = `Expected a ${next} in flow sequence`
doc.errors.push(new YAMLSemanticError(item, msg))
}
const value = doc.resolveNode(item)
@@ -113,10 +129,7 @@ function resolveFlowSeqItems(doc, cst) {
next = ','
}
}
if (cst.items[cst.items.length - 1].char !== ']')
doc.errors.push(
new YAMLSemanticError(cst, 'Expected flow sequence to end with ]')
)
checkFlowCollectionEnd(doc.errors, cst)
if (key !== undefined) items.push(new Pair(key))
return { comments, items }
}
41 changes: 41 additions & 0 deletions src/schema/parseUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,45 @@
import { YAMLSemanticError } from '../errors'
import { Type } from '../constants'

export function checkFlowCollectionEnd(errors, cst) {
let char, name
switch (cst.type) {
case Type.FLOW_MAP:
char = '}'
name = 'flow map'
break
case Type.FLOW_SEQ:
char = ']'
name = 'flow sequence'
break
default:
errors.push(new YAMLSemanticError(cst, 'Not a flow collection!?'))
return
}

let lastItem
for (let i = cst.items.length - 1; i >= 0; --i) {
const item = cst.items[i]
if (!item || item.type !== Type.COMMENT) {
lastItem = item
break
}
}

if (lastItem && lastItem.char !== char) {
const msg = `Expected ${name} to end with ${char}`
let err
if (typeof lastItem.offset === 'number') {
err = new YAMLSemanticError(cst, msg)
err.offset = lastItem.offset + 1
} else {
err = new YAMLSemanticError(lastItem, msg)
if (lastItem.range && lastItem.range.end)
err.offset = lastItem.range.end - lastItem.range.start
}
errors.push(err)
}
}

export function checkKeyLength(errors, node, itemIdx, key, keyStart) {
if (!key || typeof keyStart !== 'number') return
11 changes: 4 additions & 7 deletions src/stringify.js
Original file line number Diff line number Diff line change
@@ -247,13 +247,10 @@ function plainString(item, ctx, onComment, onChompKeep) {
return blockString(item, ctx, onComment, onChompKeep)
}
const str = value.replace(/\n+/g, `$&\n${indent}`)
// May need to verify that output will be parsed as a string, as plain numbers
// and booleans get parsed with those types, e.g. '42', 'true' & '0.9e-3'.
if (
actualString &&
/^[\w.+-]+$/.test(str) &&
typeof tags.resolveScalar(str).value !== 'string'
) {
// Verify that output will be parsed as a string, as e.g. plain numbers and
// booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
// and others in v1.1.
if (actualString && typeof tags.resolveScalar(str).value !== 'string') {
return doubleQuotedString(value, ctx)
}
const body = implicitKey
20 changes: 13 additions & 7 deletions src/deprecation.js → src/warnings.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/* global global, console */

function warn(msg) {
if (global && global.process && global.process.emitWarning) {
global.process.emitWarning(msg, 'DeprecationWarning')
} else {
export function warn(warning, type) {
if (global && global._YAML_SILENCE_WARNINGS) return
const { emitWarning } = global && global.process
// This will throw in Jest if `warning` is an Error instance due to
// https://github.com/facebook/jest/issues/2549
if (emitWarning) emitWarning(warning, type)
else {
// eslint-disable-next-line no-console
console.warn(`DeprecationWarning: ${msg}`)
console.warn(type ? `${type}: ${warning}` : warning)
}
}

@@ -15,7 +18,10 @@ export function warnFileDeprecation(filename) {
.replace(/.*yaml[/\\]/i, '')
.replace(/\.js$/, '')
.replace(/\\/g, '/')
warn(`The endpoint 'yaml/${path}' will be removed in a future release.`)
warn(
`The endpoint 'yaml/${path}' will be removed in a future release.`,
'DeprecationWarning'
)
}

const warned = {}
@@ -25,5 +31,5 @@ export function warnOptionDeprecation(name, alternative) {
warned[name] = true
let msg = `The option '${name}' will be removed in a future release`
msg += alternative ? `, use '${alternative}' instead.` : '.'
warn(msg)
warn(msg, 'DeprecationWarning')
}
12 changes: 12 additions & 0 deletions tests/cst/corner-cases.js
Original file line number Diff line number Diff line change
@@ -202,6 +202,18 @@ describe('collection indicator as last char', () => {
]
})
})

test('implicit map value separator', () => {
const src = 'a:'
const doc = parse(src)[0]
expect(doc.contents[0]).toMatchObject({
type: 'MAP',
items: [
{ type: 'PLAIN', strValue: 'a' },
{ type: 'MAP_VALUE', node: null }
]
})
})
})

test('parse an empty string as an empty document', () => {
14 changes: 7 additions & 7 deletions tests/cst/getLinePos.js → tests/cst/source-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getLinePos from '../../src/cst/getLinePos'
import { getLinePos } from '../../src/cst/source-utils'
import parse from '../../src/cst/parse'

test('lineStarts for empty document', () => {
@@ -40,10 +40,10 @@ test('getLinePos()', () => {
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()
expect(getLinePos()).toBeNull()
expect(getLinePos(0)).toBeNull()
expect(getLinePos(1)).toBeNull()
expect(getLinePos(-1, cst)).toBeNull()
expect(getLinePos(13, cst)).toBeNull()
expect(getLinePos(Math.MAXINT, cst)).toBeNull()
})
2 changes: 1 addition & 1 deletion tests/doc/YAML-1.1.spec.js
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ test('Use preceding directives if none defined', () => {
---
!bar "Using previous YAML directive"
`
const docs = YAML.parseAllDocuments(src, { prettyErrors: true })
const docs = YAML.parseAllDocuments(src, { prettyErrors: false })
expect(docs).toHaveLength(5)
expect(docs.map(doc => doc.errors)).toMatchObject([[], [], [], [], []])
const warn = tag => ({
15 changes: 11 additions & 4 deletions tests/doc/YAML-1.2.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import YAML from '../../src/index'
import { strOptions } from '../../src/tags/options'

const collectionKeyWarning =
'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'

const spec = {
'2.1. Collections': {
'Example 2.1. Sequence of Scalars': {
@@ -199,7 +202,8 @@ rbi:
'2001-08-14'
]
}
]
],
warnings: [[collectionKeyWarning]]
},

'Example 2.12. Compact Nested Mapping': {
@@ -913,7 +917,8 @@ Chomping: |
65
avg: # Average
0.278`,
tgt: [{ '{ first: Sammy, last: Sosa }': { hr: 65, avg: 0.278 } }]
tgt: [{ '{ first: Sammy, last: Sosa }': { hr: 65, avg: 0.278 } }],
warnings: [[collectionKeyWarning]]
}
},
'6.8. Directives': {
@@ -1424,7 +1429,8 @@ foo: bar
[{ '': 'empty key entry' }],
[{ '{ JSON: like }': 'adjacent' }]
]
]
],
warnings: [[collectionKeyWarning]]
},

'Example 7.22. Invalid Implicit Keys': {
@@ -1681,7 +1687,8 @@ last line
src: `- sun: yellow
- ? earth: blue
: moon: white\n`,
tgt: [[{ sun: 'yellow' }, { '{ earth: blue }': { moon: 'white' } }]]
tgt: [[{ sun: 'yellow' }, { '{ earth: blue }': { moon: 'white' } }]],
warnings: [[collectionKeyWarning]]
}
},

74 changes: 55 additions & 19 deletions tests/doc/errors.js
Original file line number Diff line number Diff line change
@@ -46,15 +46,47 @@ describe('eemeli/yaml#7', () => {
})
})

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: 1, col: 1 },
end: { line: 1, col: 2 }
describe('missing flow collection terminator', () => {
test('start only of flow map (eemeli/yaml#8)', () => {
const doc = YAML.parseDocument('{', { prettyErrors: true })
expect(doc.errors).toMatchObject([
{
name: 'YAMLSemanticError',
message:
'Expected flow map to end with } at line 1, column 2:\n\n{\n ^\n',
nodeType: 'FLOW_MAP',
range: { start: 1, end: 2 },
linePos: { start: { line: 1, col: 2 }, end: { line: 1, col: 3 } }
}
])
})

test('start only of flow sequence (eemeli/yaml#8)', () => {
const doc = YAML.parseDocument('[', { prettyErrors: true })
expect(doc.errors).toMatchObject([
{
name: 'YAMLSemanticError',
message:
'Expected flow sequence to end with ] at line 1, column 2:\n\n[\n ^\n',
nodeType: 'FLOW_SEQ',
range: { start: 1, end: 2 },
linePos: { start: { line: 1, col: 2 }, end: { line: 1, col: 3 } }
}
])
})

test('flow sequence without end', () => {
const doc = YAML.parseDocument('[ foo, bar,', { prettyErrors: true })
expect(doc.errors).toMatchObject([
{
name: 'YAMLSemanticError',
message:
'Expected flow sequence to end with ] at line 1, column 12:\n\n[ foo, bar,\n ^\n',
nodeType: 'FLOW_SEQ',
range: { start: 11, end: 12 },
linePos: { start: { line: 1, col: 12 }, end: { line: 1, col: 13 } }
}
])
})
})

@@ -65,7 +97,8 @@ describe('pretty errors', () => {
expect(doc.errors).toMatchObject([
{
name: 'YAMLSemanticError',
message: 'Implicit map keys need to be followed by map values',
message:
'Implicit map keys need to be followed by map values at line 2, column 1:\n\ndef\n^^^\n',
nodeType: 'PLAIN',
range: { start: 9, end: 12 },
linePos: { start: { line: 2, col: 1 }, end: { line: 2, col: 4 } }
@@ -80,27 +113,30 @@ describe('pretty errors', () => {
expect(docs[0].errors).toMatchObject([
{
name: 'YAMLSyntaxError',
message: 'Flow map contains an unexpected ,',
message:
'Flow map contains an unexpected , at line 1, column 3:\n\n{ , }\n ^\n',
nodeType: 'FLOW_MAP',
range: { start: 0, end: 5 },
linePos: { start: { line: 1, col: 1 }, end: { line: 1, col: 6 } }
range: { start: 2, end: 3 },
linePos: { start: { line: 1, col: 3 }, end: { line: 1, col: 4 } }
}
])
expect(docs[0].errors[0]).not.toHaveProperty('source')
expect(docs[1].errors).toMatchObject([
{
name: 'YAMLSyntaxError',
message: 'Flow map contains an unexpected ,',
message:
'Flow map contains an unexpected , at line 3, column 7:\n\n{ 123,,, }\n ^\n',
nodeType: 'FLOW_MAP',
range: { start: 10, end: 20 },
linePos: { start: { line: 3, col: 1 }, end: { line: 3, col: 11 } }
range: { start: 16, end: 17 },
linePos: { start: { line: 3, col: 7 }, end: { line: 3, col: 8 } }
},
{
name: 'YAMLSyntaxError',
message: 'Flow map contains an unexpected ,',
message:
'Flow map contains an unexpected , at line 3, column 8:\n\n{ 123,,, }\n ^\n',
nodeType: 'FLOW_MAP',
range: { start: 10, end: 20 },
linePos: { start: { line: 3, col: 1 }, end: { line: 3, col: 11 } }
range: { start: 17, end: 18 },
linePos: { start: { line: 3, col: 8 }, end: { line: 3, col: 9 } }
}
])
expect(docs[1].errors[0]).not.toHaveProperty('source')
61 changes: 61 additions & 0 deletions tests/doc/parse.js
Original file line number Diff line number Diff line change
@@ -337,6 +337,15 @@ test('eemeli/yaml#38', () => {
})
})

test('eemeli/yaml#120', () => {
const src = `test:
- test1: test1
test2:`
expect(YAML.parse(src)).toEqual({
test: [{ test1: 'test1', test2: null }]
})
})

test('fake node should respect setOrigRanges()', () => {
const cst = YAML.parseCST('a:\r\n # 123')
expect(cst.setOrigRanges()).toBe(true)
@@ -399,6 +408,14 @@ describe('Excessive entity expansion attacks', () => {
const srcB = fs.readFileSync(path.resolve(root, 'billion-laughs.yml'), 'utf8')
const srcQ = fs.readFileSync(path.resolve(root, 'quadratic.yml'), 'utf8')

let origEmitWarning
beforeAll(() => {
origEmitWarning = process.emitWarning
})
afterAll(() => {
process.emitWarning = origEmitWarning
})

describe('Limit count by default', () => {
for (const [name, src] of [
['js-yaml case 1', src1],
@@ -407,18 +424,21 @@ describe('Excessive entity expansion attacks', () => {
['quadratic expansion', srcQ]
]) {
test(name, () => {
process.emitWarning = jest.fn()
expect(() => YAML.parse(src)).toThrow(/Excessive alias count/)
})
}
})

describe('Work sensibly even with disabled limits', () => {
test('js-yaml case 1', () => {
process.emitWarning = jest.fn()
const obj = YAML.parse(src1, { maxAliasCount: -1 })
expect(obj).toMatchObject({})
const key = Object.keys(obj)[0]
expect(key.length).toBeGreaterThan(2000)
expect(key.length).toBeLessThan(8000)
expect(process.emitWarning).toHaveBeenCalled()
})

test('js-yaml case 2', () => {
@@ -475,3 +495,44 @@ describe('Excessive entity expansion attacks', () => {
}
})
})

test('Anchor for empty node (6KGN)', () => {
const src = `a: &anchor\nb: *anchor`
expect(YAML.parse(src)).toMatchObject({ a: null, b: null })
})

describe('handling complex keys', () => {
let origEmitWarning
beforeAll(() => {
origEmitWarning = process.emitWarning
})
afterAll(() => {
process.emitWarning = origEmitWarning
})

test('add warning to doc when casting key in collection to string', () => {
const doc = YAML.parseDocument('[foo]: bar')
const message =
'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'
expect(doc.warnings).toMatchObject([{ message }])
})

test('do not add warning when using mapIsMap: true', () => {
const doc = YAML.parseDocument('[foo]: bar', { mapAsMap: true })
expect(doc.warnings).toMatchObject([])
})

test('warn when casting key in collection to string', () => {
process.emitWarning = jest.fn()
const obj = YAML.parse('[foo]: bar')
expect(Object.keys(obj)).toMatchObject(['[ foo ]'])
expect(process.emitWarning).toHaveBeenCalled()
})

test('warn when casting key in sequence to string', () => {
process.emitWarning = jest.fn()
const obj = YAML.parse('[ [foo]: bar ]')
expect(obj).toMatchObject([{ '[ foo ]': 'bar' }])
expect(process.emitWarning).toHaveBeenCalled()
})
})
84 changes: 71 additions & 13 deletions tests/doc/stringify.js
Original file line number Diff line number Diff line change
@@ -89,6 +89,21 @@ blah blah\n`)
})
})

describe('timestamp-like string (YAML 1.1)', () => {
for (const [name, str] of [
['canonical', '2001-12-15T02:59:43.1Z'],
['validIso8601', '2001-12-14t21:59:43.10-05:00'],
['spaceSeparated', '2001-12-14 21:59:43.10 -5'],
['noTimeZone', '2001-12-15 2:59:43.10']
]) {
test(name, () => {
const res = YAML.stringify(str, { version: '1.1' })
expect(res).toBe(`"${str}"\n`)
expect(YAML.parse(res, { version: '1.1' })).toBe(str)
})
}
})

describe('circular references', () => {
test('parent at root', () => {
const map = { foo: 'bar' }
@@ -166,25 +181,32 @@ test('array', () => {
)
})

test('object', () => {
const object = { x: 3, y: [4], z: { w: 'five', v: 6 } }
const str = YAML.stringify(object)
expect(str).toBe(
`x: 3
describe('maps', () => {
test('JS Object', () => {
const object = { x: 3, y: [4], z: { w: 'five', v: 6 } }
const str = YAML.stringify(object)
expect(str).toBe(
`x: 3
y:
- 4
z:
w: five
v: 6\n`
)
})
)
})

test('Map with non-Pair item', () => {
const doc = new YAML.Document()
doc.contents = YAML.createNode({ x: 3, y: 4 })
expect(String(doc)).toBe('x: 3\ny: 4\n')
doc.contents.items.push('TEST')
expect(() => String(doc)).toThrow(/^Map items must all be pairs.*TEST/)
test('Map with non-Pair item', () => {
const doc = new YAML.Document()
doc.contents = YAML.createNode({ x: 3, y: 4 })
expect(String(doc)).toBe('x: 3\ny: 4\n')
doc.contents.items.push('TEST')
expect(() => String(doc)).toThrow(/^Map items must all be pairs.*TEST/)
})

test('Keep block scalar types for keys', () => {
const doc = YAML.parseDocument('? >\n foo\n: bar')
expect(String(doc)).toBe('? >\n foo\n: bar\n')
})
})

test('eemeli/yaml#43: Quoting colons', () => {
@@ -308,3 +330,39 @@ test('eemeli/yaml#87', () => {
doc.set('test', { a: 'test' })
expect(String(doc)).toBe('test:\n a: test\n')
})

describe('simple keys', () => {
test('key with null value', () => {
const doc = YAML.parseDocument('~: ~')
expect(String(doc)).toBe('? null\n')
doc.options.simpleKeys = true
expect(String(doc)).toBe('null: null\n')
})

test('key with block scalar value', () => {
const doc = YAML.parseDocument('foo: bar')
doc.contents.items[0].key.type = 'BLOCK_LITERAL'
expect(String(doc)).toBe('? |-\n foo\n: bar\n')
doc.options.simpleKeys = true
expect(String(doc)).toBe('"foo": bar\n')
})

test('key with comment', () => {
const doc = YAML.parseDocument('foo: bar')
doc.contents.items[0].key.comment = 'FOO'
expect(String(doc)).toBe('? foo #FOO\n: bar\n')
doc.options.simpleKeys = true
expect(() => String(doc)).toThrow(
/With simple keys, key nodes cannot have comments/
)
})

test('key with collection value', () => {
const doc = YAML.parseDocument('[foo]: bar')
expect(String(doc)).toBe('? [ foo ]\n: bar\n')
doc.options.simpleKeys = true
expect(() => String(doc)).toThrow(
/With simple keys, collection cannot be used as a key value/
)
})
})
38 changes: 38 additions & 0 deletions tests/doc/types.js
Original file line number Diff line number Diff line change
@@ -643,3 +643,41 @@ invoice:
})
})
})

describe('schema changes', () => {
test('write as json', () => {
const doc = YAML.parseDocument('foo: bar', { schema: 'core' })
expect(doc.options.schema).toBe('core')
doc.setSchema('json')
expect(doc.options.schema).toBe('json')
expect(String(doc)).toBe('"foo": "bar"\n')
})

test('fail for missing type', () => {
const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', {
version: '1.1'
})
expect(doc.options.version).toBe('1.1')
doc.setSchema('1.2')
expect(doc.version).toBeNull()
expect(doc.options.version).toBe('1.2')
expect(doc.options.schema).toBeUndefined()
expect(() => String(doc)).toThrow(/Tag not resolved for Date value/)
})

test('set schema + custom tags', () => {
const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', {
version: '1.1'
})
doc.setSchema('json', ['timestamp'])
expect(String(doc)).toBe('"foo": 1971-02-03T12:13:14\n')
})

test('set version + custom tags', () => {
const doc = YAML.parseDocument('foo: 1971-02-03T12:13:14', {
version: '1.1'
})
doc.setSchema(1.2, ['timestamp'])
expect(String(doc)).toBe('foo: 1971-02-03T12:13:14\n')
})
})
2 changes: 1 addition & 1 deletion types/binary.js
Original file line number Diff line number Diff line change
@@ -4,4 +4,4 @@ Object.defineProperty(exports, '__esModule', { value: true })
exports.binary = require('../dist/tags/yaml-1.1/binary').default
exports.default = [exports.binary]

require('../dist/deprecation').warnFileDeprecation(__filename)
require('../dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion types/omap.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('../dist/tags/yaml-1.1/omap').default
require('../dist/deprecation').warnFileDeprecation(__filename)
require('../dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion types/pairs.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('../dist/tags/yaml-1.1/pairs').default
require('../dist/deprecation').warnFileDeprecation(__filename)
require('../dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion types/set.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
module.exports = require('../dist/tags/yaml-1.1/set').default
require('../dist/deprecation').warnFileDeprecation(__filename)
require('../dist/warnings').warnFileDeprecation(__filename)
2 changes: 1 addition & 1 deletion types/timestamp.js
Original file line number Diff line number Diff line change
@@ -7,4 +7,4 @@ exports.floatTime = ts.floatTime
exports.intTime = ts.intTime
exports.timestamp = ts.timestamp

require('../dist/deprecation').warnFileDeprecation(__filename)
require('../dist/warnings').warnFileDeprecation(__filename)