resolve-flow-collection.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import { isPair } from '../nodes/identity.js';
  2. import { Pair } from '../nodes/Pair.js';
  3. import { YAMLMap } from '../nodes/YAMLMap.js';
  4. import { YAMLSeq } from '../nodes/YAMLSeq.js';
  5. import { resolveEnd } from './resolve-end.js';
  6. import { resolveProps } from './resolve-props.js';
  7. import { containsNewline } from './util-contains-newline.js';
  8. import { mapIncludes } from './util-map-includes.js';
  9. const blockMsg = 'Block collections are not allowed within flow collections';
  10. const isBlock = (token) => token && (token.type === 'block-map' || token.type === 'block-seq');
  11. function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
  12. const isMap = fc.start.source === '{';
  13. const fcName = isMap ? 'flow map' : 'flow sequence';
  14. const NodeClass = (tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq));
  15. const coll = new NodeClass(ctx.schema);
  16. coll.flow = true;
  17. const atRoot = ctx.atRoot;
  18. if (atRoot)
  19. ctx.atRoot = false;
  20. if (ctx.atKey)
  21. ctx.atKey = false;
  22. let offset = fc.offset + fc.start.source.length;
  23. for (let i = 0; i < fc.items.length; ++i) {
  24. const collItem = fc.items[i];
  25. const { start, key, sep, value } = collItem;
  26. const props = resolveProps(start, {
  27. flow: fcName,
  28. indicator: 'explicit-key-ind',
  29. next: key ?? sep?.[0],
  30. offset,
  31. onError,
  32. parentIndent: fc.indent,
  33. startOnNewline: false
  34. });
  35. if (!props.found) {
  36. if (!props.anchor && !props.tag && !sep && !value) {
  37. if (i === 0 && props.comma)
  38. onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`);
  39. else if (i < fc.items.length - 1)
  40. onError(props.start, 'UNEXPECTED_TOKEN', `Unexpected empty item in ${fcName}`);
  41. if (props.comment) {
  42. if (coll.comment)
  43. coll.comment += '\n' + props.comment;
  44. else
  45. coll.comment = props.comment;
  46. }
  47. offset = props.end;
  48. continue;
  49. }
  50. if (!isMap && ctx.options.strict && containsNewline(key))
  51. onError(key, // checked by containsNewline()
  52. 'MULTILINE_IMPLICIT_KEY', 'Implicit keys of flow sequence pairs need to be on a single line');
  53. }
  54. if (i === 0) {
  55. if (props.comma)
  56. onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`);
  57. }
  58. else {
  59. if (!props.comma)
  60. onError(props.start, 'MISSING_CHAR', `Missing , between ${fcName} items`);
  61. if (props.comment) {
  62. let prevItemComment = '';
  63. loop: for (const st of start) {
  64. switch (st.type) {
  65. case 'comma':
  66. case 'space':
  67. break;
  68. case 'comment':
  69. prevItemComment = st.source.substring(1);
  70. break loop;
  71. default:
  72. break loop;
  73. }
  74. }
  75. if (prevItemComment) {
  76. let prev = coll.items[coll.items.length - 1];
  77. if (isPair(prev))
  78. prev = prev.value ?? prev.key;
  79. if (prev.comment)
  80. prev.comment += '\n' + prevItemComment;
  81. else
  82. prev.comment = prevItemComment;
  83. props.comment = props.comment.substring(prevItemComment.length + 1);
  84. }
  85. }
  86. }
  87. if (!isMap && !sep && !props.found) {
  88. // item is a value in a seq
  89. // → key & sep are empty, start does not include ? or :
  90. const valueNode = value
  91. ? composeNode(ctx, value, props, onError)
  92. : composeEmptyNode(ctx, props.end, sep, null, props, onError);
  93. coll.items.push(valueNode);
  94. offset = valueNode.range[2];
  95. if (isBlock(value))
  96. onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg);
  97. }
  98. else {
  99. // item is a key+value pair
  100. // key value
  101. ctx.atKey = true;
  102. const keyStart = props.end;
  103. const keyNode = key
  104. ? composeNode(ctx, key, props, onError)
  105. : composeEmptyNode(ctx, keyStart, start, null, props, onError);
  106. if (isBlock(key))
  107. onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg);
  108. ctx.atKey = false;
  109. // value properties
  110. const valueProps = resolveProps(sep ?? [], {
  111. flow: fcName,
  112. indicator: 'map-value-ind',
  113. next: value,
  114. offset: keyNode.range[2],
  115. onError,
  116. parentIndent: fc.indent,
  117. startOnNewline: false
  118. });
  119. if (valueProps.found) {
  120. if (!isMap && !props.found && ctx.options.strict) {
  121. if (sep)
  122. for (const st of sep) {
  123. if (st === valueProps.found)
  124. break;
  125. if (st.type === 'newline') {
  126. onError(st, 'MULTILINE_IMPLICIT_KEY', 'Implicit keys of flow sequence pairs need to be on a single line');
  127. break;
  128. }
  129. }
  130. if (props.start < valueProps.found.offset - 1024)
  131. onError(valueProps.found, 'KEY_OVER_1024_CHARS', 'The : indicator must be at most 1024 chars after the start of an implicit flow sequence key');
  132. }
  133. }
  134. else if (value) {
  135. if ('source' in value && value.source && value.source[0] === ':')
  136. onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`);
  137. else
  138. onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`);
  139. }
  140. // value value
  141. const valueNode = value
  142. ? composeNode(ctx, value, valueProps, onError)
  143. : valueProps.found
  144. ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError)
  145. : null;
  146. if (valueNode) {
  147. if (isBlock(value))
  148. onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg);
  149. }
  150. else if (valueProps.comment) {
  151. if (keyNode.comment)
  152. keyNode.comment += '\n' + valueProps.comment;
  153. else
  154. keyNode.comment = valueProps.comment;
  155. }
  156. const pair = new Pair(keyNode, valueNode);
  157. if (ctx.options.keepSourceTokens)
  158. pair.srcToken = collItem;
  159. if (isMap) {
  160. const map = coll;
  161. if (mapIncludes(ctx, map.items, keyNode))
  162. onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique');
  163. map.items.push(pair);
  164. }
  165. else {
  166. const map = new YAMLMap(ctx.schema);
  167. map.flow = true;
  168. map.items.push(pair);
  169. const endRange = (valueNode ?? keyNode).range;
  170. map.range = [keyNode.range[0], endRange[1], endRange[2]];
  171. coll.items.push(map);
  172. }
  173. offset = valueNode ? valueNode.range[2] : valueProps.end;
  174. }
  175. }
  176. const expectedEnd = isMap ? '}' : ']';
  177. const [ce, ...ee] = fc.end;
  178. let cePos = offset;
  179. if (ce && ce.source === expectedEnd)
  180. cePos = ce.offset + ce.source.length;
  181. else {
  182. const name = fcName[0].toUpperCase() + fcName.substring(1);
  183. const msg = atRoot
  184. ? `${name} must end with a ${expectedEnd}`
  185. : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`;
  186. onError(offset, atRoot ? 'MISSING_CHAR' : 'BAD_INDENT', msg);
  187. if (ce && ce.source.length !== 1)
  188. ee.unshift(ce);
  189. }
  190. if (ee.length > 0) {
  191. const end = resolveEnd(ee, cePos, ctx.options.strict, onError);
  192. if (end.comment) {
  193. if (coll.comment)
  194. coll.comment += '\n' + end.comment;
  195. else
  196. coll.comment = end.comment;
  197. }
  198. coll.range = [fc.offset, cePos, end.offset];
  199. }
  200. else {
  201. coll.range = [fc.offset, cePos, cePos];
  202. }
  203. return coll;
  204. }
  205. export { resolveFlowCollection };