resolve-block-scalar.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. 'use strict';
  2. var Scalar = require('../nodes/Scalar.js');
  3. function resolveBlockScalar(ctx, scalar, onError) {
  4. const start = scalar.offset;
  5. const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
  6. if (!header)
  7. return { value: '', type: null, comment: '', range: [start, start, start] };
  8. const type = header.mode === '>' ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
  9. const lines = scalar.source ? splitLines(scalar.source) : [];
  10. // determine the end of content & start of chomping
  11. let chompStart = lines.length;
  12. for (let i = lines.length - 1; i >= 0; --i) {
  13. const content = lines[i][1];
  14. if (content === '' || content === '\r')
  15. chompStart = i;
  16. else
  17. break;
  18. }
  19. // shortcut for empty contents
  20. if (chompStart === 0) {
  21. const value = header.chomp === '+' && lines.length > 0
  22. ? '\n'.repeat(Math.max(1, lines.length - 1))
  23. : '';
  24. let end = start + header.length;
  25. if (scalar.source)
  26. end += scalar.source.length;
  27. return { value, type, comment: header.comment, range: [start, end, end] };
  28. }
  29. // find the indentation level to trim from start
  30. let trimIndent = scalar.indent + header.indent;
  31. let offset = scalar.offset + header.length;
  32. let contentStart = 0;
  33. for (let i = 0; i < chompStart; ++i) {
  34. const [indent, content] = lines[i];
  35. if (content === '' || content === '\r') {
  36. if (header.indent === 0 && indent.length > trimIndent)
  37. trimIndent = indent.length;
  38. }
  39. else {
  40. if (indent.length < trimIndent) {
  41. const message = 'Block scalars with more-indented leading empty lines must use an explicit indentation indicator';
  42. onError(offset + indent.length, 'MISSING_CHAR', message);
  43. }
  44. if (header.indent === 0)
  45. trimIndent = indent.length;
  46. contentStart = i;
  47. if (trimIndent === 0 && !ctx.atRoot) {
  48. const message = 'Block scalar values in collections must be indented';
  49. onError(offset, 'BAD_INDENT', message);
  50. }
  51. break;
  52. }
  53. offset += indent.length + content.length + 1;
  54. }
  55. // include trailing more-indented empty lines in content
  56. for (let i = lines.length - 1; i >= chompStart; --i) {
  57. if (lines[i][0].length > trimIndent)
  58. chompStart = i + 1;
  59. }
  60. let value = '';
  61. let sep = '';
  62. let prevMoreIndented = false;
  63. // leading whitespace is kept intact
  64. for (let i = 0; i < contentStart; ++i)
  65. value += lines[i][0].slice(trimIndent) + '\n';
  66. for (let i = contentStart; i < chompStart; ++i) {
  67. let [indent, content] = lines[i];
  68. offset += indent.length + content.length + 1;
  69. const crlf = content[content.length - 1] === '\r';
  70. if (crlf)
  71. content = content.slice(0, -1);
  72. /* istanbul ignore if already caught in lexer */
  73. if (content && indent.length < trimIndent) {
  74. const src = header.indent
  75. ? 'explicit indentation indicator'
  76. : 'first line';
  77. const message = `Block scalar lines must not be less indented than their ${src}`;
  78. onError(offset - content.length - (crlf ? 2 : 1), 'BAD_INDENT', message);
  79. indent = '';
  80. }
  81. if (type === Scalar.Scalar.BLOCK_LITERAL) {
  82. value += sep + indent.slice(trimIndent) + content;
  83. sep = '\n';
  84. }
  85. else if (indent.length > trimIndent || content[0] === '\t') {
  86. // more-indented content within a folded block
  87. if (sep === ' ')
  88. sep = '\n';
  89. else if (!prevMoreIndented && sep === '\n')
  90. sep = '\n\n';
  91. value += sep + indent.slice(trimIndent) + content;
  92. sep = '\n';
  93. prevMoreIndented = true;
  94. }
  95. else if (content === '') {
  96. // empty line
  97. if (sep === '\n')
  98. value += '\n';
  99. else
  100. sep = '\n';
  101. }
  102. else {
  103. value += sep + content;
  104. sep = ' ';
  105. prevMoreIndented = false;
  106. }
  107. }
  108. switch (header.chomp) {
  109. case '-':
  110. break;
  111. case '+':
  112. for (let i = chompStart; i < lines.length; ++i)
  113. value += '\n' + lines[i][0].slice(trimIndent);
  114. if (value[value.length - 1] !== '\n')
  115. value += '\n';
  116. break;
  117. default:
  118. value += '\n';
  119. }
  120. const end = start + header.length + scalar.source.length;
  121. return { value, type, comment: header.comment, range: [start, end, end] };
  122. }
  123. function parseBlockScalarHeader({ offset, props }, strict, onError) {
  124. /* istanbul ignore if should not happen */
  125. if (props[0].type !== 'block-scalar-header') {
  126. onError(props[0], 'IMPOSSIBLE', 'Block scalar header not found');
  127. return null;
  128. }
  129. const { source } = props[0];
  130. const mode = source[0];
  131. let indent = 0;
  132. let chomp = '';
  133. let error = -1;
  134. for (let i = 1; i < source.length; ++i) {
  135. const ch = source[i];
  136. if (!chomp && (ch === '-' || ch === '+'))
  137. chomp = ch;
  138. else {
  139. const n = Number(ch);
  140. if (!indent && n)
  141. indent = n;
  142. else if (error === -1)
  143. error = offset + i;
  144. }
  145. }
  146. if (error !== -1)
  147. onError(error, 'UNEXPECTED_TOKEN', `Block scalar header includes extra characters: ${source}`);
  148. let hasSpace = false;
  149. let comment = '';
  150. let length = source.length;
  151. for (let i = 1; i < props.length; ++i) {
  152. const token = props[i];
  153. switch (token.type) {
  154. case 'space':
  155. hasSpace = true;
  156. // fallthrough
  157. case 'newline':
  158. length += token.source.length;
  159. break;
  160. case 'comment':
  161. if (strict && !hasSpace) {
  162. const message = 'Comments must be separated from other tokens by white space characters';
  163. onError(token, 'MISSING_CHAR', message);
  164. }
  165. length += token.source.length;
  166. comment = token.source.substring(1);
  167. break;
  168. case 'error':
  169. onError(token, 'UNEXPECTED_TOKEN', token.message);
  170. length += token.source.length;
  171. break;
  172. /* istanbul ignore next should not happen */
  173. default: {
  174. const message = `Unexpected token in block scalar header: ${token.type}`;
  175. onError(token, 'UNEXPECTED_TOKEN', message);
  176. const ts = token.source;
  177. if (ts && typeof ts === 'string')
  178. length += ts.length;
  179. }
  180. }
  181. }
  182. return { mode, indent, chomp, comment, length };
  183. }
  184. /** @returns Array of lines split up as `[indent, content]` */
  185. function splitLines(source) {
  186. const split = source.split(/\n( *)/);
  187. const first = split[0];
  188. const m = first.match(/^( *)/);
  189. const line0 = m?.[1]
  190. ? [m[1], first.slice(m[1].length)]
  191. : ['', first];
  192. const lines = [line0];
  193. for (let i = 1; i < split.length; i += 2)
  194. lines.push([split[i], split[i + 1]]);
  195. return lines;
  196. }
  197. exports.resolveBlockScalar = resolveBlockScalar;