diff --git a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemPredicate.spec.js b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemPredicate.spec.js index 9da44431e..f8376f406 100644 --- a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemPredicate.spec.js +++ b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemPredicate.spec.js @@ -1,6 +1,6 @@ exports['mcfunction argument minecraft:item_predicate Parse "#stick" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_predicate", "range": { "start": 0, "end": 6 @@ -22,7 +22,7 @@ exports['mcfunction argument minecraft:item_predicate Parse "#stick" 1'] = { exports['mcfunction argument minecraft:item_predicate Parse "#stick{foo:bar}" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_predicate", "range": { "start": 0, "end": 15 @@ -146,7 +146,7 @@ exports['mcfunction argument minecraft:item_predicate Parse "#stick{foo:bar}" 1' exports['mcfunction argument minecraft:item_predicate Parse "minecraft:stick" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_predicate", "range": { "start": 0, "end": 15 @@ -168,7 +168,7 @@ exports['mcfunction argument minecraft:item_predicate Parse "minecraft:stick" 1' exports['mcfunction argument minecraft:item_predicate Parse "stick" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_predicate", "range": { "start": 0, "end": 5 diff --git a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemStack.spec.js b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemStack.spec.js index 61dedbebe..777a13d67 100644 --- a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemStack.spec.js +++ b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftItemStack.spec.js @@ -1,6 +1,6 @@ exports['mcfunction argument minecraft:item_stack Parse "minecraft:stick" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_stack", "range": { "start": 0, "end": 15 @@ -22,7 +22,7 @@ exports['mcfunction argument minecraft:item_stack Parse "minecraft:stick" 1'] = exports['mcfunction argument minecraft:item_stack Parse "stick" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_stack", "range": { "start": 0, "end": 5 @@ -43,7 +43,7 @@ exports['mcfunction argument minecraft:item_stack Parse "stick" 1'] = { exports['mcfunction argument minecraft:item_stack Parse "stick{foo:bar}" 1'] = { "node": { - "type": "mcfunction:item", + "type": "mcfunction:item_stack", "range": { "start": 0, "end": 14 diff --git a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftParticle.spec.js b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftParticle.spec.js index 60bf4fc9a..0787b8081 100644 --- a/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftParticle.spec.js +++ b/__snapshots__/packages/java-edition/test-out/mcfunction/parser/argument/minecraftParticle.spec.js @@ -923,7 +923,7 @@ exports['mcfunction argument minecraft:particle Parse "item carrot_on_a_stick" 1 ] }, { - "type": "mcfunction:item", + "type": "mcfunction:item_stack", "range": { "start": 5, "end": 22 diff --git a/packages/core/src/parser/resourceLocation.ts b/packages/core/src/parser/resourceLocation.ts index c74d71d13..7b70d50c9 100644 --- a/packages/core/src/parser/resourceLocation.ts +++ b/packages/core/src/parser/resourceLocation.ts @@ -14,6 +14,7 @@ const Terminators = new Set([ '\r', '\n', '=', + '~', ',', '"', "'", @@ -24,6 +25,7 @@ const Terminators = new Set([ '(', ')', ';', + '|', ]) export const LegalResourceLocationCharacters = new Set([ 'a', diff --git a/packages/java-edition/src/mcfunction/checker/index.ts b/packages/java-edition/src/mcfunction/checker/index.ts index 529dd09a5..c82ee0ad6 100644 --- a/packages/java-edition/src/mcfunction/checker/index.ts +++ b/packages/java-edition/src/mcfunction/checker/index.ts @@ -9,7 +9,7 @@ import type { EntitySelectorInvertableArgumentValueNode } from '../node/index.js import { BlockNode, EntityNode, - ItemNode, + ItemStackNode, JsonNode, NbtNode, NbtResourceNode, @@ -40,8 +40,8 @@ const rootCommand = ( block(node, ctx) } else if (EntityNode.is(node)) { entity(node, ctx) - } else if (ItemNode.is(node)) { - item(node, ctx) + } else if (ItemStackNode.is(node)) { + itemStack(node, ctx) } else if (ParticleNode.is(node)) { particle(node, ctx) } else if (JsonNode.is(node)) { @@ -81,15 +81,44 @@ const entity: core.SyncChecker = (node, ctx) => { } } -const item: core.SyncChecker = (node, ctx) => { - if (!node.nbt) { - return +const itemStack: core.SyncChecker = (node, ctx) => { + if (node.nbt) { + nbt.checker.index( + 'minecraft:item', + core.ResourceLocationNode.toString(node.id, 'full'), + )(node.nbt, ctx) } + if (node.components) { + const groupedComponents = new Map< + string, + core.PairNode[] + >() - nbt.checker.index( - 'minecraft:item', - core.ResourceLocationNode.toString(node.id, 'full'), - )(node.nbt, ctx) + node.components!.children.forEach(component => { + const componentName = core.ResourceLocationNode.toString( + component.key!, + 'full', + ) + + if (!groupedComponents.has(componentName)) { + groupedComponents.set(componentName, []) + } + + groupedComponents.get(componentName)!.push(component) + }) + + groupedComponents.forEach((components) => { + if (components.length > 1) { + components.forEach(component => { + ctx.err.report( + localize('mcfunction.parser.duplicate-components'), + component.key!.range, + core.ErrorSeverity.Warning, + ) + }) + } + }) + } } const jsonChecker: core.SyncChecker = (node, ctx) => { @@ -215,7 +244,7 @@ export function register(meta: core.MetaRegistry) { meta.registerChecker('mcfunction:command', command) meta.registerChecker('mcfunction:block', block) meta.registerChecker('mcfunction:entity', entity) - meta.registerChecker('mcfunction:item', item) + meta.registerChecker('mcfunction:item_stack', itemStack) meta.registerChecker('mcfunction:json', jsonChecker) meta.registerChecker('mcfunction:particle', particle) } diff --git a/packages/java-edition/src/mcfunction/completer/argument.ts b/packages/java-edition/src/mcfunction/completer/argument.ts index b8a634d3a..340f798e2 100644 --- a/packages/java-edition/src/mcfunction/completer/argument.ts +++ b/packages/java-edition/src/mcfunction/completer/argument.ts @@ -27,7 +27,7 @@ import { import * as json from '@spyglassmc/json' import { localeQuote, localize } from '@spyglassmc/locales' import type * as mcf from '@spyglassmc/mcfunction' -import * as nbt from '@spyglassmc/nbt' +import type * as nbt from '@spyglassmc/nbt' import { getTagValues } from '../../common/index.js' import { ReleaseVersion } from '../../dependency/common.js' import { @@ -45,6 +45,9 @@ import { } from '../common/index.js' import type { BlockStatesNode, + ComponentListNode, + ComponentTestsAllOfNode, + ComponentTestsNode, EntitySelectorArgumentsNode, } from '../node/index.js' import { @@ -53,7 +56,8 @@ import { EntitySelectorAtVariable, EntitySelectorNode, IntRangeNode, - ItemNode, + ItemPredicateNode, + ItemStackNode, ObjectiveCriteriaNode, ParticleNode, ScoreHolderNode, @@ -124,7 +128,7 @@ export const getMockNodes: mcf.completer.MockNodesGetter = ( case 'minecraft:item_enchantment': return ResourceLocationNode.mock(range, { category: 'enchantment' }) case 'minecraft:item_predicate': - return ItemNode.mock(range, true) + return ItemPredicateNode.mock(range) case 'minecraft:item_slot': return LiteralNode.mock(range, { pool: getItemSlotArgumentValues(ctx), @@ -134,7 +138,7 @@ export const getMockNodes: mcf.completer.MockNodesGetter = ( pool: getItemSlotsArgumentValues(ctx), }) case 'minecraft:item_stack': - return ItemNode.mock(range, false) + return ItemStackNode.mock(range) case 'minecraft:loot_modifier': return ResourceLocationNode.mock(range, { category: 'item_modifier' }) case 'minecraft:loot_predicate': @@ -280,15 +284,70 @@ const blockStates: Completer = (node, ctx) => { })(node, ctx) } +const componentList: Completer = (node, ctx) => { + return completer.record< + ResourceLocationNode, + nbt.NbtNode, + ComponentListNode + >({ + key: ( + _record, + pair, + ctx, + range, + _insertValue, + _insertComma, + _existingKeys, + ) => { + const id = pair?.key ?? + ResourceLocationNode.mock(pair?.key ?? range, { + category: 'data_component_type', + }) + return completer.resourceLocation(id, ctx) + }, + value: () => { + return [] // TODO + }, + })(node, ctx) +} + +const componentTests: Completer = (node, ctx) => { + // TODO: improve this completer + return [] +} + const coordinate: Completer = (node, _ctx) => { return [CompletionItem.create('~', node)] } -const item: Completer = (node, ctx) => { +const itemStack: Completer = (node, ctx) => { const ans: CompletionItem[] = [] if (Range.contains(node.id, ctx.offset, true)) { ans.push(...completer.resourceLocation(node.id, ctx)) } + if (node.components && Range.contains(node.components, ctx.offset, true)) { + ans.push(...componentList(node.components, ctx)) + } + if (node.nbt && Range.contains(node.nbt, ctx.offset, true)) { + // TODO + } + return ans +} + +const itemPredicate: Completer = (node, ctx) => { + const ans: CompletionItem[] = [] + if (Range.contains(node.id, ctx.offset, true)) { + ans.push(CompletionItem.create('*', node, { sortText: '##' })) + if (node.id.type === 'resource_location') { + ans.push(...completer.resourceLocation(node.id, ctx)) + } + } + if (node.tests && Range.contains(node.tests, ctx.offset, true)) { + ans.push(...componentTests(node.tests, ctx)) + } + if (node.nbt && Range.contains(node.nbt, ctx.offset, true)) { + // TODO + } return ans } @@ -344,7 +403,7 @@ const particle: Completer = (node, ctx) => { VectorNode.mock(ctx.offset, { dimension: 3 }), ], falling_dust: [BlockNode.mock(ctx.offset, false)], - item: [ItemNode.mock(ctx.offset, false)], + item: [ItemStackNode.mock(ctx.offset)], sculk_charge: [FloatNode.mock(ctx.offset)], shriek: [IntegerNode.mock(ctx.offset)], vibration: [ @@ -476,6 +535,14 @@ const vector: Completer = (node, _ctx) => { export function register(meta: MetaRegistry) { meta.registerCompleter('mcfunction:block', block) + meta.registerCompleter( + 'mcfunction:component_list', + componentList, + ) + meta.registerCompleter( + 'mcfunction:component_tests', + componentTests, + ) meta.registerCompleter('mcfunction:coordinate', coordinate) meta.registerCompleter( 'mcfunction:entity_selector', @@ -486,7 +553,11 @@ export function register(meta: MetaRegistry) { selectorArguments, ) meta.registerCompleter('mcfunction:int_range', intRange) - meta.registerCompleter('mcfunction:item', item) + meta.registerCompleter('mcfunction:item_stack', itemStack) + meta.registerCompleter( + 'mcfunction:item_predicate', + itemPredicate, + ) meta.registerCompleter( 'mcfunction:objective_criteria', objectiveCriteria, diff --git a/packages/java-edition/src/mcfunction/node/argument.ts b/packages/java-edition/src/mcfunction/node/argument.ts index ed9ca0092..04372396f 100644 --- a/packages/java-edition/src/mcfunction/node/argument.ts +++ b/packages/java-edition/src/mcfunction/node/argument.ts @@ -293,24 +293,73 @@ export interface FloatRangeNode extends core.AstNode { value: [number | undefined, number | undefined] } -export interface ItemNode extends core.AstNode { - type: 'mcfunction:item' - children: (core.ResourceLocationNode | nbt.NbtCompoundNode)[] +export interface ItemStackNode extends core.AstNode { + type: 'mcfunction:item_stack' + children: + (core.ResourceLocationNode | ComponentListNode | nbt.NbtCompoundNode)[] id: core.ResourceLocationNode - nbt?: nbt.NbtCompoundNode + components?: ComponentListNode // since 1.20.5 + nbt?: nbt.NbtCompoundNode // until 1.20.5 } -export namespace ItemNode { - export function is(node: core.AstNode | undefined): node is ItemNode { - return (node as ItemNode | undefined)?.type === 'mcfunction:item' +export namespace ItemStackNode { + export function is(node: core.AstNode | undefined): node is ItemStackNode { + return (node as ItemStackNode | undefined)?.type === + 'mcfunction:item_stack' + } + + export function mock( + range: core.RangeLike, + ): ItemStackNode { + const id = core.ResourceLocationNode.mock(range, { category: 'item' }) + return { + type: 'mcfunction:item_stack', + range: core.Range.get(range), + children: [id], + id, + } } +} - export function mock(range: core.RangeLike, isPredicate: boolean): ItemNode { +export interface ComponentListNode extends core.AstNode { + type: 'mcfunction:component_list' + children: core.PairNode[] +} + +export namespace ComponentListNode { + export function is(node: core.AstNode): node is ComponentListNode { + return (node as ComponentListNode).type === 'mcfunction:component_list' + } +} + +export interface ItemPredicateNode extends core.AstNode { + type: 'mcfunction:item_predicate' + children: ( + | core.ResourceLocationNode + | core.LiteralNode + | ComponentTestsNode + | nbt.NbtCompoundNode + )[] + id: core.ResourceLocationNode | core.LiteralNode + tests?: ComponentTestsNode // since 1.20.5 + nbt?: nbt.NbtCompoundNode // until 1.20.5 +} +export namespace ItemPredicateNode { + export function is( + node: core.AstNode | undefined, + ): node is ItemPredicateNode { + return (node as ItemPredicateNode | undefined)?.type === + 'mcfunction:item_predicate' + } + + export function mock( + range: core.RangeLike, + ): ItemPredicateNode { const id = core.ResourceLocationNode.mock(range, { category: 'item', - allowTag: isPredicate, + allowTag: true, }) return { - type: 'mcfunction:item', + type: 'mcfunction:item_predicate', range: core.Range.get(range), children: [id], id, @@ -318,6 +367,81 @@ export namespace ItemNode { } } +export interface ComponentTestsNode extends core.AstNode { + type: 'mcfunction:component_tests' + children: ComponentTestsAnyOfNode[] +} + +export namespace ComponentTestsNode { + export function is(node: core.AstNode): node is ComponentTestsNode { + return (node as ComponentTestsNode).type === 'mcfunction:component_tests' + } +} + +export interface ComponentTestsAnyOfNode extends core.AstNode { + type: 'mcfunction:component_tests_any_of' + children: ComponentTestsAllOfNode[] +} + +export namespace ComponentTestsAnyOfNode { + export function is(node: core.AstNode): node is ComponentTestsAnyOfNode { + return (node as ComponentTestsAnyOfNode).type === + 'mcfunction:component_tests_any_of' + } +} + +export interface ComponentTestsAllOfNode extends core.AstNode { + type: 'mcfunction:component_tests_all_of' + children: ComponentTestNode[] +} + +export namespace ComponentTestsAllOfNode { + export function is(node: core.AstNode): node is ComponentTestsAllOfNode { + return (node as ComponentTestsAllOfNode).type === + 'mcfunction:component_tests_all_of' + } +} + +export interface ComponentTestBaseNode extends core.AstNode { + negated: boolean +} + +export interface ComponentTestExactNode extends ComponentTestBaseNode { + type: 'mcfunction:component_test_exact' + children: (core.ResourceLocationNode | nbt.NbtNode)[] + component: core.ResourceLocationNode + value?: nbt.NbtNode +} + +export interface ComponentTestExistsNode extends ComponentTestBaseNode { + type: 'mcfunction:component_test_exists' + children: [core.ResourceLocationNode] + component: core.ResourceLocationNode +} + +export interface ComponentTestSubpredicateNode extends ComponentTestBaseNode { + type: 'mcfunction:component_test_sub_predicate' + children: (core.ResourceLocationNode | nbt.NbtNode)[] + subPredicateType: core.ResourceLocationNode + subPredicate?: nbt.NbtNode +} + +export type ComponentTestNode = + | ComponentTestExactNode + | ComponentTestExistsNode + | ComponentTestSubpredicateNode + +export namespace ComponentTestNode { + export function is(node: core.AstNode): node is ComponentTestNode { + return (node as ComponentTestNode).type === + 'mcfunction:component_test_exact' || + (node as ComponentTestNode).type === + 'mcfunction:component_test_exists' || + (node as ComponentTestNode).type === + 'mcfunction:component_test_sub_predicate' + } +} + export interface IntRangeNode extends core.AstNode { type: 'mcfunction:int_range' children: (core.IntegerNode | core.LiteralNode)[] @@ -423,7 +547,7 @@ export interface ParticleNode extends core.AstNode { | core.FloatNode | core.IntegerNode | BlockNode - | ItemNode + | ItemStackNode | VectorNode // Since 1.20.5 | nbt.NbtCompoundNode diff --git a/packages/java-edition/src/mcfunction/parser/argument.ts b/packages/java-edition/src/mcfunction/parser/argument.ts index c08ce0b75..4fcb15a47 100644 --- a/packages/java-edition/src/mcfunction/parser/argument.ts +++ b/packages/java-edition/src/mcfunction/parser/argument.ts @@ -20,6 +20,11 @@ import { } from '../common/index.js' import type { BlockNode, + ComponentTestExactNode, + ComponentTestExistsNode, + ComponentTestNode, + ComponentTestsAllOfNode, + ComponentTestSubpredicateNode, CoordinateNode, EntityNode, EntitySelectorAdvancementsArgumentCriteriaNode, @@ -29,7 +34,8 @@ import type { EntitySelectorVariable, FloatRangeNode, IntRangeNode, - ItemNode, + ItemPredicateNode, + ItemStackNode, JsonNode, MessageNode, NbtNode, @@ -41,6 +47,9 @@ import type { } from '../node/index.js' import { BlockStatesNode, + ComponentListNode, + ComponentTestsAnyOfNode, + ComponentTestsNode, CoordinateSystem, EntitySelectorArgumentsNode, EntitySelectorAtVariable, @@ -84,6 +93,11 @@ function shouldValidateLength(ctx: core.ParserContext) { return !release || ReleaseVersion.cmp(release, '1.18') < 0 } +function shouldUseOldItemStackFormat(ctx: core.ParserContext) { + const release = ctx.project['loadedVersion'] as ReleaseVersion | undefined + return !release || ReleaseVersion.cmp(release, '1.20.5') < 0 +} + /** * @returns The parser for the specified argument tree node. All argument parsers used in the `mcfunction` package * fail on empty input. @@ -501,31 +515,72 @@ const greedyString: core.InfallibleParser = core.string({ unquotable: { blockList: new Set(['\n', '\r']) }, }) -function item(isPredicate: false): core.InfallibleParser -function item(isPredicate: true): core.InfallibleParser -function item(isPredicate: boolean): core.InfallibleParser { +const itemStack: core.InfallibleParser = (src, ctx) => { + const oldFormat = shouldUseOldItemStackFormat(ctx) return core.map< - core.SequenceUtil, - ItemNode + core.SequenceUtil< + core.ResourceLocationNode | ComponentListNode | nbt.NbtCompoundNode + >, + ItemStackNode >( core.sequence([ - core.resourceLocation({ category: 'item', allowTag: isPredicate }), - core.optional(core.failOnEmpty(nbt.parser.compound)), + core.resourceLocation({ category: 'item' }), + oldFormat + ? core.optional(core.failOnEmpty(nbt.parser.compound)) + : core.optional(core.failOnEmpty(components)), ]), (res) => { - const ans: ItemNode = { - type: 'mcfunction:item', + const ans: ItemStackNode = { + type: 'mcfunction:item_stack', range: res.range, children: res.children, id: res.children.find(core.ResourceLocationNode.is)!, + components: res.children.find(ComponentListNode.is), nbt: res.children.find(nbt.NbtCompoundNode.is), } return ans }, - ) + )(src, ctx) +} + +const itemPredicate: core.InfallibleParser = (src, ctx) => { + const oldFormat = shouldUseOldItemStackFormat(ctx) + return core.map< + core.SequenceUtil< + | core.LiteralNode + | core.ResourceLocationNode + | ComponentTestsNode + | nbt.NbtCompoundNode + >, + ItemPredicateNode + >( + core.sequence([ + oldFormat + ? core.resourceLocation({ category: 'item', allowTag: true }) + : core.any([ + core.resourceLocation({ category: 'item', allowTag: true }), + core.literal('*'), + ]), + oldFormat + ? core.optional(core.failOnEmpty(nbt.parser.compound)) + : core.optional(core.failOnEmpty(componentTests)), + ]), + (res) => { + const ans: ItemPredicateNode = { + type: 'mcfunction:item_predicate', + range: res.range, + children: res.children, + id: (res.children.find(core.ResourceLocationNode.is) || + res.children.find(core.LiteralNode.is))!, + tests: res.children.find( + ComponentTestsNode.is, + ), + nbt: res.children.find(nbt.NbtCompoundNode.is), + } + return ans + }, + )(src, ctx) } -const itemStack: core.InfallibleParser = item(false) -const itemPredicate: core.InfallibleParser = item(true) function jsonParser(typeRef: `::${string}::${string}`): core.Parser { return core.map( @@ -1767,3 +1822,189 @@ function vector( return ans } } + +const components: core.InfallibleParser = core.map( + core.record({ + start: '[', + pair: { + key: core.resourceLocation({ category: 'data_component_type' }), + sep: '=', + value: nbt.parser.entry, + end: ',', + trailingEnd: true, + }, + end: ']', + }), + (res) => { + const ans: ComponentListNode = { + type: 'mcfunction:component_list', + range: res.range, + children: res.children, + } + return ans + }, +) + +const componentTest: core.InfallibleParser = (src, ctx) => { + const start = src.cursor + src.skipWhitespace() + const negated = src.trySkip('!') + src.skipWhitespace() + + const resLoc = core.resourceLocation({ category: 'data_component_type' })( + src, + ctx, + ) + src.skipWhitespace() + + if (src.trySkip('=')) { + let value: core.Result | undefined = nbt.parser.entry( + src, + ctx, + ) + + if (value == core.Failure) { + ctx.err.report(localize('expected', localize('nbt.node')), src) + src.skipUntilOrEnd(',', '|', ']') + value = undefined + } + + src.skipWhitespace() + const ans: ComponentTestExactNode = { + type: 'mcfunction:component_test_exact', + range: core.Range.create(start, src), + children: [resLoc, ...(value ? [value] : [])], + component: resLoc, + value: value, + negated, + } + return ans + } + if (src.trySkip('~')) { + resLoc.options.category = 'item_sub_predicate_type' + let predicate: core.Result | undefined = nbt.parser.entry( + src, + ctx, + ) + + if (predicate == core.Failure) { + ctx.err.report(localize('expected', localize('nbt.node')), src) + src.skipUntilOrEnd(',', '|', ']') + predicate = undefined + } + + src.skipWhitespace() + const ans: ComponentTestSubpredicateNode = { + type: 'mcfunction:component_test_sub_predicate', + range: core.Range.create(start, src), + children: [resLoc, ...(predicate ? [predicate] : [])], + subPredicateType: resLoc, + subPredicate: predicate, + negated, + } + return ans + } + const ans: ComponentTestExistsNode = { + type: 'mcfunction:component_test_exists', + range: core.Range.create(start, src), + children: [resLoc], + component: resLoc, + negated, + } + return ans +} + +const componentTestsAllOf: core.InfallibleParser = ( + src, + ctx, +) => { + const parser: core.InfallibleParser = ( + src, + ctx, + ) => { + const children = [] + const start = src.cursor + + while (src.canRead()) { + src.skipWhitespace() + const testNode = componentTest(src, ctx) + + children.push(testNode) + src.skipWhitespace() + + if (src.peek() === ',') { + src.skip() + } else if (src.peek() === '|' || src.peek() === ']') { + break + } else { + ctx.err.report(localize('expected', localeQuote(']')), src) + src.skipUntilOrEnd(',', '|', ']') + } + } + + const ans: ComponentTestsAllOfNode = { + type: 'mcfunction:component_tests_all_of', + range: core.Range.create(start, src), + children, + } + + return ans + } + + return parser(src, ctx) +} + +const componentTestsAnyOf: core.InfallibleParser = ( + src, + ctx, +) => { + const parser: core.InfallibleParser = ( + src, + ctx, + ) => { + const children = [] + const start = src.cursor + + while (src.canRead()) { + src.skipWhitespace() + const allOfNode = componentTestsAllOf(src, ctx) + children.push(allOfNode) + src.skipWhitespace() + + if (src.peek() === '|') { + src.skip() + } else if (src.peek() === ']') { + break + } else { + ctx.err.report(localize('expected', localeQuote(']')), src) + src.skipUntilOrEnd('|', ']') + } + } + + const ans: ComponentTestsAnyOfNode = { + type: 'mcfunction:component_tests_any_of', + range: core.Range.create(start, src), + children, + } + + return ans + } + + return parser(src, ctx) +} + +const componentTests: core.InfallibleParser = core.map( + core.sequence([ + core.literal('['), + core.optional(core.failOnEmpty(componentTestsAnyOf)), + core.literal(']'), + ]), + (res) => { + const ans: ComponentTestsNode = { + type: 'mcfunction:component_tests', + range: res.range, + children: res.children.filter(ComponentTestsAnyOfNode.is).map(c => c), + } + return ans + }, +) diff --git a/packages/locales/src/locales/en.json b/packages/locales/src/locales/en.json index 93a9956ed..8e97d7d66 100644 --- a/packages/locales/src/locales/en.json +++ b/packages/locales/src/locales/en.json @@ -116,6 +116,7 @@ "mcdoc.type.struct": "a map-like", "mcfunction.checker.command.data-modify-unapplicable-operation": "Operation %0% can only be used on %1%; the target path has type %2% instead", "mcfunction.completer.block.states.default-value": "Default: %0%", + "mcfunction.parser.duplicate-components": "Duplicate component", "mcfunction.parser.entity-selector.arguments.not-applicable": "%0% is not applicable here", "mcfunction.parser.entity-selector.arguments.unknown": "Unknown entity selector argument %0%", "mcfunction.parser.entity-selector.entities-disallowed": "The selector contains non-player entities",