From 7b256d7612a4c29cdf5dfd48a15e01c8dbbb5291 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Wed, 9 Dec 2020 15:10:42 +0300 Subject: [PATCH] Add options to specify string quoting --- CHANGELOG.md | 2 + README.md | 6 +- lib/dumper.js | 31 +++++-- test/issues/0529.js | 194 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 test/issues/0529.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5563b2..c58e3778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 650453e9..1c72d85c 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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" diff --git a/lib/dumper.js b/lib/dumper.js index 32619a44..4d2226dd 100644 --- a/lib/dumper.js +++ b/lib/dumper.js @@ -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)); @@ -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; @@ -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; @@ -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++) { @@ -338,8 +346,10 @@ 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)) { @@ -347,7 +357,10 @@ function chooseScalarStyle(string, singleLineOnly, indentPerLevel, lineWidth, te } // 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. @@ -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 @@ -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: diff --git a/test/issues/0529.js b/test/issues/0529.js new file mode 100644 index 00000000..52e171cc --- /dev/null +++ b/test/issues/0529.js @@ -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); + }); +});