resolve-block-scalar.js 7.1 KB

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