339 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var Scalar = require('../nodes/Scalar.js');
 | |
| var foldFlowLines = require('./foldFlowLines.js');
 | |
| 
 | |
| const getFoldOptions = (ctx, isBlock) => ({
 | |
|     indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart,
 | |
|     lineWidth: ctx.options.lineWidth,
 | |
|     minContentWidth: ctx.options.minContentWidth
 | |
| });
 | |
| // Also checks for lines starting with %, as parsing the output as YAML 1.1 will
 | |
| // presume that's starting a new document.
 | |
| const containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str);
 | |
| function lineLengthOverLimit(str, lineWidth, indentLength) {
 | |
|     if (!lineWidth || lineWidth < 0)
 | |
|         return false;
 | |
|     const limit = lineWidth - indentLength;
 | |
|     const strLen = str.length;
 | |
|     if (strLen <= limit)
 | |
|         return false;
 | |
|     for (let i = 0, start = 0; i < strLen; ++i) {
 | |
|         if (str[i] === '\n') {
 | |
|             if (i - start > limit)
 | |
|                 return true;
 | |
|             start = i + 1;
 | |
|             if (strLen - start <= limit)
 | |
|                 return false;
 | |
|         }
 | |
|     }
 | |
|     return true;
 | |
| }
 | |
| function doubleQuotedString(value, ctx) {
 | |
|     const json = JSON.stringify(value);
 | |
|     if (ctx.options.doubleQuotedAsJSON)
 | |
|         return json;
 | |
|     const { implicitKey } = ctx;
 | |
|     const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
 | |
|     const indent = ctx.indent || (containsDocumentMarker(value) ? '  ' : '');
 | |
|     let str = '';
 | |
|     let start = 0;
 | |
|     for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
 | |
|         if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') {
 | |
|             // space before newline needs to be escaped to not be folded
 | |
|             str += json.slice(start, i) + '\\ ';
 | |
|             i += 1;
 | |
|             start = i;
 | |
|             ch = '\\';
 | |
|         }
 | |
|         if (ch === '\\')
 | |
|             switch (json[i + 1]) {
 | |
|                 case 'u':
 | |
|                     {
 | |
|                         str += json.slice(start, i);
 | |
|                         const code = json.substr(i + 2, 4);
 | |
|                         switch (code) {
 | |
|                             case '0000':
 | |
|                                 str += '\\0';
 | |
|                                 break;
 | |
|                             case '0007':
 | |
|                                 str += '\\a';
 | |
|                                 break;
 | |
|                             case '000b':
 | |
|                                 str += '\\v';
 | |
|                                 break;
 | |
|                             case '001b':
 | |
|                                 str += '\\e';
 | |
|                                 break;
 | |
|                             case '0085':
 | |
|                                 str += '\\N';
 | |
|                                 break;
 | |
|                             case '00a0':
 | |
|                                 str += '\\_';
 | |
|                                 break;
 | |
|                             case '2028':
 | |
|                                 str += '\\L';
 | |
|                                 break;
 | |
|                             case '2029':
 | |
|                                 str += '\\P';
 | |
|                                 break;
 | |
|                             default:
 | |
|                                 if (code.substr(0, 2) === '00')
 | |
|                                     str += '\\x' + code.substr(2);
 | |
|                                 else
 | |
|                                     str += json.substr(i, 6);
 | |
|                         }
 | |
|                         i += 5;
 | |
|                         start = i + 1;
 | |
|                     }
 | |
|                     break;
 | |
|                 case 'n':
 | |
|                     if (implicitKey ||
 | |
|                         json[i + 2] === '"' ||
 | |
|                         json.length < minMultiLineLength) {
 | |
|                         i += 1;
 | |
|                     }
 | |
|                     else {
 | |
|                         // folding will eat first newline
 | |
|                         str += json.slice(start, i) + '\n\n';
 | |
|                         while (json[i + 2] === '\\' &&
 | |
|                             json[i + 3] === 'n' &&
 | |
|                             json[i + 4] !== '"') {
 | |
|                             str += '\n';
 | |
|                             i += 2;
 | |
|                         }
 | |
|                         str += indent;
 | |
|                         // space after newline needs to be escaped to not be folded
 | |
|                         if (json[i + 2] === ' ')
 | |
|                             str += '\\';
 | |
|                         i += 1;
 | |
|                         start = i + 1;
 | |
|                     }
 | |
|                     break;
 | |
|                 default:
 | |
|                     i += 1;
 | |
|             }
 | |
|     }
 | |
|     str = start ? str + json.slice(start) : json;
 | |
|     return implicitKey
 | |
|         ? str
 | |
|         : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
 | |
| }
 | |
| function singleQuotedString(value, ctx) {
 | |
|     if (ctx.options.singleQuote === false ||
 | |
|         (ctx.implicitKey && value.includes('\n')) ||
 | |
|         /[ \t]\n|\n[ \t]/.test(value) // single quoted string can't have leading or trailing whitespace around newline
 | |
|     )
 | |
|         return doubleQuotedString(value, ctx);
 | |
|     const indent = ctx.indent || (containsDocumentMarker(value) ? '  ' : '');
 | |
|     const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'";
 | |
|     return ctx.implicitKey
 | |
|         ? res
 | |
|         : foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
 | |
| }
 | |
| function quotedString(value, ctx) {
 | |
|     const { singleQuote } = ctx.options;
 | |
|     let qs;
 | |
|     if (singleQuote === false)
 | |
|         qs = doubleQuotedString;
 | |
|     else {
 | |
|         const hasDouble = value.includes('"');
 | |
|         const hasSingle = value.includes("'");
 | |
|         if (hasDouble && !hasSingle)
 | |
|             qs = singleQuotedString;
 | |
|         else if (hasSingle && !hasDouble)
 | |
|             qs = doubleQuotedString;
 | |
|         else
 | |
|             qs = singleQuote ? singleQuotedString : doubleQuotedString;
 | |
|     }
 | |
|     return qs(value, ctx);
 | |
| }
 | |
| // The negative lookbehind avoids a polynomial search,
 | |
| // but isn't supported yet on Safari: https://caniuse.com/js-regexp-lookbehind
 | |
| let blockEndNewlines;
 | |
| try {
 | |
|     blockEndNewlines = new RegExp('(^|(?<!\n))\n+(?!\n|$)', 'g');
 | |
| }
 | |
| catch {
 | |
|     blockEndNewlines = /\n+(?!\n|$)/g;
 | |
| }
 | |
| function blockString({ comment, type, value }, ctx, onComment, onChompKeep) {
 | |
|     const { blockQuote, commentString, lineWidth } = ctx.options;
 | |
|     // 1. Block can't end in whitespace unless the last line is non-empty.
 | |
|     // 2. Strings consisting of only whitespace are best rendered explicitly.
 | |
|     if (!blockQuote || /\n[\t ]+$/.test(value)) {
 | |
|         return quotedString(value, ctx);
 | |
|     }
 | |
|     const indent = ctx.indent ||
 | |
|         (ctx.forceBlockIndent || containsDocumentMarker(value) ? '  ' : '');
 | |
|     const literal = blockQuote === 'literal'
 | |
|         ? true
 | |
|         : blockQuote === 'folded' || type === Scalar.Scalar.BLOCK_FOLDED
 | |
|             ? false
 | |
|             : type === Scalar.Scalar.BLOCK_LITERAL
 | |
|                 ? true
 | |
|                 : !lineLengthOverLimit(value, lineWidth, indent.length);
 | |
|     if (!value)
 | |
|         return literal ? '|\n' : '>\n';
 | |
|     // determine chomping from whitespace at value end
 | |
|     let chomp;
 | |
|     let endStart;
 | |
|     for (endStart = value.length; endStart > 0; --endStart) {
 | |
|         const ch = value[endStart - 1];
 | |
|         if (ch !== '\n' && ch !== '\t' && ch !== ' ')
 | |
|             break;
 | |
|     }
 | |
|     let end = value.substring(endStart);
 | |
|     const endNlPos = end.indexOf('\n');
 | |
|     if (endNlPos === -1) {
 | |
|         chomp = '-'; // strip
 | |
|     }
 | |
|     else if (value === end || endNlPos !== end.length - 1) {
 | |
|         chomp = '+'; // keep
 | |
|         if (onChompKeep)
 | |
|             onChompKeep();
 | |
|     }
 | |
|     else {
 | |
|         chomp = ''; // clip
 | |
|     }
 | |
|     if (end) {
 | |
|         value = value.slice(0, -end.length);
 | |
|         if (end[end.length - 1] === '\n')
 | |
|             end = end.slice(0, -1);
 | |
|         end = end.replace(blockEndNewlines, `$&${indent}`);
 | |
|     }
 | |
|     // determine indent indicator from whitespace at value start
 | |
|     let startWithSpace = false;
 | |
|     let startEnd;
 | |
|     let startNlPos = -1;
 | |
|     for (startEnd = 0; startEnd < value.length; ++startEnd) {
 | |
|         const ch = value[startEnd];
 | |
|         if (ch === ' ')
 | |
|             startWithSpace = true;
 | |
|         else if (ch === '\n')
 | |
|             startNlPos = startEnd;
 | |
|         else
 | |
|             break;
 | |
|     }
 | |
|     let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd);
 | |
|     if (start) {
 | |
|         value = value.substring(start.length);
 | |
|         start = start.replace(/\n+/g, `$&${indent}`);
 | |
|     }
 | |
|     const indentSize = indent ? '2' : '1'; // root is at -1
 | |
|     // Leading | or > is added later
 | |
|     let header = (startWithSpace ? indentSize : '') + chomp;
 | |
|     if (comment) {
 | |
|         header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
 | |
|         if (onComment)
 | |
|             onComment();
 | |
|     }
 | |
|     if (!literal) {
 | |
|         const foldedValue = value
 | |
|             .replace(/\n+/g, '\n$&')
 | |
|             .replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded
 | |
|             //                ^ more-ind. ^ empty     ^ capture next empty lines only at end of indent
 | |
|             .replace(/\n+/g, `$&${indent}`);
 | |
|         let literalFallback = false;
 | |
|         const foldOptions = getFoldOptions(ctx, true);
 | |
|         if (blockQuote !== 'folded' && type !== Scalar.Scalar.BLOCK_FOLDED) {
 | |
|             foldOptions.onOverflow = () => {
 | |
|                 literalFallback = true;
 | |
|             };
 | |
|         }
 | |
|         const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions);
 | |
|         if (!literalFallback)
 | |
|             return `>${header}\n${indent}${body}`;
 | |
|     }
 | |
|     value = value.replace(/\n+/g, `$&${indent}`);
 | |
|     return `|${header}\n${indent}${start}${value}${end}`;
 | |
| }
 | |
| function plainString(item, ctx, onComment, onChompKeep) {
 | |
|     const { type, value } = item;
 | |
|     const { actualString, implicitKey, indent, indentStep, inFlow } = ctx;
 | |
|     if ((implicitKey && value.includes('\n')) ||
 | |
|         (inFlow && /[[\]{},]/.test(value))) {
 | |
|         return quotedString(value, ctx);
 | |
|     }
 | |
|     if (/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
 | |
|         // not allowed:
 | |
|         // - '-' or '?'
 | |
|         // - start with an indicator character (except [?:-]) or /[?-] /
 | |
|         // - '\n ', ': ' or ' \n' anywhere
 | |
|         // - '#' not preceded by a non-space char
 | |
|         // - end with ' ' or ':'
 | |
|         return implicitKey || inFlow || !value.includes('\n')
 | |
|             ? quotedString(value, ctx)
 | |
|             : blockString(item, ctx, onComment, onChompKeep);
 | |
|     }
 | |
|     if (!implicitKey &&
 | |
|         !inFlow &&
 | |
|         type !== Scalar.Scalar.PLAIN &&
 | |
|         value.includes('\n')) {
 | |
|         // Where allowed & type not set explicitly, prefer block style for multiline strings
 | |
|         return blockString(item, ctx, onComment, onChompKeep);
 | |
|     }
 | |
|     if (containsDocumentMarker(value)) {
 | |
|         if (indent === '') {
 | |
|             ctx.forceBlockIndent = true;
 | |
|             return blockString(item, ctx, onComment, onChompKeep);
 | |
|         }
 | |
|         else if (implicitKey && indent === indentStep) {
 | |
|             return quotedString(value, ctx);
 | |
|         }
 | |
|     }
 | |
|     const str = value.replace(/\n+/g, `$&\n${indent}`);
 | |
|     // Verify that output will be parsed as a string, as e.g. plain numbers and
 | |
|     // booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
 | |
|     // and others in v1.1.
 | |
|     if (actualString) {
 | |
|         const test = (tag) => tag.default && tag.tag !== 'tag:yaml.org,2002:str' && tag.test?.test(str);
 | |
|         const { compat, tags } = ctx.doc.schema;
 | |
|         if (tags.some(test) || compat?.some(test))
 | |
|             return quotedString(value, ctx);
 | |
|     }
 | |
|     return implicitKey
 | |
|         ? str
 | |
|         : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
 | |
| }
 | |
| function stringifyString(item, ctx, onComment, onChompKeep) {
 | |
|     const { implicitKey, inFlow } = ctx;
 | |
|     const ss = typeof item.value === 'string'
 | |
|         ? item
 | |
|         : Object.assign({}, item, { value: String(item.value) });
 | |
|     let { type } = item;
 | |
|     if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
 | |
|         // force double quotes on control characters & unpaired surrogates
 | |
|         if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value))
 | |
|             type = Scalar.Scalar.QUOTE_DOUBLE;
 | |
|     }
 | |
|     const _stringify = (_type) => {
 | |
|         switch (_type) {
 | |
|             case Scalar.Scalar.BLOCK_FOLDED:
 | |
|             case Scalar.Scalar.BLOCK_LITERAL:
 | |
|                 return implicitKey || inFlow
 | |
|                     ? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
 | |
|                     : blockString(ss, ctx, onComment, onChompKeep);
 | |
|             case Scalar.Scalar.QUOTE_DOUBLE:
 | |
|                 return doubleQuotedString(ss.value, ctx);
 | |
|             case Scalar.Scalar.QUOTE_SINGLE:
 | |
|                 return singleQuotedString(ss.value, ctx);
 | |
|             case Scalar.Scalar.PLAIN:
 | |
|                 return plainString(ss, ctx, onComment, onChompKeep);
 | |
|             default:
 | |
|                 return null;
 | |
|         }
 | |
|     };
 | |
|     let res = _stringify(type);
 | |
|     if (res === null) {
 | |
|         const { defaultKeyType, defaultStringType } = ctx.options;
 | |
|         const t = (implicitKey && defaultKeyType) || defaultStringType;
 | |
|         res = _stringify(t);
 | |
|         if (res === null)
 | |
|             throw new Error(`Unsupported default string type ${t}`);
 | |
|     }
 | |
|     return res;
 | |
| }
 | |
| 
 | |
| exports.stringifyString = stringifyString;
 |