You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
12 KiB
314 lines
12 KiB
import { Scalar } from '../nodes/Scalar.js'; |
|
import { foldFlowLines, FOLD_QUOTED, FOLD_FLOW, FOLD_BLOCK } from './foldFlowLines.js'; |
|
|
|
const getFoldOptions = (ctx) => ({ |
|
indentAtStart: 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)); |
|
} |
|
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)); |
|
} |
|
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); |
|
} |
|
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(/\n+(?!\n|$)/g, `$&${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)); |
|
return `${header}\n${indent}${body}`; |
|
} |
|
function plainString(item, ctx, onComment, onChompKeep) { |
|
const { type, value } = item; |
|
const { actualString, implicitKey, indent, inFlow } = ctx; |
|
if ((implicitKey && /[\n[\]{},]/.test(value)) || |
|
(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 (indent === '' && containsDocumentMarker(value)) { |
|
ctx.forceBlockIndent = true; |
|
return blockString(item, ctx, onComment, onChompKeep); |
|
} |
|
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)); |
|
} |
|
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 };
|
|
|