Nav apraksta

stringifyString.js 12KB

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