json-schema-to-grammar.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. // WARNING: This file was ported from json_schema_to_grammar.py, please fix bugs / add features there first.
  2. const SPACE_RULE = '" "?';
  3. function _buildRepetition(itemRule, minItems, maxItems, opts={}) {
  4. const separatorRule = opts.separatorRule ?? '';
  5. const itemRuleIsLiteral = opts.itemRuleIsLiteral ?? false
  6. if (separatorRule === '') {
  7. if (minItems === 0 && maxItems === 1) {
  8. return `${itemRule}?`;
  9. } else if (minItems === 1 && maxItems === undefined) {
  10. return `${itemRule}+`;
  11. }
  12. }
  13. let result = '';
  14. if (minItems > 0) {
  15. if (itemRuleIsLiteral && separatorRule === '') {
  16. result = `"${itemRule.slice(1, -1).repeat(minItems)}"`;
  17. } else {
  18. result = Array.from({ length: minItems }, () => itemRule)
  19. .join(separatorRule !== '' ? ` ${separatorRule} ` : ' ');
  20. }
  21. }
  22. const optRepetitions = (upToN, prefixWithSep=false) => {
  23. const content = separatorRule !== '' && prefixWithSep ? `${separatorRule} ${itemRule}` : itemRule;
  24. if (upToN === 0) {
  25. return '';
  26. } else if (upToN === 1) {
  27. return `(${content})?`;
  28. } else if (separatorRule !== '' && !prefixWithSep) {
  29. return `(${content} ${optRepetitions(upToN - 1, true)})?`;
  30. } else {
  31. return Array.from({ length: upToN }, () => `(${content}`).join(' ').trim() + Array.from({ length: upToN }, () => ')?').join('');
  32. }
  33. };
  34. if (minItems > 0 && maxItems !== minItems) {
  35. result += ' ';
  36. }
  37. if (maxItems !== undefined) {
  38. result += optRepetitions(maxItems - minItems, minItems > 0);
  39. } else {
  40. const itemOperator = `(${separatorRule !== '' ? separatorRule + ' ' : ''}${itemRule})`;
  41. if (minItems === 0 && separatorRule !== '') {
  42. result = `(${itemRule} ${itemOperator}*)?`;
  43. } else {
  44. result += `${itemOperator}*`;
  45. }
  46. }
  47. return result;
  48. }
  49. class BuiltinRule {
  50. constructor(content, deps) {
  51. this.content = content;
  52. this.deps = deps || [];
  53. }
  54. }
  55. const UP_TO_15_DIGITS = _buildRepetition('[0-9]', 0, 15);
  56. const PRIMITIVE_RULES = {
  57. boolean : new BuiltinRule('("true" | "false") space', []),
  58. 'decimal-part' : new BuiltinRule('[0-9] ' + UP_TO_15_DIGITS, []),
  59. 'integral-part': new BuiltinRule('[0-9] | [1-9] ' + UP_TO_15_DIGITS, []),
  60. number : new BuiltinRule('("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space', ['integral-part', 'decimal-part']),
  61. integer : new BuiltinRule('("-"? integral-part) space', ['integral-part']),
  62. value : new BuiltinRule('object | array | string | number | boolean | null', ['object', 'array', 'string', 'number', 'boolean', 'null']),
  63. object : new BuiltinRule('"{" space ( string ":" space value ("," space string ":" space value)* )? "}" space', ['string', 'value']),
  64. array : new BuiltinRule('"[" space ( value ("," space value)* )? "]" space', ['value']),
  65. uuid : new BuiltinRule('"\\"" ' + [8, 4, 4, 4, 12].map(n => [...new Array(n)].map(_ => '[0-9a-fA-F]').join('')).join(' "-" ') + ' "\\"" space', []),
  66. char : new BuiltinRule(`[^"\\\\] | "\\\\" (["\\\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])`, []),
  67. string : new BuiltinRule(`"\\"" char* "\\"" space`, ['char']),
  68. null : new BuiltinRule('"null" space', []),
  69. };
  70. // TODO: support "uri", "email" string formats
  71. const STRING_FORMAT_RULES = {
  72. 'date' : new BuiltinRule('[0-9] [0-9] [0-9] [0-9] "-" ( "0" [1-9] | "1" [0-2] ) "-" ( \"0\" [1-9] | [1-2] [0-9] | "3" [0-1] )', []),
  73. 'time' : new BuiltinRule('([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9] [0-9] [0-9] )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )', []),
  74. 'date-time' : new BuiltinRule('date "T" time', ['date', 'time']),
  75. 'date-string' : new BuiltinRule('"\\"" date "\\"" space', ['date']),
  76. 'time-string' : new BuiltinRule('"\\"" time "\\"" space', ['time']),
  77. 'date-time-string': new BuiltinRule('"\\"" date-time "\\"" space', ['date-time']),
  78. }
  79. const RESERVED_NAMES = {'root': true, ...PRIMITIVE_RULES, ...STRING_FORMAT_RULES};
  80. const INVALID_RULE_CHARS_RE = /[^\dA-Za-z-]+/g;
  81. const GRAMMAR_LITERAL_ESCAPE_RE = /[\n\r"]/g;
  82. const GRAMMAR_RANGE_LITERAL_ESCAPE_RE = /[\n\r"\]\-\\]/g;
  83. const GRAMMAR_LITERAL_ESCAPES = { '\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]' };
  84. const NON_LITERAL_SET = new Set('|.()[]{}*+?');
  85. const ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = new Set('[]()|{}*+?');
  86. export class SchemaConverter {
  87. constructor(options) {
  88. this._propOrder = options.prop_order || {};
  89. this._allowFetch = options.allow_fetch || false;
  90. this._dotall = options.dotall || false;
  91. this._rules = {'space': SPACE_RULE};
  92. this._refs = {};
  93. this._refsBeingResolved = new Set();
  94. }
  95. _formatLiteral(literal) {
  96. const escaped = literal.replace(
  97. GRAMMAR_LITERAL_ESCAPE_RE,
  98. m => GRAMMAR_LITERAL_ESCAPES[m]
  99. );
  100. return `"${escaped}"`;
  101. }
  102. _formatRangeChar(literal) {
  103. return JSON.stringify(literal).slice(1, -1).replace(
  104. GRAMMAR_RANGE_LITERAL_ESCAPE_RE,
  105. m => GRAMMAR_LITERAL_ESCAPES[m]
  106. );
  107. }
  108. _addRule(name, rule) {
  109. let escName = name.replace(INVALID_RULE_CHARS_RE, '-');
  110. let key = escName;
  111. if (escName in this._rules) {
  112. if (this._rules[escName] === rule) {
  113. return key;
  114. }
  115. let i = 0;
  116. while ((`${escName}${i}` in this._rules) && (this._rules[`${escName}${i}`] !== rule)) {
  117. i += 1;
  118. }
  119. key = `${escName}${i}`;
  120. }
  121. this._rules[key] = rule;
  122. return key;
  123. }
  124. async resolveRefs(schema, url) {
  125. const visit = async (n) => {
  126. if (Array.isArray(n)) {
  127. return Promise.all(n.map(visit));
  128. } else if (typeof n === 'object' && n !== null) {
  129. let ref = n.$ref;
  130. let target;
  131. if (ref !== undefined && !this._refs[ref]) {
  132. if (ref.startsWith('https://')) {
  133. if (!this._allowFetch) {
  134. throw new Error('Fetching remote schemas is not allowed (use --allow-fetch for force)');
  135. }
  136. const fetch = (await import('node-fetch')).default;
  137. const fragSplit = ref.split('#');
  138. const baseUrl = fragSplit[0];
  139. target = this._refs[baseUrl];
  140. if (!target) {
  141. target = await this.resolveRefs(await fetch(ref).then(res => res.json()), baseUrl);
  142. this._refs[baseUrl] = target;
  143. }
  144. if (fragSplit.length === 1 || fragSplit[fragSplit.length - 1] === '') {
  145. return target;
  146. }
  147. } else if (ref.startsWith('#/')) {
  148. target = schema;
  149. ref = `${url}${ref}`;
  150. n.$ref = ref;
  151. } else {
  152. throw new Error(`Unsupported ref ${ref}`);
  153. }
  154. const selectors = ref.split('#')[1].split('/').slice(1);
  155. for (const sel of selectors) {
  156. if (!target || !(sel in target)) {
  157. throw new Error(`Error resolving ref ${ref}: ${sel} not in ${JSON.stringify(target)}`);
  158. }
  159. target = target[sel];
  160. }
  161. this._refs[ref] = target;
  162. } else {
  163. await Promise.all(Object.values(n).map(visit));
  164. }
  165. }
  166. return n;
  167. };
  168. return visit(schema);
  169. }
  170. _generateUnionRule(name, altSchemas) {
  171. return altSchemas
  172. .map((altSchema, i) => this.visit(altSchema, `${name ?? ''}${name ? '-' : 'alternative-'}${i}`))
  173. .join(' | ');
  174. }
  175. _visitPattern(pattern, name) {
  176. if (!pattern.startsWith('^') || !pattern.endsWith('$')) {
  177. throw new Error('Pattern must start with "^" and end with "$"');
  178. }
  179. pattern = pattern.slice(1, -1);
  180. const subRuleIds = {};
  181. let i = 0;
  182. const length = pattern.length;
  183. const getDot = () => {
  184. let rule;
  185. if (this._dotall) {
  186. rule = '[\\U00000000-\\U0010FFFF]';
  187. } else {
  188. // Accept any character... except \n and \r line break chars (\x0A and \xOD)
  189. rule = '[^\\x0A\\x0D]';
  190. }
  191. return this._addRule('dot', rule);
  192. };
  193. const toRule = ([s, isLiteral]) => isLiteral ? "\"" + s + "\"" : s;
  194. const transform = () => {
  195. const start = i;
  196. // For each component of this sequence, store its string representation and whether it's a literal.
  197. // We only need a flat structure here to apply repetition operators to the last item, and
  198. // to merge literals at the and (we're parsing grouped ( sequences ) recursively and don't treat '|' specially
  199. // (GBNF's syntax is luckily very close to regular expressions!)
  200. const seq = [];
  201. const joinSeq = () => {
  202. const ret = [];
  203. for (const [isLiteral, g] of groupBy(seq, x => x[1])) {
  204. if (isLiteral) {
  205. ret.push([[...g].map(x => x[0]).join(''), true]);
  206. } else {
  207. ret.push(...g);
  208. }
  209. }
  210. if (ret.length === 1) {
  211. return ret[0];
  212. }
  213. return [ret.map(x => toRule(x)).join(' '), false];
  214. };
  215. while (i < length) {
  216. const c = pattern[i];
  217. if (c === '.') {
  218. seq.push([getDot(), false]);
  219. i += 1;
  220. } else if (c === '(') {
  221. i += 1;
  222. if (i < length) {
  223. if (pattern[i] === '?') {
  224. throw new Error(`Unsupported pattern syntax "${pattern[i]}" at index ${i} of /${pattern}/`);
  225. }
  226. }
  227. seq.push([`(${toRule(transform())})`, false]);
  228. } else if (c === ')') {
  229. i += 1;
  230. if (start <= 0 || pattern[start - 1] !== '(') {
  231. throw new Error(`Unbalanced parentheses; start = ${start}, i = ${i}, pattern = ${pattern}`);
  232. }
  233. return joinSeq();
  234. } else if (c === '[') {
  235. let squareBrackets = c;
  236. i += 1;
  237. while (i < length && pattern[i] !== ']') {
  238. if (pattern[i] === '\\') {
  239. squareBrackets += pattern.slice(i, i + 2);
  240. i += 2;
  241. } else {
  242. squareBrackets += pattern[i];
  243. i += 1;
  244. }
  245. }
  246. if (i >= length) {
  247. throw new Error(`Unbalanced square brackets; start = ${start}, i = ${i}, pattern = ${pattern}`);
  248. }
  249. squareBrackets += ']';
  250. i += 1;
  251. seq.push([squareBrackets, false]);
  252. } else if (c === '|') {
  253. seq.push(['|', false]);
  254. i += 1;
  255. } else if (c === '*' || c === '+' || c === '?') {
  256. seq[seq.length - 1] = [toRule(seq[seq.length - 1]) + c, false];
  257. i += 1;
  258. } else if (c === '{') {
  259. let curlyBrackets = c;
  260. i += 1;
  261. while (i < length && pattern[i] !== '}') {
  262. curlyBrackets += pattern[i];
  263. i += 1;
  264. }
  265. if (i >= length) {
  266. throw new Error(`Unbalanced curly brackets; start = ${start}, i = ${i}, pattern = ${pattern}`);
  267. }
  268. curlyBrackets += '}';
  269. i += 1;
  270. const nums = curlyBrackets.slice(1, -1).split(',').map(s => s.trim());
  271. let minTimes, maxTimes;
  272. if (nums.length === 1) {
  273. minTimes = parseInt(nums[0], 10);
  274. maxTimes = minTimes;
  275. } else {
  276. if (nums.length !== 2) {
  277. throw new Error(`Invalid quantifier ${curlyBrackets}`);
  278. }
  279. minTimes = nums[0] ? parseInt(nums[0], 10) : 0;
  280. maxTimes = nums[1] ? parseInt(nums[1], 10) : Infinity;
  281. }
  282. let [sub, subIsLiteral] = seq[seq.length - 1];
  283. if (!subIsLiteral) {
  284. let id = subRuleIds[sub];
  285. if (id === undefined) {
  286. id = this._addRule(`${name}-${Object.keys(subRuleIds).length + 1}`, sub);
  287. subRuleIds[sub] = id;
  288. }
  289. sub = id;
  290. }
  291. seq[seq.length - 1] = [
  292. _buildRepetition(subIsLiteral ? `"${sub}"` : sub, minTimes, maxTimes, {itemRuleIsLiteral: subIsLiteral}),
  293. false
  294. ];
  295. } else {
  296. let literal = '';
  297. while (i < length) {
  298. if (pattern[i] === '\\' && i < length - 1) {
  299. const next = pattern[i + 1];
  300. if (ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.has(next)) {
  301. i += 1;
  302. literal += pattern[i];
  303. i += 1;
  304. } else {
  305. literal += pattern.slice(i, i + 2);
  306. i += 2;
  307. }
  308. } else if (pattern[i] === '"') {
  309. literal += '\\"';
  310. i += 1;
  311. } else if (!NON_LITERAL_SET.has(pattern[i]) &&
  312. (i === length - 1 || literal === '' || pattern[i + 1] === '.' || !NON_LITERAL_SET.has(pattern[i+1]))) {
  313. literal += pattern[i];
  314. i += 1;
  315. } else {
  316. break;
  317. }
  318. }
  319. if (literal !== '') {
  320. seq.push([literal, true]);
  321. }
  322. }
  323. }
  324. return joinSeq();
  325. };
  326. return this._addRule(name, "\"\\\"\" " + toRule(transform()) + " \"\\\"\" space")
  327. }
  328. _resolveRef(ref) {
  329. let refName = ref.split('/').pop();
  330. if (!(refName in this._rules) && !this._refsBeingResolved.has(ref)) {
  331. this._refsBeingResolved.add(ref);
  332. const resolved = this._refs[ref];
  333. refName = this.visit(resolved, refName);
  334. this._refsBeingResolved.delete(ref);
  335. }
  336. return refName;
  337. }
  338. _generateConstantRule(value) {
  339. return this._formatLiteral(JSON.stringify(value));
  340. }
  341. visit(schema, name) {
  342. const schemaType = schema.type;
  343. const schemaFormat = schema.format;
  344. const ruleName = name in RESERVED_NAMES ? name + '-' : name == '' ? 'root' : name;
  345. const ref = schema.$ref;
  346. if (ref !== undefined) {
  347. return this._addRule(ruleName, this._resolveRef(ref));
  348. } else if (schema.oneOf || schema.anyOf) {
  349. return this._addRule(ruleName, this._generateUnionRule(name, schema.oneOf || schema.anyOf));
  350. } else if (Array.isArray(schemaType)) {
  351. return this._addRule(ruleName, this._generateUnionRule(name, schemaType.map(t => ({ type: t }))));
  352. } else if ('const' in schema) {
  353. return this._addRule(ruleName, this._generateConstantRule(schema.const));
  354. } else if ('enum' in schema) {
  355. const rule = schema.enum.map(v => this._generateConstantRule(v)).join(' | ');
  356. return this._addRule(ruleName, rule);
  357. } else if ((schemaType === undefined || schemaType === 'object') &&
  358. ('properties' in schema ||
  359. ('additionalProperties' in schema && schema.additionalProperties !== true))) {
  360. const required = new Set(schema.required || []);
  361. const properties = Object.entries(schema.properties ?? {});
  362. return this._addRule(ruleName, this._buildObjectRule(properties, required, name, schema.additionalProperties));
  363. } else if ((schemaType === undefined || schemaType === 'object') && 'allOf' in schema) {
  364. const required = new Set();
  365. const properties = [];
  366. const addComponent = (compSchema, isRequired) => {
  367. const ref = compSchema.$ref;
  368. if (ref !== undefined) {
  369. compSchema = this._refs[ref];
  370. }
  371. if ('properties' in compSchema) {
  372. for (const [propName, propSchema] of Object.entries(compSchema.properties)) {
  373. properties.push([propName, propSchema]);
  374. if (isRequired) {
  375. required.add(propName);
  376. }
  377. }
  378. }
  379. };
  380. for (const t of schema.allOf) {
  381. if ('anyOf' in t) {
  382. for (const tt of t.anyOf) {
  383. addComponent(tt, false);
  384. }
  385. } else {
  386. addComponent(t, true);
  387. }
  388. }
  389. return this._addRule(ruleName, this._buildObjectRule(properties, required, name, /* additionalProperties= */ false));
  390. } else if ((schemaType === undefined || schemaType === 'array') && ('items' in schema || 'prefixItems' in schema)) {
  391. const items = schema.items ?? schema.prefixItems;
  392. if (Array.isArray(items)) {
  393. return this._addRule(
  394. ruleName,
  395. '"[" space ' +
  396. items.map((item, i) => this.visit(item, `${name ?? ''}${name ? '-' : ''}tuple-${i}`)).join(' "," space ') +
  397. ' "]" space'
  398. );
  399. } else {
  400. const itemRuleName = this.visit(items, `${name ?? ''}${name ? '-' : ''}item`);
  401. const minItems = schema.minItems || 0;
  402. const maxItems = schema.maxItems;
  403. return this._addRule(ruleName, '"[" space ' + _buildRepetition(itemRuleName, minItems, maxItems, {separatorRule: '"," space'}) + ' "]" space');
  404. }
  405. } else if ((schemaType === undefined || schemaType === 'string') && 'pattern' in schema) {
  406. return this._visitPattern(schema.pattern, ruleName);
  407. } else if ((schemaType === undefined || schemaType === 'string') && /^uuid[1-5]?$/.test(schema.format || '')) {
  408. return this._addPrimitive(
  409. ruleName === 'root' ? 'root' : schemaFormat,
  410. PRIMITIVE_RULES['uuid']
  411. );
  412. } else if ((schemaType === undefined || schemaType === 'string') && `${schema.format}-string` in STRING_FORMAT_RULES) {
  413. const primName = `${schema.format}-string`
  414. return this._addRule(ruleName, this._addPrimitive(primName, STRING_FORMAT_RULES[primName]));
  415. } else if (schemaType === 'string' && ('minLength' in schema || 'maxLength' in schema)) {
  416. const charRuleName = this._addPrimitive('char', PRIMITIVE_RULES['char']);
  417. const minLen = schema.minLength || 0;
  418. const maxLen = schema.maxLength;
  419. return this._addRule(ruleName, '"\\\"" ' + _buildRepetition(charRuleName, minLen, maxLen) + ' "\\\"" space');
  420. } else if ((schemaType === 'object') || (Object.keys(schema).length === 0)) {
  421. return this._addRule(ruleName, this._addPrimitive('object', PRIMITIVE_RULES['object']));
  422. } else {
  423. if (!(schemaType in PRIMITIVE_RULES)) {
  424. throw new Error(`Unrecognized schema: ${JSON.stringify(schema)}`);
  425. }
  426. // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
  427. return this._addPrimitive(ruleName === 'root' ? 'root' : schemaType, PRIMITIVE_RULES[schemaType]);
  428. }
  429. }
  430. _addPrimitive(name, rule) {
  431. let n = this._addRule(name, rule.content);
  432. for (const dep of rule.deps) {
  433. const depRule = PRIMITIVE_RULES[dep] || STRING_FORMAT_RULES[dep];
  434. if (!depRule) {
  435. throw new Error(`Rule ${dep} not known`);
  436. }
  437. if (!(dep in this._rules)) {
  438. this._addPrimitive(dep, depRule);
  439. }
  440. }
  441. return n;
  442. }
  443. _buildObjectRule(properties, required, name, additionalProperties) {
  444. const propOrder = this._propOrder;
  445. // sort by position in prop_order (if specified) then by original order
  446. const sortedProps = properties.map(([k]) => k).sort((a, b) => {
  447. const orderA = propOrder[a] || Infinity;
  448. const orderB = propOrder[b] || Infinity;
  449. return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
  450. });
  451. const propKvRuleNames = {};
  452. for (const [propName, propSchema] of properties) {
  453. const propRuleName = this.visit(propSchema, `${name ?? ''}${name ? '-' : ''}${propName}`);
  454. propKvRuleNames[propName] = this._addRule(
  455. `${name ?? ''}${name ? '-' : ''}${propName}-kv`,
  456. `${this._formatLiteral(JSON.stringify(propName))} space ":" space ${propRuleName}`
  457. );
  458. }
  459. const requiredProps = sortedProps.filter(k => required.has(k));
  460. const optionalProps = sortedProps.filter(k => !required.has(k));
  461. if (typeof additionalProperties === 'object' || additionalProperties === true) {
  462. const subName = `${name ?? ''}${name ? '-' : ''}additional`;
  463. const valueRule = this.visit(additionalProperties === true ? {} : additionalProperties, `${subName}-value`);
  464. propKvRuleNames['*'] = this._addRule(
  465. `${subName}-kv`,
  466. `${this._addPrimitive('string', PRIMITIVE_RULES['string'])} ":" space ${valueRule}`);
  467. optionalProps.push('*');
  468. }
  469. let rule = '"{" space ';
  470. rule += requiredProps.map(k => propKvRuleNames[k]).join(' "," space ');
  471. if (optionalProps.length > 0) {
  472. rule += ' (';
  473. if (requiredProps.length > 0) {
  474. rule += ' "," space ( ';
  475. }
  476. const getRecursiveRefs = (ks, firstIsOptional) => {
  477. const [k, ...rest] = ks;
  478. const kvRuleName = propKvRuleNames[k];
  479. let res;
  480. if (k === '*') {
  481. res = this._addRule(
  482. `${name ?? ''}${name ? '-' : ''}additional-kvs`,
  483. `${kvRuleName} ( "," space ` + kvRuleName + ` )*`
  484. )
  485. } else if (firstIsOptional) {
  486. res = `( "," space ${kvRuleName} )?`;
  487. } else {
  488. res = kvRuleName;
  489. }
  490. if (rest.length > 0) {
  491. res += ' ' + this._addRule(
  492. `${name ?? ''}${name ? '-' : ''}${k}-rest`,
  493. getRecursiveRefs(rest, true)
  494. );
  495. }
  496. return res;
  497. };
  498. rule += optionalProps.map((_, i) => getRecursiveRefs(optionalProps.slice(i), false)).join(' | ');
  499. if (requiredProps.length > 0) {
  500. rule += ' )';
  501. }
  502. rule += ' )?';
  503. }
  504. rule += ' "}" space';
  505. return rule;
  506. }
  507. formatGrammar() {
  508. let grammar = '';
  509. for (const [name, rule] of Object.entries(this._rules).sort(([a], [b]) => a.localeCompare(b))) {
  510. grammar += `${name} ::= ${rule}\n`;
  511. }
  512. return grammar;
  513. }
  514. }
  515. // Helper function to group elements by a key function
  516. function* groupBy(iterable, keyFn) {
  517. let lastKey = null;
  518. let group = [];
  519. for (const element of iterable) {
  520. const key = keyFn(element);
  521. if (lastKey !== null && key !== lastKey) {
  522. yield [lastKey, group];
  523. group = [];
  524. }
  525. group.push(element);
  526. lastKey = key;
  527. }
  528. if (group.length > 0) {
  529. yield [lastKey, group];
  530. }
  531. }