[d24f17c] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | var Scalar = require('../nodes/Scalar.js');
|
---|
| 4 | var foldFlowLines = require('./foldFlowLines.js');
|
---|
| 5 |
|
---|
| 6 | const getFoldOptions = (ctx, isBlock) => ({
|
---|
| 7 | indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart,
|
---|
| 8 | lineWidth: ctx.options.lineWidth,
|
---|
| 9 | minContentWidth: ctx.options.minContentWidth
|
---|
| 10 | });
|
---|
| 11 | // Also checks for lines starting with %, as parsing the output as YAML 1.1 will
|
---|
| 12 | // presume that's starting a new document.
|
---|
| 13 | const containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str);
|
---|
| 14 | function lineLengthOverLimit(str, lineWidth, indentLength) {
|
---|
| 15 | if (!lineWidth || lineWidth < 0)
|
---|
| 16 | return false;
|
---|
| 17 | const limit = lineWidth - indentLength;
|
---|
| 18 | const strLen = str.length;
|
---|
| 19 | if (strLen <= limit)
|
---|
| 20 | return false;
|
---|
| 21 | for (let i = 0, start = 0; i < strLen; ++i) {
|
---|
| 22 | if (str[i] === '\n') {
|
---|
| 23 | if (i - start > limit)
|
---|
| 24 | return true;
|
---|
| 25 | start = i + 1;
|
---|
| 26 | if (strLen - start <= limit)
|
---|
| 27 | return false;
|
---|
| 28 | }
|
---|
| 29 | }
|
---|
| 30 | return true;
|
---|
| 31 | }
|
---|
| 32 | function doubleQuotedString(value, ctx) {
|
---|
| 33 | const json = JSON.stringify(value);
|
---|
| 34 | if (ctx.options.doubleQuotedAsJSON)
|
---|
| 35 | return json;
|
---|
| 36 | const { implicitKey } = ctx;
|
---|
| 37 | const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
|
---|
| 38 | const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
|
---|
| 39 | let str = '';
|
---|
| 40 | let start = 0;
|
---|
| 41 | for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
|
---|
| 42 | if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') {
|
---|
| 43 | // space before newline needs to be escaped to not be folded
|
---|
| 44 | str += json.slice(start, i) + '\\ ';
|
---|
| 45 | i += 1;
|
---|
| 46 | start = i;
|
---|
| 47 | ch = '\\';
|
---|
| 48 | }
|
---|
| 49 | if (ch === '\\')
|
---|
| 50 | switch (json[i + 1]) {
|
---|
| 51 | case 'u':
|
---|
| 52 | {
|
---|
| 53 | str += json.slice(start, i);
|
---|
| 54 | const code = json.substr(i + 2, 4);
|
---|
| 55 | switch (code) {
|
---|
| 56 | case '0000':
|
---|
| 57 | str += '\\0';
|
---|
| 58 | break;
|
---|
| 59 | case '0007':
|
---|
| 60 | str += '\\a';
|
---|
| 61 | break;
|
---|
| 62 | case '000b':
|
---|
| 63 | str += '\\v';
|
---|
| 64 | break;
|
---|
| 65 | case '001b':
|
---|
| 66 | str += '\\e';
|
---|
| 67 | break;
|
---|
| 68 | case '0085':
|
---|
| 69 | str += '\\N';
|
---|
| 70 | break;
|
---|
| 71 | case '00a0':
|
---|
| 72 | str += '\\_';
|
---|
| 73 | break;
|
---|
| 74 | case '2028':
|
---|
| 75 | str += '\\L';
|
---|
| 76 | break;
|
---|
| 77 | case '2029':
|
---|
| 78 | str += '\\P';
|
---|
| 79 | break;
|
---|
| 80 | default:
|
---|
| 81 | if (code.substr(0, 2) === '00')
|
---|
| 82 | str += '\\x' + code.substr(2);
|
---|
| 83 | else
|
---|
| 84 | str += json.substr(i, 6);
|
---|
| 85 | }
|
---|
| 86 | i += 5;
|
---|
| 87 | start = i + 1;
|
---|
| 88 | }
|
---|
| 89 | break;
|
---|
| 90 | case 'n':
|
---|
| 91 | if (implicitKey ||
|
---|
| 92 | json[i + 2] === '"' ||
|
---|
| 93 | json.length < minMultiLineLength) {
|
---|
| 94 | i += 1;
|
---|
| 95 | }
|
---|
| 96 | else {
|
---|
| 97 | // folding will eat first newline
|
---|
| 98 | str += json.slice(start, i) + '\n\n';
|
---|
| 99 | while (json[i + 2] === '\\' &&
|
---|
| 100 | json[i + 3] === 'n' &&
|
---|
| 101 | json[i + 4] !== '"') {
|
---|
| 102 | str += '\n';
|
---|
| 103 | i += 2;
|
---|
| 104 | }
|
---|
| 105 | str += indent;
|
---|
| 106 | // space after newline needs to be escaped to not be folded
|
---|
| 107 | if (json[i + 2] === ' ')
|
---|
| 108 | str += '\\';
|
---|
| 109 | i += 1;
|
---|
| 110 | start = i + 1;
|
---|
| 111 | }
|
---|
| 112 | break;
|
---|
| 113 | default:
|
---|
| 114 | i += 1;
|
---|
| 115 | }
|
---|
| 116 | }
|
---|
| 117 | str = start ? str + json.slice(start) : json;
|
---|
| 118 | return implicitKey
|
---|
| 119 | ? str
|
---|
| 120 | : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
|
---|
| 121 | }
|
---|
| 122 | function singleQuotedString(value, ctx) {
|
---|
| 123 | if (ctx.options.singleQuote === false ||
|
---|
| 124 | (ctx.implicitKey && value.includes('\n')) ||
|
---|
| 125 | /[ \t]\n|\n[ \t]/.test(value) // single quoted string can't have leading or trailing whitespace around newline
|
---|
| 126 | )
|
---|
| 127 | return doubleQuotedString(value, ctx);
|
---|
| 128 | const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
|
---|
| 129 | const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'";
|
---|
| 130 | return ctx.implicitKey
|
---|
| 131 | ? res
|
---|
| 132 | : foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
|
---|
| 133 | }
|
---|
| 134 | function quotedString(value, ctx) {
|
---|
| 135 | const { singleQuote } = ctx.options;
|
---|
| 136 | let qs;
|
---|
| 137 | if (singleQuote === false)
|
---|
| 138 | qs = doubleQuotedString;
|
---|
| 139 | else {
|
---|
| 140 | const hasDouble = value.includes('"');
|
---|
| 141 | const hasSingle = value.includes("'");
|
---|
| 142 | if (hasDouble && !hasSingle)
|
---|
| 143 | qs = singleQuotedString;
|
---|
| 144 | else if (hasSingle && !hasDouble)
|
---|
| 145 | qs = doubleQuotedString;
|
---|
| 146 | else
|
---|
| 147 | qs = singleQuote ? singleQuotedString : doubleQuotedString;
|
---|
| 148 | }
|
---|
| 149 | return qs(value, ctx);
|
---|
| 150 | }
|
---|
| 151 | // The negative lookbehind avoids a polynomial search,
|
---|
| 152 | // but isn't supported yet on Safari: https://caniuse.com/js-regexp-lookbehind
|
---|
| 153 | let blockEndNewlines;
|
---|
| 154 | try {
|
---|
| 155 | blockEndNewlines = new RegExp('(^|(?<!\n))\n+(?!\n|$)', 'g');
|
---|
| 156 | }
|
---|
| 157 | catch {
|
---|
| 158 | blockEndNewlines = /\n+(?!\n|$)/g;
|
---|
| 159 | }
|
---|
| 160 | function blockString({ comment, type, value }, ctx, onComment, onChompKeep) {
|
---|
| 161 | const { blockQuote, commentString, lineWidth } = ctx.options;
|
---|
| 162 | // 1. Block can't end in whitespace unless the last line is non-empty.
|
---|
| 163 | // 2. Strings consisting of only whitespace are best rendered explicitly.
|
---|
| 164 | if (!blockQuote || /\n[\t ]+$/.test(value) || /^\s*$/.test(value)) {
|
---|
| 165 | return quotedString(value, ctx);
|
---|
| 166 | }
|
---|
| 167 | const indent = ctx.indent ||
|
---|
| 168 | (ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : '');
|
---|
| 169 | const literal = blockQuote === 'literal'
|
---|
| 170 | ? true
|
---|
| 171 | : blockQuote === 'folded' || type === Scalar.Scalar.BLOCK_FOLDED
|
---|
| 172 | ? false
|
---|
| 173 | : type === Scalar.Scalar.BLOCK_LITERAL
|
---|
| 174 | ? true
|
---|
| 175 | : !lineLengthOverLimit(value, lineWidth, indent.length);
|
---|
| 176 | if (!value)
|
---|
| 177 | return literal ? '|\n' : '>\n';
|
---|
| 178 | // determine chomping from whitespace at value end
|
---|
| 179 | let chomp;
|
---|
| 180 | let endStart;
|
---|
| 181 | for (endStart = value.length; endStart > 0; --endStart) {
|
---|
| 182 | const ch = value[endStart - 1];
|
---|
| 183 | if (ch !== '\n' && ch !== '\t' && ch !== ' ')
|
---|
| 184 | break;
|
---|
| 185 | }
|
---|
| 186 | let end = value.substring(endStart);
|
---|
| 187 | const endNlPos = end.indexOf('\n');
|
---|
| 188 | if (endNlPos === -1) {
|
---|
| 189 | chomp = '-'; // strip
|
---|
| 190 | }
|
---|
| 191 | else if (value === end || endNlPos !== end.length - 1) {
|
---|
| 192 | chomp = '+'; // keep
|
---|
| 193 | if (onChompKeep)
|
---|
| 194 | onChompKeep();
|
---|
| 195 | }
|
---|
| 196 | else {
|
---|
| 197 | chomp = ''; // clip
|
---|
| 198 | }
|
---|
| 199 | if (end) {
|
---|
| 200 | value = value.slice(0, -end.length);
|
---|
| 201 | if (end[end.length - 1] === '\n')
|
---|
| 202 | end = end.slice(0, -1);
|
---|
| 203 | end = end.replace(blockEndNewlines, `$&${indent}`);
|
---|
| 204 | }
|
---|
| 205 | // determine indent indicator from whitespace at value start
|
---|
| 206 | let startWithSpace = false;
|
---|
| 207 | let startEnd;
|
---|
| 208 | let startNlPos = -1;
|
---|
| 209 | for (startEnd = 0; startEnd < value.length; ++startEnd) {
|
---|
| 210 | const ch = value[startEnd];
|
---|
| 211 | if (ch === ' ')
|
---|
| 212 | startWithSpace = true;
|
---|
| 213 | else if (ch === '\n')
|
---|
| 214 | startNlPos = startEnd;
|
---|
| 215 | else
|
---|
| 216 | break;
|
---|
| 217 | }
|
---|
| 218 | let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd);
|
---|
| 219 | if (start) {
|
---|
| 220 | value = value.substring(start.length);
|
---|
| 221 | start = start.replace(/\n+/g, `$&${indent}`);
|
---|
| 222 | }
|
---|
| 223 | const indentSize = indent ? '2' : '1'; // root is at -1
|
---|
| 224 | let header = (literal ? '|' : '>') + (startWithSpace ? indentSize : '') + chomp;
|
---|
| 225 | if (comment) {
|
---|
| 226 | header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
|
---|
| 227 | if (onComment)
|
---|
| 228 | onComment();
|
---|
| 229 | }
|
---|
| 230 | if (literal) {
|
---|
| 231 | value = value.replace(/\n+/g, `$&${indent}`);
|
---|
| 232 | return `${header}\n${indent}${start}${value}${end}`;
|
---|
| 233 | }
|
---|
| 234 | value = value
|
---|
| 235 | .replace(/\n+/g, '\n$&')
|
---|
| 236 | .replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded
|
---|
| 237 | // ^ more-ind. ^ empty ^ capture next empty lines only at end of indent
|
---|
| 238 | .replace(/\n+/g, `$&${indent}`);
|
---|
| 239 | const body = foldFlowLines.foldFlowLines(`${start}${value}${end}`, indent, foldFlowLines.FOLD_BLOCK, getFoldOptions(ctx, true));
|
---|
| 240 | return `${header}\n${indent}${body}`;
|
---|
| 241 | }
|
---|
| 242 | function plainString(item, ctx, onComment, onChompKeep) {
|
---|
| 243 | const { type, value } = item;
|
---|
| 244 | const { actualString, implicitKey, indent, indentStep, inFlow } = ctx;
|
---|
| 245 | if ((implicitKey && value.includes('\n')) ||
|
---|
| 246 | (inFlow && /[[\]{},]/.test(value))) {
|
---|
| 247 | return quotedString(value, ctx);
|
---|
| 248 | }
|
---|
| 249 | if (!value ||
|
---|
| 250 | /^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
|
---|
| 251 | // not allowed:
|
---|
| 252 | // - empty string, '-' or '?'
|
---|
| 253 | // - start with an indicator character (except [?:-]) or /[?-] /
|
---|
| 254 | // - '\n ', ': ' or ' \n' anywhere
|
---|
| 255 | // - '#' not preceded by a non-space char
|
---|
| 256 | // - end with ' ' or ':'
|
---|
| 257 | return implicitKey || inFlow || !value.includes('\n')
|
---|
| 258 | ? quotedString(value, ctx)
|
---|
| 259 | : blockString(item, ctx, onComment, onChompKeep);
|
---|
| 260 | }
|
---|
| 261 | if (!implicitKey &&
|
---|
| 262 | !inFlow &&
|
---|
| 263 | type !== Scalar.Scalar.PLAIN &&
|
---|
| 264 | value.includes('\n')) {
|
---|
| 265 | // Where allowed & type not set explicitly, prefer block style for multiline strings
|
---|
| 266 | return blockString(item, ctx, onComment, onChompKeep);
|
---|
| 267 | }
|
---|
| 268 | if (containsDocumentMarker(value)) {
|
---|
| 269 | if (indent === '') {
|
---|
| 270 | ctx.forceBlockIndent = true;
|
---|
| 271 | return blockString(item, ctx, onComment, onChompKeep);
|
---|
| 272 | }
|
---|
| 273 | else if (implicitKey && indent === indentStep) {
|
---|
| 274 | return quotedString(value, ctx);
|
---|
| 275 | }
|
---|
| 276 | }
|
---|
| 277 | const str = value.replace(/\n+/g, `$&\n${indent}`);
|
---|
| 278 | // Verify that output will be parsed as a string, as e.g. plain numbers and
|
---|
| 279 | // booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
|
---|
| 280 | // and others in v1.1.
|
---|
| 281 | if (actualString) {
|
---|
| 282 | const test = (tag) => tag.default && tag.tag !== 'tag:yaml.org,2002:str' && tag.test?.test(str);
|
---|
| 283 | const { compat, tags } = ctx.doc.schema;
|
---|
| 284 | if (tags.some(test) || compat?.some(test))
|
---|
| 285 | return quotedString(value, ctx);
|
---|
| 286 | }
|
---|
| 287 | return implicitKey
|
---|
| 288 | ? str
|
---|
| 289 | : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
|
---|
| 290 | }
|
---|
| 291 | function stringifyString(item, ctx, onComment, onChompKeep) {
|
---|
| 292 | const { implicitKey, inFlow } = ctx;
|
---|
| 293 | const ss = typeof item.value === 'string'
|
---|
| 294 | ? item
|
---|
| 295 | : Object.assign({}, item, { value: String(item.value) });
|
---|
| 296 | let { type } = item;
|
---|
| 297 | if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
|
---|
| 298 | // force double quotes on control characters & unpaired surrogates
|
---|
| 299 | if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value))
|
---|
| 300 | type = Scalar.Scalar.QUOTE_DOUBLE;
|
---|
| 301 | }
|
---|
| 302 | const _stringify = (_type) => {
|
---|
| 303 | switch (_type) {
|
---|
| 304 | case Scalar.Scalar.BLOCK_FOLDED:
|
---|
| 305 | case Scalar.Scalar.BLOCK_LITERAL:
|
---|
| 306 | return implicitKey || inFlow
|
---|
| 307 | ? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
|
---|
| 308 | : blockString(ss, ctx, onComment, onChompKeep);
|
---|
| 309 | case Scalar.Scalar.QUOTE_DOUBLE:
|
---|
| 310 | return doubleQuotedString(ss.value, ctx);
|
---|
| 311 | case Scalar.Scalar.QUOTE_SINGLE:
|
---|
| 312 | return singleQuotedString(ss.value, ctx);
|
---|
| 313 | case Scalar.Scalar.PLAIN:
|
---|
| 314 | return plainString(ss, ctx, onComment, onChompKeep);
|
---|
| 315 | default:
|
---|
| 316 | return null;
|
---|
| 317 | }
|
---|
| 318 | };
|
---|
| 319 | let res = _stringify(type);
|
---|
| 320 | if (res === null) {
|
---|
| 321 | const { defaultKeyType, defaultStringType } = ctx.options;
|
---|
| 322 | const t = (implicitKey && defaultKeyType) || defaultStringType;
|
---|
| 323 | res = _stringify(t);
|
---|
| 324 | if (res === null)
|
---|
| 325 | throw new Error(`Unsupported default string type ${t}`);
|
---|
| 326 | }
|
---|
| 327 | return res;
|
---|
| 328 | }
|
---|
| 329 |
|
---|
| 330 | exports.stringifyString = stringifyString;
|
---|