Skip to content

Commit

Permalink
Add options to specify string quoting
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Dec 9, 2020
1 parent e852066 commit 7b256d7
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Added `.mjs` (es modules) support.
- Added `quotingType` and `forceQuotes` options for dumper to configure
string literal style, #290, #529.

### Fixed
- Astral characters are no longer encoded by dump/safeDump, #587.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ options:
- `noArrayIndent` _(default: false)_ - when true, will not add an indentation level to array elements
- `skipInvalid` _(default: false)_ - do not throw on invalid types (like function
in the safe schema) and skip pairs and single values with such types.
- `flowLevel` (default: -1) - specifies level of nesting, when to switch from
- `flowLevel` _(default: -1)_ - specifies level of nesting, when to switch from
block to flow style for collections. -1 means block style everwhere
- `styles` - "tag" => "style" map. Each tag may have own set of styles.
- `schema` _(default: `DEFAULT_SCHEMA`)_ specifies a schema to use.
Expand All @@ -135,6 +135,8 @@ options:
- `noCompatMode` _(default: `false`)_ - if `true` don't try to be compatible with older
yaml versions. Currently: don't quote "yes", "no" and so on, as required for YAML 1.1
- `condenseFlow` _(default: `false`)_ - if `true` flow sequences will be condensed, omitting the space between `a, b`. Eg. `'[a,b]'`, and omitting the space between `key: value` and quoting the key. Eg. `'{"a":b}'` Can be useful when using yaml for pretty URL query params as spaces are %-encoded.
- `quotingType` _(`'` or `"`, default: `'`)_ - strings will be quoted using this quoting style. If you specify single quotes, double quotes will still be used for non-printable characters.
- `forceQuotes` _(default: `false`)_ - if `true`, all non-key strings will be quoted even if they normally don't need to.

The following table show availlable styles (e.g. "canonical",
"binary"...) available for each tag (.e.g. !!null, !!int ...). Yaml
Expand All @@ -149,7 +151,7 @@ output is shown on the right side after `=>` (default setting) or `->`:
!!int
"binary" -> "0b1", "0b101010", "0b1110001111010"
"octal" -> "01", "052", "016172"
"octal" -> "0o1", "0o52", "0o16172"
"decimal" => "1", "42", "7290"
"hexadecimal" -> "0x1", "0x2A", "0x1C7A"
Expand Down
31 changes: 23 additions & 8 deletions lib/dumper.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ function encodeHex(character) {
return '\\' + handle + common.repeat('0', length - string.length) + string;
}


var QUOTING_TYPE_SINGLE = 1,
QUOTING_TYPE_DOUBLE = 2;

function State(options) {
this.schema = options['schema'] || DEFAULT_SCHEMA;
this.indent = Math.max(1, (options['indent'] || 2));
Expand All @@ -117,6 +121,8 @@ function State(options) {
this.noRefs = options['noRefs'] || false;
this.noCompatMode = options['noCompatMode'] || false;
this.condenseFlow = options['condenseFlow'] || false;
this.quotingType = options['quotingType'] === '"' ? QUOTING_TYPE_DOUBLE : QUOTING_TYPE_SINGLE;
this.forceQuotes = options['forceQuotes'] || false;

this.implicitTypes = this.schema.compiledImplicit;
this.explicitTypes = this.schema.compiledExplicit;
Expand Down Expand Up @@ -285,7 +291,9 @@ var STYLE_PLAIN = 1,
// STYLE_PLAIN or STYLE_SINGLE => no \n are in the string.
// STYLE_LITERAL => no lines are suitable for folding (or lineWidth is -1).
// STYLE_FOLDED => a line > lineWidth and can be folded (and lineWidth != -1).
function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, testAmbiguousType) {
function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth,
testAmbiguousType, quotingType, forceQuotes) {

var i;
var char = 0;
var prevChar = null;
Expand All @@ -296,7 +304,7 @@ function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, te
var plain = isPlainSafeFirst(codePointAt(string, 0))
&& !isWhitespace(codePointAt(string, string.length - 1));

if (singleLineOnly) {
if (singleLineOnly || forceQuotes) {
// Case: no block styles.
// Check for disallowed characters to rule out plain and single.
for (i = 0; i < string.length; char >= 0x10000 ? i += 2 : i++) {
Expand Down Expand Up @@ -338,16 +346,21 @@ function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, te
if (!hasLineBreak && !hasFoldableLine) {
// Strings interpretable as another type have to be quoted;
// e.g. the string 'true' vs. the boolean true.
return plain && !testAmbiguousType(string)
? STYLE_PLAIN : STYLE_SINGLE;
if (plain && !forceQuotes && !testAmbiguousType(string)) {
return STYLE_PLAIN;
}
return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE;
}
// Edge case: block indentation indicator can only have one digit.
if (indentPerLevel > 9 && needIndentIndicator(string)) {
return STYLE_DOUBLE;
}
// At this point we know block styles are valid.
// Prefer literal style unless we want to fold.
return hasFoldableLine ? STYLE_FOLDED : STYLE_LITERAL;
if (!forceQuotes) {
return hasFoldableLine ? STYLE_FOLDED : STYLE_LITERAL;
}
return quotingType === QUOTING_TYPE_DOUBLE ? STYLE_DOUBLE : STYLE_SINGLE;
}

// Note: line breaking/folding is implemented for only the folded style.
Expand All @@ -359,11 +372,11 @@ function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, te
function writeScalar(state, string, level, iskey) {
state.dump = (function () {
if (string.length === 0) {
return "''";
return state.quotingType === QUOTING_TYPE_DOUBLE ? '""' : "''";
}
if (!state.noCompatMode &&
DEPRECATED_BOOLEANS_SYNTAX.indexOf(string) !== -1) {
return "'" + string + "'";
return state.quotingType === QUOTING_TYPE_DOUBLE ? ('"' + string + '"') : ("'" + string + "'");
}

var indent = state.indent * Math.max(1, level); // no 0-indent scalars
Expand All @@ -385,7 +398,9 @@ function writeScalar(state, string, level, iskey) {
return testImplicitResolving(state, string);
}

switch (chooseScalarStyle(string, singleLineOnly, state.indent, lineWidth, testAmbiguity)) {
switch (chooseScalarStyle(string, singleLineOnly, state.indent, lineWidth,
testAmbiguity, state.quotingType, state.forceQuotes && !iskey)) {

case STYLE_PLAIN:
return string;
case STYLE_SINGLE:
Expand Down
194 changes: 194 additions & 0 deletions test/issues/0529.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
'use strict';

/* eslint-disable max-len */

const assert = require('assert');
const yaml = require('../../');

const sample = {
// normal key-value pair
simple_key: 'value',

// special characters in key
'foo\'bar"baz': 1,

// non-printables in key
'foo\vbar': 1,

// multiline key
'foo\nbar\nbaz': 1,

// ambiguous type, looks like a number
'0x1234': 1,
ambiguous: '0x1234',

// ambiguous type, looks like a quoted string
"'foo'": 1,
ambiguous1: "'foo'",
'"foo"': 1,
ambiguous2: '"foo"',

// quote in output
quote1: "foo'bar",
quote2: 'foo"bar',

// spaces at the beginning or end
space1: ' test',
space2: 'test ',

// test test test ...
wrapped: 'test '.repeat(20).trim(),

// multiline value
multiline: 'foo\nbar\nbaz',

// needs leading space indicator
leading_space: '\n test',

// non-printables in value
nonprintable1: 'foo\vbar',
nonprintable2: 'foo\vbar ' + 'test '.repeat(20).trim(),
nonprintable3: 'foo\vbar ' + 'foo\nbar\nbaz',

// empty string
empty: '',

// bool compat
yes: 'yes'
};


describe('should format strings with specified quoting type', function () {
it('quotingType=\', forceQuotes=false', function () {
const expected = `
simple_key: value
foo'bar"baz: 1
"foo\\vbar": 1
"foo\\nbar\\nbaz": 1
'0x1234': 1
ambiguous: '0x1234'
'''foo''': 1
ambiguous1: '''foo'''
'"foo"': 1
ambiguous2: '"foo"'
quote1: foo'bar
quote2: foo"bar
space1: ' test'
space2: 'test '
wrapped: >-
test test test test test test test test test test test test test test test
test test test test test
multiline: |-
foo
bar
baz
leading_space: |2-
test
nonprintable1: "foo\\vbar"
nonprintable2: "foo\\vbar test test test test test test test test test test test test test test test test test test test test"
nonprintable3: "foo\\vbar foo\\nbar\\nbaz"
empty: ''
'yes': 'yes'
`.replace(/^\n/, '');

assert.strictEqual(yaml.dump(sample, { quotingType: "'", forceQuotes: false }), expected);
});


it('quotingType=\", forceQuotes=false', function () {
const expected = `
simple_key: value
foo'bar"baz: 1
"foo\\vbar": 1
"foo\\nbar\\nbaz": 1
"0x1234": 1
ambiguous: "0x1234"
"'foo'": 1
ambiguous1: "'foo'"
"\\"foo\\"": 1
ambiguous2: "\\"foo\\""
quote1: foo'bar
quote2: foo"bar
space1: " test"
space2: "test "
wrapped: >-
test test test test test test test test test test test test test test test
test test test test test
multiline: |-
foo
bar
baz
leading_space: |2-
test
nonprintable1: "foo\\vbar"
nonprintable2: "foo\\vbar test test test test test test test test test test test test test test test test test test test test"
nonprintable3: "foo\\vbar foo\\nbar\\nbaz"
empty: ""
"yes": "yes"
`.replace(/^\n/, '');

assert.strictEqual(yaml.dump(sample, { quotingType: '"', forceQuotes: false }), expected);
});


it('quotingType=\', forceQuotes=true', function () {
const expected = `
simple_key: 'value'
foo'bar"baz: 1
"foo\\vbar": 1
"foo\\nbar\\nbaz": 1
'0x1234': 1
ambiguous: '0x1234'
'''foo''': 1
ambiguous1: '''foo'''
'"foo"': 1
ambiguous2: '"foo"'
quote1: 'foo''bar'
quote2: 'foo"bar'
space1: ' test'
space2: 'test '
wrapped: 'test test test test test test test test test test test test test test test test test test test test'
multiline: "foo\\nbar\\nbaz"
leading_space: "\\n test"
nonprintable1: "foo\\vbar"
nonprintable2: "foo\\vbar test test test test test test test test test test test test test test test test test test test test"
nonprintable3: "foo\\vbar foo\\nbar\\nbaz"
empty: ''
'yes': 'yes'
`.replace(/^\n/, '');

assert.strictEqual(yaml.dump(sample, { quotingType: "'", forceQuotes: true }), expected);
});


it('quotingType=\", forceQuotes=true', function () {
const expected = `
simple_key: "value"
foo'bar"baz: 1
"foo\\vbar": 1
"foo\\nbar\\nbaz": 1
"0x1234": 1
ambiguous: "0x1234"
"'foo'": 1
ambiguous1: "'foo'"
"\\"foo\\"": 1
ambiguous2: "\\"foo\\""
quote1: "foo'bar"
quote2: "foo\\"bar"
space1: " test"
space2: "test "
wrapped: "test test test test test test test test test test test test test test test test test test test test"
multiline: "foo\\nbar\\nbaz"
leading_space: "\\n test"
nonprintable1: "foo\\vbar"
nonprintable2: "foo\\vbar test test test test test test test test test test test test test test test test test test test test"
nonprintable3: "foo\\vbar foo\\nbar\\nbaz"
empty: ""
"yes": "yes"
`.replace(/^\n/, '');

assert.strictEqual(yaml.dump(sample, { quotingType: '"', forceQuotes: true }), expected);
});
});

0 comments on commit 7b256d7

Please # to comment.