No Description

stringifyString.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. 'use strict';
  2. var Scalar = require('../nodes/Scalar.js');
  3. var foldFlowLines = require('./foldFlowLines.js');
  4. const getFoldOptions = (ctx, isBlock) => ({
  5. indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart,
  6. lineWidth: ctx.options.lineWidth,
  7. minContentWidth: ctx.options.minContentWidth
  8. });
  9. // Also checks for lines starting with %, as parsing the output as YAML 1.1 will
  10. // presume that's starting a new document.
  11. const containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str);
  12. function lineLengthOverLimit(str, lineWidth, indentLength) {
  13. if (!lineWidth || lineWidth < 0)
  14. return false;
  15. const limit = lineWidth - indentLength;
  16. const strLen = str.length;
  17. if (strLen <= limit)
  18. return false;
  19. for (let i = 0, start = 0; i < strLen; ++i) {
  20. if (str[i] === '\n') {
  21. if (i - start > limit)
  22. return true;
  23. start = i + 1;
  24. if (strLen - start <= limit)
  25. return false;
  26. }
  27. }
  28. return true;
  29. }
  30. function doubleQuotedString(value, ctx) {
  31. const json = JSON.stringify(value);
  32. if (ctx.options.doubleQuotedAsJSON)
  33. return json;
  34. const { implicitKey } = ctx;
  35. const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
  36. const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
  37. let str = '';
  38. let start = 0;
  39. for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
  40. if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') {
  41. // space before newline needs to be escaped to not be folded
  42. str += json.slice(start, i) + '\\ ';
  43. i += 1;
  44. start = i;
  45. ch = '\\';
  46. }
  47. if (ch === '\\')
  48. switch (json[i + 1]) {
  49. case 'u':
  50. {
  51. str += json.slice(start, i);
  52. const code = json.substr(i + 2, 4);
  53. switch (code) {
  54. case '0000':
  55. str += '\\0';
  56. break;
  57. case '0007':
  58. str += '\\a';
  59. break;
  60. case '000b':
  61. str += '\\v';
  62. break;
  63. case '001b':
  64. str += '\\e';
  65. break;
  66. case '0085':
  67. str += '\\N';
  68. break;
  69. case '00a0':
  70. str += '\\_';
  71. break;
  72. case '2028':
  73. str += '\\L';
  74. break;
  75. case '2029':
  76. str += '\\P';
  77. break;
  78. default:
  79. if (code.substr(0, 2) === '00')
  80. str += '\\x' + code.substr(2);
  81. else
  82. str += json.substr(i, 6);
  83. }
  84. i += 5;
  85. start = i + 1;
  86. }
  87. break;
  88. case 'n':
  89. if (implicitKey ||
  90. json[i + 2] === '"' ||
  91. json.length < minMultiLineLength) {
  92. i += 1;
  93. }
  94. else {
  95. // folding will eat first newline
  96. str += json.slice(start, i) + '\n\n';
  97. while (json[i + 2] === '\\' &&
  98. json[i + 3] === 'n' &&
  99. json[i + 4] !== '"') {
  100. str += '\n';
  101. i += 2;
  102. }
  103. str += indent;
  104. // space after newline needs to be escaped to not be folded
  105. if (json[i + 2] === ' ')
  106. str += '\\';
  107. i += 1;
  108. start = i + 1;
  109. }
  110. break;
  111. default:
  112. i += 1;
  113. }
  114. }
  115. str = start ? str + json.slice(start) : json;
  116. return implicitKey
  117. ? str
  118. : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
  119. }
  120. function singleQuotedString(value, ctx) {
  121. if (ctx.options.singleQuote === false ||
  122. (ctx.implicitKey && value.includes('\n')) ||
  123. /[ \t]\n|\n[ \t]/.test(value) // single quoted string can't have leading or trailing whitespace around newline
  124. )
  125. return doubleQuotedString(value, ctx);
  126. const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
  127. const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'";
  128. return ctx.implicitKey
  129. ? res
  130. : foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
  131. }
  132. function quotedString(value, ctx) {
  133. const { singleQuote } = ctx.options;
  134. let qs;
  135. if (singleQuote === false)
  136. qs = doubleQuotedString;
  137. else {
  138. const hasDouble = value.includes('"');
  139. const hasSingle = value.includes("'");
  140. if (hasDouble && !hasSingle)
  141. qs = singleQuotedString;
  142. else if (hasSingle && !hasDouble)
  143. qs = doubleQuotedString;
  144. else
  145. qs = singleQuote ? singleQuotedString : doubleQuotedString;
  146. }
  147. return qs(value, ctx);
  148. }
  149. function blockString({ comment, type, value }, ctx, onComment, onChompKeep) {
  150. const { blockQuote, commentString, lineWidth } = ctx.options;
  151. // 1. Block can't end in whitespace unless the last line is non-empty.
  152. // 2. Strings consisting of only whitespace are best rendered explicitly.
  153. if (!blockQuote || /\n[\t ]+$/.test(value) || /^\s*$/.test(value)) {
  154. return quotedString(value, ctx);
  155. }
  156. const indent = ctx.indent ||
  157. (ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : '');
  158. const literal = blockQuote === 'literal'
  159. ? true
  160. : blockQuote === 'folded' || type === Scalar.Scalar.BLOCK_FOLDED
  161. ? false
  162. : type === Scalar.Scalar.BLOCK_LITERAL
  163. ? true
  164. : !lineLengthOverLimit(value, lineWidth, indent.length);
  165. if (!value)
  166. return literal ? '|\n' : '>\n';
  167. // determine chomping from whitespace at value end
  168. let chomp;
  169. let endStart;
  170. for (endStart = value.length; endStart > 0; --endStart) {
  171. const ch = value[endStart - 1];
  172. if (ch !== '\n' && ch !== '\t' && ch !== ' ')
  173. break;
  174. }
  175. let end = value.substring(endStart);
  176. const endNlPos = end.indexOf('\n');
  177. if (endNlPos === -1) {
  178. chomp = '-'; // strip
  179. }
  180. else if (value === end || endNlPos !== end.length - 1) {
  181. chomp = '+'; // keep
  182. if (onChompKeep)
  183. onChompKeep();
  184. }
  185. else {
  186. chomp = ''; // clip
  187. }
  188. if (end) {
  189. value = value.slice(0, -end.length);
  190. if (end[end.length - 1] === '\n')
  191. end = end.slice(0, -1);
  192. end = end.replace(/\n+(?!\n|$)/g, `$&${indent}`);
  193. }
  194. // determine indent indicator from whitespace at value start
  195. let startWithSpace = false;
  196. let startEnd;
  197. let startNlPos = -1;
  198. for (startEnd = 0; startEnd < value.length; ++startEnd) {
  199. const ch = value[startEnd];
  200. if (ch === ' ')
  201. startWithSpace = true;
  202. else if (ch === '\n')
  203. startNlPos = startEnd;
  204. else
  205. break;
  206. }
  207. let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd);
  208. if (start) {
  209. value = value.substring(start.length);
  210. start = start.replace(/\n+/g, `$&${indent}`);
  211. }
  212. const indentSize = indent ? '2' : '1'; // root is at -1
  213. let header = (literal ? '|' : '>') + (startWithSpace ? indentSize : '') + chomp;
  214. if (comment) {
  215. header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
  216. if (onComment)
  217. onComment();
  218. }
  219. if (literal) {
  220. value = value.replace(/\n+/g, `$&${indent}`);
  221. return `${header}\n${indent}${start}${value}${end}`;
  222. }
  223. value = value
  224. .replace(/\n+/g, '\n$&')
  225. .replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded
  226. // ^ more-ind. ^ empty ^ capture next empty lines only at end of indent
  227. .replace(/\n+/g, `$&${indent}`);
  228. const body = foldFlowLines.foldFlowLines(`${start}${value}${end}`, indent, foldFlowLines.FOLD_BLOCK, getFoldOptions(ctx, true));
  229. return `${header}\n${indent}${body}`;
  230. }
  231. function plainString(item, ctx, onComment, onChompKeep) {
  232. const { type, value } = item;
  233. const { actualString, implicitKey, indent, indentStep, inFlow } = ctx;
  234. if ((implicitKey && /[\n[\]{},]/.test(value)) ||
  235. (inFlow && /[[\]{},]/.test(value))) {
  236. return quotedString(value, ctx);
  237. }
  238. if (!value ||
  239. /^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
  240. // not allowed:
  241. // - empty string, '-' or '?'
  242. // - start with an indicator character (except [?:-]) or /[?-] /
  243. // - '\n ', ': ' or ' \n' anywhere
  244. // - '#' not preceded by a non-space char
  245. // - end with ' ' or ':'
  246. return implicitKey || inFlow || !value.includes('\n')
  247. ? quotedString(value, ctx)
  248. : blockString(item, ctx, onComment, onChompKeep);
  249. }
  250. if (!implicitKey &&
  251. !inFlow &&
  252. type !== Scalar.Scalar.PLAIN &&
  253. value.includes('\n')) {
  254. // Where allowed & type not set explicitly, prefer block style for multiline strings
  255. return blockString(item, ctx, onComment, onChompKeep);
  256. }
  257. if (containsDocumentMarker(value)) {
  258. if (indent === '') {
  259. ctx.forceBlockIndent = true;
  260. return blockString(item, ctx, onComment, onChompKeep);
  261. }
  262. else if (implicitKey && indent === indentStep) {
  263. return quotedString(value, ctx);
  264. }
  265. }
  266. const str = value.replace(/\n+/g, `$&\n${indent}`);
  267. // Verify that output will be parsed as a string, as e.g. plain numbers and
  268. // booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
  269. // and others in v1.1.
  270. if (actualString) {
  271. const test = (tag) => tag.default && tag.tag !== 'tag:yaml.org,2002:str' && tag.test?.test(str);
  272. const { compat, tags } = ctx.doc.schema;
  273. if (tags.some(test) || compat?.some(test))
  274. return quotedString(value, ctx);
  275. }
  276. return implicitKey
  277. ? str
  278. : foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
  279. }
  280. function stringifyString(item, ctx, onComment, onChompKeep) {
  281. const { implicitKey, inFlow } = ctx;
  282. const ss = typeof item.value === 'string'
  283. ? item
  284. : Object.assign({}, item, { value: String(item.value) });
  285. let { type } = item;
  286. if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
  287. // force double quotes on control characters & unpaired surrogates
  288. if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value))
  289. type = Scalar.Scalar.QUOTE_DOUBLE;
  290. }
  291. const _stringify = (_type) => {
  292. switch (_type) {
  293. case Scalar.Scalar.BLOCK_FOLDED:
  294. case Scalar.Scalar.BLOCK_LITERAL:
  295. return implicitKey || inFlow
  296. ? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
  297. : blockString(ss, ctx, onComment, onChompKeep);
  298. case Scalar.Scalar.QUOTE_DOUBLE:
  299. return doubleQuotedString(ss.value, ctx);
  300. case Scalar.Scalar.QUOTE_SINGLE:
  301. return singleQuotedString(ss.value, ctx);
  302. case Scalar.Scalar.PLAIN:
  303. return plainString(ss, ctx, onComment, onChompKeep);
  304. default:
  305. return null;
  306. }
  307. };
  308. let res = _stringify(type);
  309. if (res === null) {
  310. const { defaultKeyType, defaultStringType } = ctx.options;
  311. const t = (implicitKey && defaultKeyType) || defaultStringType;
  312. res = _stringify(t);
  313. if (res === null)
  314. throw new Error(`Unsupported default string type ${t}`);
  315. }
  316. return res;
  317. }
  318. exports.stringifyString = stringifyString;