UNPKG

13 kBJavaScriptView Raw
1'use strict';
2
3var Scalar = require('../nodes/Scalar.js');
4var foldFlowLines = require('./foldFlowLines.js');
5
6const 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.
13const containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str);
14function 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}
32function 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}
122function 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}
134function 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
153let blockEndNewlines;
154try {
155 blockEndNewlines = new RegExp('(^|(?<!\n))\n+(?!\n|$)', 'g');
156}
157catch {
158 blockEndNewlines = /\n+(?!\n|$)/g;
159}
160function 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}
242function 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}
291function 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
330exports.stringifyString = stringifyString;