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;
|
---|