123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- import { Scalar } from '../nodes/Scalar.js';
- import { foldFlowLines, FOLD_QUOTED, FOLD_FLOW, FOLD_BLOCK } from './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(str, indent, 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(res, indent, 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) || /^\s*$/.test(value)) {
- return quotedString(value, ctx);
- }
- const indent = ctx.indent ||
- (ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : '');
- const literal = blockQuote === 'literal'
- ? true
- : blockQuote === 'folded' || type === Scalar.BLOCK_FOLDED
- ? false
- : type === 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
- let header = (literal ? '|' : '>') + (startWithSpace ? indentSize : '') + chomp;
- if (comment) {
- header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
- if (onComment)
- onComment();
- }
- if (literal) {
- value = value.replace(/\n+/g, `$&${indent}`);
- return `${header}\n${indent}${start}${value}${end}`;
- }
- value = 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}`);
- const body = foldFlowLines(`${start}${value}${end}`, indent, FOLD_BLOCK, getFoldOptions(ctx, true));
- return `${header}\n${indent}${body}`;
- }
- 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 (!value ||
- /^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
- // not allowed:
- // - empty string, '-' 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.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(str, indent, 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.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.QUOTE_DOUBLE;
- }
- const _stringify = (_type) => {
- switch (_type) {
- case Scalar.BLOCK_FOLDED:
- case Scalar.BLOCK_LITERAL:
- return implicitKey || inFlow
- ? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
- : blockString(ss, ctx, onComment, onChompKeep);
- case Scalar.QUOTE_DOUBLE:
- return doubleQuotedString(ss.value, ctx);
- case Scalar.QUOTE_SINGLE:
- return singleQuotedString(ss.value, ctx);
- case 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;
- }
- export { stringifyString };
|