resolve-block-scalar.js 7.1 KB

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