resolve-flow-collection.js 8.3 KB

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