resolve-block-scalar.js 7.3 KB

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