Skip to main content

steel_core/command/execution/
item_predicate.rs

1//! Vanilla item-predicate command argument parsing and matching.
2
3use std::cmp::Ordering;
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use steel_registry::{
7    REGISTRY, RegistryExt as _, TaggedRegistryExt as _,
8    attribute::{AttributeModifierOperation, AttributeRef},
9    data_component_predicate as registered,
10    data_component_predicate::DataComponentPredicateCodec as _,
11    data_components::{
12        Component, ComponentData, ComponentEntry, DataComponentType, PotionContents,
13        vanilla_components,
14    },
15    enchantment::EnchantmentRef,
16    equipment::EquipmentSlotGroup,
17    item_predicate::{DoubleBounds, IntBounds, ItemPredicate as RegisteredItemPredicate},
18    item_stack::ItemStack,
19    items::ItemRef,
20};
21use steel_utils::DowncastType;
22use steel_utils::{Identifier, nbt::parse_snbt_argument};
23use text_components::TextComponent;
24
25use crate::command::brigadier::{
26    CommandSyntaxError, CommandSyntaxErrorKind, ReaderCursor, StringReader, SuggestionsBuilder,
27};
28
29use super::{
30    argument::{matches_substring, parse_identifier},
31    item::{component_value_is_valid, numeric_i32, read_component_value},
32};
33
34const VANILLA_DATA_COMPONENT_PREDICATE_KEYS: &[&str] = &[
35    "damage",
36    "enchantments",
37    "stored_enchantments",
38    "potion_contents",
39    "custom_data",
40    "container",
41    "bundle_contents",
42    "firework_explosion",
43    "fireworks",
44    "writable_book_content",
45    "written_book_content",
46    "attribute_modifiers",
47    "trim",
48    "jukebox_playable",
49    "villager/variant",
50];
51
52/// A fully decoded item predicate retained until command execution.
53#[derive(Clone, Debug, PartialEq)]
54pub(crate) struct ItemPredicate {
55    target: ItemPredicateTarget,
56    conditions: Vec<ItemPredicateCondition>,
57}
58
59#[derive(Clone, Debug, PartialEq)]
60enum ItemPredicateTarget {
61    Any,
62    Item(ItemRef),
63    Tag(Identifier),
64}
65
66#[derive(Clone, Debug, PartialEq)]
67struct ItemPredicateCondition {
68    alternatives: Vec<ItemPredicateTerm>,
69}
70
71#[derive(Clone, Debug, PartialEq)]
72enum ItemPredicateTerm {
73    Always,
74    ComponentPresence(Identifier),
75    ComponentValue {
76        key: Identifier,
77        value: Box<ComponentData>,
78    },
79    Count(IntRange),
80    DataPredicate(DataComponentPredicate),
81    Not(Box<Self>),
82}
83
84#[derive(Clone, Debug, PartialEq)]
85enum DataComponentPredicate {
86    CustomData(vanilla_components::CustomData),
87    Damage(DamagePredicate),
88    Enchantments {
89        stored: bool,
90        predicates: Vec<EnchantmentPredicate>,
91    },
92    AttributeModifiers(AttributeModifiersPredicate),
93    Potions(registered::PotionsPredicate),
94    Container(registered::ContainerPredicate),
95    Bundle(registered::BundlePredicate),
96    FireworkExplosion(registered::FireworkExplosionPredicate),
97    Fireworks(registered::FireworksPredicate),
98    WritableBook(registered::WritableBookPredicate),
99    WrittenBook(registered::WrittenBookPredicate),
100    Trim(registered::TrimPredicate),
101    JukeboxPlayable(registered::JukeboxPlayablePredicate),
102    VillagerType(registered::VillagerTypePredicate),
103}
104
105trait ItemInstanceView {
106    fn item_ref(&self) -> ItemRef;
107    fn item_count(&self) -> i32;
108    fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData>;
109
110    fn component<T: Component + DowncastType>(
111        &self,
112        component: DataComponentType<T>,
113    ) -> Option<&T> {
114        self.effective_value_raw(component.key())
115            .and_then(ComponentData::downcast_ref::<T>)
116    }
117}
118
119impl ItemInstanceView for ItemStack {
120    fn item_ref(&self) -> ItemRef {
121        self.item()
122    }
123
124    fn item_count(&self) -> i32 {
125        self.count()
126    }
127
128    fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
129        self.get_effective_value_raw(key)
130    }
131}
132
133impl ItemInstanceView for steel_registry::ItemStackTemplate {
134    fn item_ref(&self) -> ItemRef {
135        self.item()
136    }
137
138    fn item_count(&self) -> i32 {
139        self.count()
140    }
141
142    fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
143        self.get_effective_value_raw(key)
144    }
145}
146
147impl ItemPredicate {
148    #[must_use]
149    pub(crate) fn matches(&self, stack: &ItemStack) -> bool {
150        self.target.matches(stack)
151            && self
152                .conditions
153                .iter()
154                .all(|condition| condition.matches(stack))
155    }
156}
157
158impl ItemPredicateTarget {
159    fn matches(&self, stack: &ItemStack) -> bool {
160        match self {
161            Self::Any => true,
162            Self::Item(item) => stack.is(item),
163            Self::Tag(tag) => REGISTRY.items.is_in_tag(stack.item(), tag),
164        }
165    }
166}
167
168impl ItemPredicateCondition {
169    fn matches(&self, stack: &ItemStack) -> bool {
170        self.alternatives
171            .iter()
172            .any(|alternative| alternative.matches(stack))
173    }
174}
175
176impl ItemPredicateTerm {
177    fn matches(&self, stack: &ItemStack) -> bool {
178        match self {
179            Self::Always => true,
180            Self::ComponentPresence(key) => stack.has_component(key),
181            Self::ComponentValue { key, value } => stack
182                .get_effective_value_raw(key)
183                .is_some_and(|actual| actual == value.as_ref()),
184            Self::Count(range) => range.matches(stack.count()),
185            Self::DataPredicate(predicate) => predicate.matches(stack),
186            Self::Not(term) => !term.matches(stack),
187        }
188    }
189}
190
191impl DataComponentPredicate {
192    #[expect(
193        clippy::too_many_lines,
194        reason = "keeping every registered predicate variant in one match makes coverage auditable"
195    )]
196    fn matches<S: ItemInstanceView + ?Sized>(&self, stack: &S) -> bool {
197        match self {
198            Self::CustomData(expected) => stack
199                .component(vanilla_components::CUSTOM_DATA)
200                .cloned()
201                .unwrap_or_default()
202                .matched_by(expected.as_compound()),
203            Self::Damage(predicate) => predicate.matches(stack),
204            Self::Enchantments { stored, predicates } => {
205                let enchantments = if *stored {
206                    stack.component(vanilla_components::STORED_ENCHANTMENTS)
207                } else {
208                    stack.component(vanilla_components::ENCHANTMENTS)
209                };
210                enchantments.is_some_and(|enchantments| {
211                    predicates
212                        .iter()
213                        .all(|predicate| predicate.matches(enchantments))
214                })
215            }
216            Self::AttributeModifiers(predicate) => stack
217                .component(vanilla_components::ATTRIBUTE_MODIFIERS)
218                .is_some_and(|modifiers| predicate.matches(modifiers)),
219            Self::Potions(predicate) => stack
220                .component(vanilla_components::POTION_CONTENTS)
221                .and_then(PotionContents::potion)
222                .is_some_and(|potion| predicate.potions().contains(potion.value())),
223            Self::Container(predicate) => stack
224                .component(vanilla_components::CONTAINER)
225                .is_some_and(|contents| {
226                    predicate.items().is_none_or(|items| {
227                        collection_matches(
228                            items,
229                            contents.items().iter().filter_map(Option::as_ref),
230                            registered_item_predicate_matches,
231                        )
232                    })
233                }),
234            Self::Bundle(predicate) => stack
235                .component(vanilla_components::BUNDLE_CONTENTS)
236                .is_some_and(|contents| {
237                    predicate.items().is_none_or(|items| {
238                        collection_matches(
239                            items,
240                            contents.items().iter(),
241                            registered_item_predicate_matches,
242                        )
243                    })
244                }),
245            Self::FireworkExplosion(predicate) => stack
246                .component(vanilla_components::FIREWORK_EXPLOSION)
247                .is_some_and(|explosion| firework_matches(predicate.predicate(), explosion)),
248            Self::Fireworks(predicate) => stack
249                .component(vanilla_components::FIREWORKS)
250                .is_some_and(|fireworks| {
251                    int_bounds_matches(predicate.flight_duration(), fireworks.flight_duration())
252                        && predicate.explosions().is_none_or(|explosions| {
253                            collection_matches(
254                                explosions,
255                                fireworks.explosions().iter(),
256                                firework_matches,
257                            )
258                        })
259                }),
260            Self::WritableBook(predicate) => stack
261                .component(vanilla_components::WRITABLE_BOOK_CONTENT)
262                .is_some_and(|book| {
263                    predicate.pages().is_none_or(|pages| {
264                        collection_matches(pages, book.pages().iter(), |predicate, page| {
265                            predicate.contents() == page.raw()
266                        })
267                    })
268                }),
269            Self::WrittenBook(predicate) => stack
270                .component(vanilla_components::WRITTEN_BOOK_CONTENT)
271                .is_some_and(|book| {
272                    predicate
273                        .author()
274                        .is_none_or(|author| author == book.author())
275                        && predicate
276                            .title()
277                            .is_none_or(|title| title == book.title().raw())
278                        && int_bounds_matches(predicate.generation(), book.generation())
279                        && predicate
280                            .resolved()
281                            .is_none_or(|resolved| resolved == book.resolved())
282                        && predicate.pages().is_none_or(|pages| {
283                            collection_matches(pages, book.pages().iter(), |predicate, page| {
284                                predicate.contents() == page.raw()
285                            })
286                        })
287                }),
288            Self::Trim(predicate) => {
289                stack
290                    .component(vanilla_components::TRIM)
291                    .is_some_and(|trim| {
292                        predicate.material().is_none_or(|materials| {
293                            trim.material()
294                                .as_reference()
295                                .is_some_and(|material| materials.contains(material))
296                        }) && predicate.pattern().is_none_or(|patterns| {
297                            trim.pattern()
298                                .as_reference()
299                                .is_some_and(|pattern| patterns.contains(pattern))
300                        })
301                    })
302            }
303            Self::JukeboxPlayable(predicate) => stack
304                .component(vanilla_components::JUKEBOX_PLAYABLE)
305                .is_some_and(|playable| {
306                    predicate.song().is_none_or(|songs| {
307                        playable
308                            .song()
309                            .as_reference()
310                            .is_some_and(|song| songs.contains(song))
311                    })
312                }),
313            Self::VillagerType(predicate) => stack
314                .component(vanilla_components::VILLAGER_VARIANT)
315                .is_some_and(|villager_type| {
316                    predicate.villager_types().contains(villager_type.value())
317                }),
318        }
319    }
320}
321
322fn int_bounds_matches(bounds: IntBounds, value: i32) -> bool {
323    bounds.min().is_none_or(|minimum| value >= minimum)
324        && bounds.max().is_none_or(|maximum| value <= maximum)
325}
326
327fn double_bounds_matches(bounds: DoubleBounds, value: f64) -> bool {
328    bounds.min().is_none_or(|minimum| value >= minimum)
329        && bounds.max().is_none_or(|maximum| value <= maximum)
330}
331
332fn collection_matches<'a, P, T: 'a>(
333    predicate: &registered::CollectionPredicate<P>,
334    values: impl IntoIterator<Item = &'a T>,
335    matches: impl Fn(&P, &T) -> bool + Copy,
336) -> bool {
337    let values = values.into_iter().collect::<Vec<_>>();
338    predicate.contains().is_none_or(|predicates| {
339        predicates
340            .iter()
341            .all(|predicate| values.iter().any(|value| matches(predicate, value)))
342    }) && predicate.counts().is_none_or(|predicates| {
343        predicates.iter().all(|predicate| {
344            let count = values
345                .iter()
346                .filter(|value| matches(predicate.test(), value))
347                .count();
348            i32::try_from(count).is_ok_and(|count| int_bounds_matches(predicate.count(), count))
349        })
350    }) && predicate.size().is_none_or(|size| {
351        i32::try_from(values.len()).is_ok_and(|length| int_bounds_matches(*size, length))
352    })
353}
354
355fn registered_item_predicate_matches(
356    predicate: &RegisteredItemPredicate,
357    template: &steel_registry::ItemStackTemplate,
358) -> bool {
359    predicate
360        .items()
361        .is_none_or(|items| items.contains(template.item_ref()))
362        && int_bounds_matches(predicate.count(), template.item_count())
363        && predicate
364            .components()
365            .exact()
366            .values()
367            .iter()
368            .all(|(entry, expected)| template.effective_value_raw(&entry.key) == Some(expected))
369        && predicate
370            .components()
371            .partial()
372            .iter()
373            .all(|predicate| registered_partial_matches(predicate, template))
374}
375
376fn registered_partial_matches<S: ItemInstanceView + ?Sized>(
377    predicate: &registered::DataComponentPredicateData,
378    stack: &S,
379) -> bool {
380    if let Some(component) = predicate.any_component() {
381        return stack.effective_value_raw(&component.key).is_some();
382    }
383    macro_rules! match_registered {
384        ($type:ty, $variant:ident) => {
385            if let Some(value) = predicate.downcast_ref::<$type>() {
386                return DataComponentPredicate::$variant(value.clone()).matches(stack);
387            }
388        };
389    }
390    match_registered!(registered::PotionsPredicate, Potions);
391    match_registered!(registered::ContainerPredicate, Container);
392    match_registered!(registered::BundlePredicate, Bundle);
393    match_registered!(registered::FireworkExplosionPredicate, FireworkExplosion);
394    match_registered!(registered::FireworksPredicate, Fireworks);
395    match_registered!(registered::WritableBookPredicate, WritableBook);
396    match_registered!(registered::WrittenBookPredicate, WrittenBook);
397    match_registered!(registered::TrimPredicate, Trim);
398    match_registered!(registered::JukeboxPlayablePredicate, JukeboxPlayable);
399    match_registered!(registered::VillagerTypePredicate, VillagerType);
400
401    if let Some(value) = predicate.downcast_ref::<registered::CustomDataPredicate>() {
402        return stack
403            .component(vanilla_components::CUSTOM_DATA)
404            .cloned()
405            .unwrap_or_default()
406            .matched_by(value.value().tag());
407    }
408    if let Some(value) = predicate.downcast_ref::<registered::DamagePredicate>() {
409        let Some(damage) = stack.component(vanilla_components::DAMAGE).copied() else {
410            return false;
411        };
412        let maximum = stack
413            .component(vanilla_components::MAX_DAMAGE)
414            .copied()
415            .unwrap_or(0);
416        return int_bounds_matches(value.durability(), maximum - damage)
417            && int_bounds_matches(value.damage(), damage);
418    }
419    if let Some(value) = predicate.downcast_ref::<registered::EnchantmentsPredicate>() {
420        return stack
421            .component(vanilla_components::ENCHANTMENTS)
422            .is_some_and(|enchantments| {
423                value
424                    .enchantments()
425                    .iter()
426                    .all(|predicate| registered_enchantment_matches(predicate, enchantments))
427            });
428    }
429    if let Some(value) = predicate.downcast_ref::<registered::StoredEnchantmentsPredicate>() {
430        return stack
431            .component(vanilla_components::STORED_ENCHANTMENTS)
432            .is_some_and(|enchantments| {
433                value
434                    .enchantments()
435                    .iter()
436                    .all(|predicate| registered_enchantment_matches(predicate, enchantments))
437            });
438    }
439    if let Some(value) = predicate.downcast_ref::<registered::AttributeModifiersPredicate>() {
440        return stack
441            .component(vanilla_components::ATTRIBUTE_MODIFIERS)
442            .is_some_and(|modifiers| {
443                value.modifiers().is_none_or(|predicate| {
444                    collection_matches(
445                        predicate,
446                        modifiers.modifiers.iter(),
447                        registered_attribute_modifier_matches,
448                    )
449                })
450            });
451    }
452    false
453}
454
455fn registered_enchantment_matches(
456    predicate: &registered::EnchantmentPredicate,
457    enchantments: &vanilla_components::ItemEnchantments,
458) -> bool {
459    enchantments.iter().any(|(key, level)| {
460        predicate.enchantments().is_none_or(|expected| {
461            REGISTRY
462                .enchantments
463                .by_key(key)
464                .is_some_and(|enchantment| expected.contains(enchantment))
465        }) && predicate
466            .levels()
467            .min()
468            .is_none_or(|minimum| i64::from(*level) >= i64::from(minimum))
469            && predicate
470                .levels()
471                .max()
472                .is_none_or(|maximum| i64::from(*level) <= i64::from(maximum))
473    })
474}
475
476fn registered_attribute_modifier_matches(
477    predicate: &registered::AttributeModifierEntryPredicate,
478    modifier: &vanilla_components::ItemAttributeModifierEntry,
479) -> bool {
480    predicate
481        .attribute()
482        .is_none_or(|attributes| attributes.contains(modifier.attribute))
483        && predicate.id().is_none_or(|id| id == &modifier.id)
484        && double_bounds_matches(predicate.amount(), modifier.amount)
485        && predicate
486            .operation()
487            .is_none_or(|operation| operation == modifier.operation)
488        && predicate.slot().is_none_or(|slot| slot == modifier.slot)
489}
490
491fn firework_matches(
492    predicate: &registered::FireworkPredicate,
493    explosion: &vanilla_components::FireworkExplosion,
494) -> bool {
495    predicate
496        .shape()
497        .is_none_or(|shape| shape == explosion.shape())
498        && predicate
499            .has_twinkle()
500            .is_none_or(|twinkle| twinkle == explosion.has_twinkle())
501        && predicate
502            .has_trail()
503            .is_none_or(|trail| trail == explosion.has_trail())
504}
505
506pub(super) fn parse_item_predicate(
507    reader: &mut StringReader<'_>,
508) -> Result<ItemPredicate, CommandSyntaxError> {
509    let start = reader.checkpoint();
510    let result = parse_item_predicate_inner(reader);
511    if result.is_err() {
512        reader.restore(start);
513    }
514    result
515}
516
517fn parse_item_predicate_inner(
518    reader: &mut StringReader<'_>,
519) -> Result<ItemPredicate, CommandSyntaxError> {
520    let target = parse_target(reader)?;
521    let before_whitespace = reader.checkpoint();
522    reader.skip_whitespace();
523    if reader.peek() != Some('[') {
524        reader.restore(before_whitespace);
525        return Ok(ItemPredicate {
526            target,
527            conditions: Vec::new(),
528        });
529    }
530
531    reader.skip();
532    reader.skip_whitespace();
533    if reader.peek() == Some(']') {
534        reader.skip();
535        return Ok(ItemPredicate {
536            target,
537            conditions: Vec::new(),
538        });
539    }
540
541    let mut conditions = Vec::new();
542    loop {
543        conditions.push(parse_condition(reader)?);
544        reader.skip_whitespace();
545        if reader.peek() != Some(',') {
546            reader.expect(']')?;
547            break;
548        }
549        reader.skip();
550    }
551
552    Ok(ItemPredicate { target, conditions })
553}
554
555fn parse_target(reader: &mut StringReader<'_>) -> Result<ItemPredicateTarget, CommandSyntaxError> {
556    reader.skip_whitespace();
557    if reader.peek() == Some('*') {
558        reader.skip();
559        return Ok(ItemPredicateTarget::Any);
560    }
561
562    if reader.peek() == Some('#') {
563        let start = reader.checkpoint();
564        reader.skip();
565        let key = parse_identifier(reader)?;
566        if REGISTRY.items.get_tag(&key).is_some() {
567            return Ok(ItemPredicateTarget::Tag(key));
568        }
569        reader.restore(start);
570        return Err(dynamic_error(reader, format!("Unknown item tag '#{key}'")));
571    }
572
573    let start = reader.checkpoint();
574    let key = parse_identifier(reader)?;
575    let Some(item) = REGISTRY.items.by_key(&key) else {
576        reader.restore(start);
577        return Err(dynamic_error(reader, format!("Unknown item '{key}'")));
578    };
579    Ok(ItemPredicateTarget::Item(item))
580}
581
582fn parse_condition(
583    reader: &mut StringReader<'_>,
584) -> Result<ItemPredicateCondition, CommandSyntaxError> {
585    let mut alternatives = Vec::new();
586    loop {
587        alternatives.push(parse_term(reader)?);
588        reader.skip_whitespace();
589        if reader.peek() != Some('|') {
590            break;
591        }
592        reader.skip();
593    }
594    Ok(ItemPredicateCondition { alternatives })
595}
596
597fn parse_term(reader: &mut StringReader<'_>) -> Result<ItemPredicateTerm, CommandSyntaxError> {
598    reader.skip_whitespace();
599    if reader.peek() == Some('!') {
600        reader.skip();
601        return parse_test(reader).map(|term| ItemPredicateTerm::Not(Box::new(term)));
602    }
603    parse_test(reader)
604}
605
606fn parse_test(reader: &mut StringReader<'_>) -> Result<ItemPredicateTerm, CommandSyntaxError> {
607    reader.skip_whitespace();
608    let key_start = reader.checkpoint();
609    let key = parse_identifier(reader)?;
610    reader.skip_whitespace();
611
612    match reader.peek() {
613        Some('=') => {
614            reader.skip();
615            parse_component_value_test(reader, key_start, key)
616        }
617        Some('~') => {
618            reader.skip();
619            parse_predicate_value_test(reader, key_start, key)
620        }
621        _ => parse_component_presence_test(reader, key_start, key),
622    }
623}
624
625fn parse_component_presence_test(
626    reader: &mut StringReader<'_>,
627    key_start: ReaderCursor,
628    key: Identifier,
629) -> Result<ItemPredicateTerm, CommandSyntaxError> {
630    if is_count_key(&key) {
631        return Ok(ItemPredicateTerm::Always);
632    }
633    if persistent_component(&key).is_some() {
634        return Ok(ItemPredicateTerm::ComponentPresence(key));
635    }
636    reader.restore(key_start);
637    Err(dynamic_error(
638        reader,
639        format!("Unknown item component '{key}'"),
640    ))
641}
642
643fn parse_component_value_test(
644    reader: &mut StringReader<'_>,
645    key_start: ReaderCursor,
646    key: Identifier,
647) -> Result<ItemPredicateTerm, CommandSyntaxError> {
648    let tag = read_nbt(reader, "component", &key)?;
649    if is_count_key(&key) {
650        return parse_int_range(&tag)
651            .map(ItemPredicateTerm::Count)
652            .ok_or_else(|| malformed_component(reader, &key));
653    }
654
655    let Some(entry) = persistent_component(&key) else {
656        reader.restore(key_start);
657        return Err(dynamic_error(
658            reader,
659            format!("Unknown item component '{key}'"),
660        ));
661    };
662    let Some(value) = read_component_value(entry, &tag) else {
663        return Err(malformed_component(reader, &key));
664    };
665    if !component_value_is_valid(&key, &value) {
666        return Err(malformed_component(reader, &key));
667    }
668    Ok(ItemPredicateTerm::ComponentValue {
669        key,
670        value: Box::new(value),
671    })
672}
673
674fn parse_predicate_value_test(
675    reader: &mut StringReader<'_>,
676    key_start: ReaderCursor,
677    key: Identifier,
678) -> Result<ItemPredicateTerm, CommandSyntaxError> {
679    let tag = read_nbt(reader, "predicate", &key)?;
680    if is_count_key(&key) {
681        return parse_int_range(&tag)
682            .map(ItemPredicateTerm::Count)
683            .ok_or_else(|| malformed_predicate(reader, &key));
684    }
685
686    if is_vanilla_predicate_key(&key) {
687        let predicate = parse_supported_data_predicate(&key, &tag)
688            .ok_or_else(|| malformed_predicate(reader, &key))?;
689        return Ok(ItemPredicateTerm::DataPredicate(predicate));
690    }
691
692    if REGISTRY.data_components.by_key(&key).is_some() {
693        if !matches!(tag, NbtTag::Compound(_)) {
694            return Err(malformed_predicate(reader, &key));
695        }
696        return Ok(ItemPredicateTerm::ComponentPresence(key));
697    }
698
699    reader.restore(key_start);
700    Err(dynamic_error(
701        reader,
702        format!("Unknown item predicate '{key}'"),
703    ))
704}
705
706fn read_nbt(
707    reader: &mut StringReader<'_>,
708    description: &str,
709    key: &Identifier,
710) -> Result<NbtTag, CommandSyntaxError> {
711    reader.skip_whitespace();
712    let (tag, consumed) = parse_snbt_argument(reader.remaining()).map_err(|error| {
713        reader.advance_bytes(error.cursor());
714        dynamic_error(reader, error.component())
715    })?;
716    if !reader.advance_bytes(consumed) {
717        return Err(dynamic_error(
718            reader,
719            format!("Malformed item {description} '{key}'"),
720        ));
721    }
722    Ok(tag)
723}
724
725fn persistent_component(key: &Identifier) -> Option<&'static ComponentEntry> {
726    let entry = REGISTRY.data_components.by_key(key)?;
727    entry.is_persistent().then_some(entry)
728}
729
730fn malformed_component(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
731    dynamic_error(reader, format!("Malformed item component '{key}'"))
732}
733
734fn malformed_predicate(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
735    dynamic_error(reader, format!("Malformed item predicate '{key}'"))
736}
737
738fn dynamic_error(
739    reader: &StringReader<'_>,
740    message: impl Into<TextComponent>,
741) -> CommandSyntaxError {
742    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
743}
744
745fn is_count_key(key: &Identifier) -> bool {
746    key.namespace == Identifier::VANILLA_NAMESPACE && key.path == "count"
747}
748
749fn is_vanilla_predicate_key(key: &Identifier) -> bool {
750    key.namespace == Identifier::VANILLA_NAMESPACE
751        && VANILLA_DATA_COMPONENT_PREDICATE_KEYS
752            .iter()
753            .any(|path| key.path == *path)
754}
755
756fn parse_supported_data_predicate(
757    key: &Identifier,
758    tag: &NbtTag,
759) -> Option<DataComponentPredicate> {
760    match key.path.as_ref() {
761        "custom_data" => vanilla_components::CustomData::from_nbt_value(tag)
762            .map(DataComponentPredicate::CustomData),
763        "damage" => parse_damage_predicate(tag).map(DataComponentPredicate::Damage),
764        "enchantments" => parse_enchantment_predicates(tag).map(|predicates| {
765            DataComponentPredicate::Enchantments {
766                stored: false,
767                predicates,
768            }
769        }),
770        "stored_enchantments" => parse_enchantment_predicates(tag).map(|predicates| {
771            DataComponentPredicate::Enchantments {
772                stored: true,
773                predicates,
774            }
775        }),
776        "attribute_modifiers" => {
777            parse_attribute_modifiers_predicate(tag).map(DataComponentPredicate::AttributeModifiers)
778        }
779        "potion_contents" => {
780            registered::PotionsPredicate::from_nbt_value(tag).map(DataComponentPredicate::Potions)
781        }
782        "container" => registered::ContainerPredicate::from_nbt_value(tag)
783            .map(DataComponentPredicate::Container),
784        "bundle_contents" => {
785            registered::BundlePredicate::from_nbt_value(tag).map(DataComponentPredicate::Bundle)
786        }
787        "firework_explosion" => registered::FireworkExplosionPredicate::from_nbt_value(tag)
788            .map(DataComponentPredicate::FireworkExplosion),
789        "fireworks" => registered::FireworksPredicate::from_nbt_value(tag)
790            .map(DataComponentPredicate::Fireworks),
791        "writable_book_content" => registered::WritableBookPredicate::from_nbt_value(tag)
792            .map(DataComponentPredicate::WritableBook),
793        "written_book_content" => registered::WrittenBookPredicate::from_nbt_value(tag)
794            .map(DataComponentPredicate::WrittenBook),
795        "trim" => registered::TrimPredicate::from_nbt_value(tag).map(DataComponentPredicate::Trim),
796        "jukebox_playable" => registered::JukeboxPlayablePredicate::from_nbt_value(tag)
797            .map(DataComponentPredicate::JukeboxPlayable),
798        "villager/variant" => registered::VillagerTypePredicate::from_nbt_value(tag)
799            .map(DataComponentPredicate::VillagerType),
800        _ => None,
801    }
802}
803
804#[derive(Clone, Copy, Debug, PartialEq)]
805struct IntRange {
806    min: Option<i32>,
807    max: Option<i32>,
808}
809
810impl IntRange {
811    const ANY: Self = Self {
812        min: None,
813        max: None,
814    };
815
816    fn matches(self, value: i32) -> bool {
817        self.min.is_none_or(|minimum| value >= minimum)
818            && self.max.is_none_or(|maximum| value <= maximum)
819    }
820
821    fn matches_u32(self, value: u32) -> bool {
822        let value = i64::from(value);
823        self.min.is_none_or(|minimum| value >= i64::from(minimum))
824            && self.max.is_none_or(|maximum| value <= i64::from(maximum))
825    }
826
827    fn matches_usize(self, value: usize) -> bool {
828        let minimum_matches = self.min.is_none_or(|minimum| {
829            minimum <= 0 || usize::try_from(minimum).is_ok_and(|minimum| value >= minimum)
830        });
831        let maximum_matches = self
832            .max
833            .is_none_or(|maximum| usize::try_from(maximum).is_ok_and(|maximum| value <= maximum));
834        minimum_matches && maximum_matches
835    }
836
837    const fn is_any(self) -> bool {
838        self.min.is_none() && self.max.is_none()
839    }
840}
841
842fn parse_int_range(tag: &NbtTag) -> Option<IntRange> {
843    if let Some(value) = numeric_i32(tag) {
844        return Some(IntRange {
845            min: Some(value),
846            max: Some(value),
847        });
848    }
849    let NbtTag::Compound(compound) = tag else {
850        return None;
851    };
852    let min = parse_optional_field(compound, "min", numeric_i32).ok()?;
853    let max = parse_optional_field(compound, "max", numeric_i32).ok()?;
854    if min.zip(max).is_some_and(|(min, max)| min > max) {
855        return None;
856    }
857    Some(IntRange { min, max })
858}
859
860#[derive(Clone, Copy, Debug, PartialEq)]
861struct DamagePredicate {
862    durability: IntRange,
863    damage: IntRange,
864}
865
866impl DamagePredicate {
867    fn matches<S: ItemInstanceView + ?Sized>(self, stack: &S) -> bool {
868        let Some(damage) = stack.component(vanilla_components::DAMAGE).copied() else {
869            return false;
870        };
871        let maximum = stack
872            .component(vanilla_components::MAX_DAMAGE)
873            .copied()
874            .unwrap_or(0);
875        self.durability.matches(maximum - damage) && self.damage.matches(damage)
876    }
877}
878
879fn parse_damage_predicate(tag: &NbtTag) -> Option<DamagePredicate> {
880    let NbtTag::Compound(compound) = tag else {
881        return None;
882    };
883    let durability = parse_optional_field(compound, "durability", parse_int_range)
884        .ok()?
885        .unwrap_or(IntRange::ANY);
886    let damage = parse_optional_field(compound, "damage", parse_int_range)
887        .ok()?
888        .unwrap_or(IntRange::ANY);
889    Some(DamagePredicate { durability, damage })
890}
891
892#[derive(Clone, Debug, PartialEq)]
893struct EnchantmentPredicate {
894    enchantments: Option<Vec<EnchantmentRef>>,
895    levels: IntRange,
896}
897
898impl EnchantmentPredicate {
899    fn matches(&self, enchantments: &vanilla_components::ItemEnchantments) -> bool {
900        if let Some(expected) = &self.enchantments {
901            return expected.iter().any(|enchantment| {
902                let level = enchantments.get_level(&enchantment.key);
903                level != 0 && self.levels.matches_u32(level)
904            });
905        }
906        if !self.levels.is_any() {
907            return enchantments
908                .iter()
909                .any(|(_, level)| self.levels.matches_u32(*level));
910        }
911        !enchantments.is_empty()
912    }
913}
914
915fn parse_enchantment_predicates(tag: &NbtTag) -> Option<Vec<EnchantmentPredicate>> {
916    match tag {
917        NbtTag::List(NbtList::Empty) => Some(Vec::new()),
918        NbtTag::List(NbtList::Compound(compounds)) => {
919            compounds.iter().map(parse_enchantment_predicate).collect()
920        }
921        _ => None,
922    }
923}
924
925fn parse_enchantment_predicate(compound: &NbtCompound) -> Option<EnchantmentPredicate> {
926    let enchantments =
927        parse_optional_field(compound, "enchantments", parse_enchantment_holder_set).ok()?;
928    let levels = parse_optional_field(compound, "levels", parse_int_range)
929        .ok()?
930        .unwrap_or(IntRange::ANY);
931    Some(EnchantmentPredicate {
932        enchantments,
933        levels,
934    })
935}
936
937fn parse_enchantment_holder_set(tag: &NbtTag) -> Option<Vec<EnchantmentRef>> {
938    match tag {
939        NbtTag::String(value) => parse_enchantment_holder(&value.to_string()),
940        NbtTag::List(NbtList::Empty) => Some(Vec::new()),
941        NbtTag::List(NbtList::String(values)) => {
942            let mut enchantments = Vec::new();
943            for value in values {
944                enchantments.push(parse_enchantment_reference(&value.to_string())?);
945            }
946            Some(enchantments)
947        }
948        _ => None,
949    }
950}
951
952fn parse_enchantment_holder(value: &str) -> Option<Vec<EnchantmentRef>> {
953    if let Some(tag) = value.strip_prefix('#') {
954        let key = parse_identifier_with_default_namespace(tag)?;
955        return REGISTRY.enchantments.get_tag(&key);
956    }
957    parse_enchantment_reference(value).map(|enchantment| vec![enchantment])
958}
959
960fn parse_enchantment_reference(value: &str) -> Option<EnchantmentRef> {
961    let key = parse_identifier_with_default_namespace(value)?;
962    REGISTRY.enchantments.by_key(&key)
963}
964
965#[derive(Clone, Debug, PartialEq)]
966struct AttributeModifiersPredicate {
967    modifiers: Option<AttributeModifierCollectionPredicate>,
968}
969
970impl AttributeModifiersPredicate {
971    fn matches(&self, modifiers: &vanilla_components::ItemAttributeModifiers) -> bool {
972        self.modifiers
973            .as_ref()
974            .is_none_or(|predicate| predicate.matches(&modifiers.modifiers))
975    }
976}
977
978fn parse_attribute_modifiers_predicate(tag: &NbtTag) -> Option<AttributeModifiersPredicate> {
979    let NbtTag::Compound(compound) = tag else {
980        return None;
981    };
982    let modifiers =
983        parse_optional_field(compound, "modifiers", parse_attribute_modifier_collection).ok()?;
984    Some(AttributeModifiersPredicate { modifiers })
985}
986
987#[derive(Clone, Debug, PartialEq)]
988struct AttributeModifierCollectionPredicate {
989    contains: Vec<AttributeModifierEntryPredicate>,
990    counts: Vec<AttributeModifierCountPredicate>,
991    size: Option<IntRange>,
992}
993
994impl AttributeModifierCollectionPredicate {
995    fn matches(&self, modifiers: &[vanilla_components::ItemAttributeModifierEntry]) -> bool {
996        let mut matched = vec![false; self.contains.len()];
997        for modifier in modifiers {
998            for (matched, predicate) in matched.iter_mut().zip(&self.contains) {
999                if !*matched && predicate.matches(modifier) {
1000                    *matched = true;
1001                }
1002            }
1003        }
1004        matched.into_iter().all(|matched| matched)
1005            && self.counts.iter().all(|predicate| {
1006                let count = modifiers
1007                    .iter()
1008                    .filter(|modifier| predicate.test.matches(modifier))
1009                    .count();
1010                predicate.count.matches_usize(count)
1011            })
1012            && self
1013                .size
1014                .is_none_or(|range| range.matches_usize(modifiers.len()))
1015    }
1016}
1017
1018fn parse_attribute_modifier_collection(
1019    tag: &NbtTag,
1020) -> Option<AttributeModifierCollectionPredicate> {
1021    let NbtTag::Compound(compound) = tag else {
1022        return None;
1023    };
1024    let contains = parse_optional_field(
1025        compound,
1026        "contains",
1027        parse_attribute_modifier_predicate_list,
1028    )
1029    .ok()?
1030    .unwrap_or_default();
1031    let counts = parse_optional_field(compound, "count", parse_attribute_modifier_count_list)
1032        .ok()?
1033        .unwrap_or_default();
1034    let size = parse_optional_field(compound, "size", parse_int_range).ok()?;
1035    Some(AttributeModifierCollectionPredicate {
1036        contains,
1037        counts,
1038        size,
1039    })
1040}
1041
1042fn parse_attribute_modifier_predicate_list(
1043    tag: &NbtTag,
1044) -> Option<Vec<AttributeModifierEntryPredicate>> {
1045    match tag {
1046        NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1047        NbtTag::List(NbtList::Compound(compounds)) => compounds
1048            .iter()
1049            .map(parse_attribute_modifier_entry)
1050            .collect(),
1051        _ => None,
1052    }
1053}
1054
1055fn parse_attribute_modifier_count_list(
1056    tag: &NbtTag,
1057) -> Option<Vec<AttributeModifierCountPredicate>> {
1058    match tag {
1059        NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1060        NbtTag::List(NbtList::Compound(compounds)) => compounds
1061            .iter()
1062            .map(parse_attribute_modifier_count)
1063            .collect(),
1064        _ => None,
1065    }
1066}
1067
1068#[derive(Clone, Debug, PartialEq)]
1069struct AttributeModifierCountPredicate {
1070    test: AttributeModifierEntryPredicate,
1071    count: IntRange,
1072}
1073
1074fn parse_attribute_modifier_count(
1075    compound: &NbtCompound,
1076) -> Option<AttributeModifierCountPredicate> {
1077    let NbtTag::Compound(test) = compound.get("test")? else {
1078        return None;
1079    };
1080    let count = parse_int_range(compound.get("count")?)?;
1081    Some(AttributeModifierCountPredicate {
1082        test: parse_attribute_modifier_entry(test)?,
1083        count,
1084    })
1085}
1086
1087#[derive(Clone, Debug, PartialEq)]
1088struct AttributeModifierEntryPredicate {
1089    attributes: Option<Vec<AttributeRef>>,
1090    id: Option<Identifier>,
1091    amount: DoubleRange,
1092    operation: Option<AttributeModifierOperation>,
1093    slot: Option<EquipmentSlotGroup>,
1094}
1095
1096impl AttributeModifierEntryPredicate {
1097    fn matches(&self, modifier: &vanilla_components::ItemAttributeModifierEntry) -> bool {
1098        self.attributes.as_ref().is_none_or(|attributes| {
1099            attributes
1100                .iter()
1101                .any(|attribute| attribute.key == modifier.attribute.key)
1102        }) && self.id.as_ref().is_none_or(|id| id == &modifier.id)
1103            && self.amount.matches(modifier.amount)
1104            && self
1105                .operation
1106                .is_none_or(|operation| operation == modifier.operation)
1107            && self.slot.is_none_or(|slot| slot == modifier.slot)
1108    }
1109}
1110
1111fn parse_attribute_modifier_entry(
1112    compound: &NbtCompound,
1113) -> Option<AttributeModifierEntryPredicate> {
1114    let attributes =
1115        parse_optional_field(compound, "attribute", parse_attribute_holder_set).ok()?;
1116    let id = parse_optional_field(compound, "id", parse_identifier_tag).ok()?;
1117    let amount = parse_optional_field(compound, "amount", parse_double_range)
1118        .ok()?
1119        .unwrap_or(DoubleRange::ANY);
1120    let operation =
1121        parse_optional_field(compound, "operation", parse_attribute_modifier_operation).ok()?;
1122    let slot = parse_optional_field(compound, "slot", parse_equipment_slot_group).ok()?;
1123    Some(AttributeModifierEntryPredicate {
1124        attributes,
1125        id,
1126        amount,
1127        operation,
1128        slot,
1129    })
1130}
1131
1132fn parse_attribute_holder_set(tag: &NbtTag) -> Option<Vec<AttributeRef>> {
1133    match tag {
1134        NbtTag::String(value) => parse_attribute_holder(&value.to_string()),
1135        NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1136        NbtTag::List(NbtList::String(values)) => {
1137            let mut attributes = Vec::new();
1138            for value in values {
1139                attributes.extend(parse_attribute_holder(&value.to_string())?);
1140            }
1141            Some(attributes)
1142        }
1143        _ => None,
1144    }
1145}
1146
1147fn parse_attribute_holder(value: &str) -> Option<Vec<AttributeRef>> {
1148    if value.starts_with('#') {
1149        // TODO: Support attribute tags once Steel's attribute registry stores them.
1150        return None;
1151    }
1152    let key = parse_identifier_with_default_namespace(value)?;
1153    REGISTRY
1154        .attributes
1155        .by_key(&key)
1156        .map(|attribute| vec![attribute])
1157}
1158
1159fn parse_identifier_tag(tag: &NbtTag) -> Option<Identifier> {
1160    let NbtTag::String(value) = tag else {
1161        return None;
1162    };
1163    parse_identifier_with_default_namespace(&value.to_string())
1164}
1165
1166fn parse_attribute_modifier_operation(tag: &NbtTag) -> Option<AttributeModifierOperation> {
1167    let NbtTag::String(value) = tag else {
1168        return None;
1169    };
1170    AttributeModifierOperation::by_name(&value.to_string())
1171}
1172
1173fn parse_equipment_slot_group(tag: &NbtTag) -> Option<EquipmentSlotGroup> {
1174    let NbtTag::String(value) = tag else {
1175        return None;
1176    };
1177    let value = value.to_string();
1178    EquipmentSlotGroup::by_name(&value).filter(|slot| slot.name() == value)
1179}
1180
1181#[derive(Clone, Copy, Debug, PartialEq)]
1182struct DoubleRange {
1183    min: Option<f64>,
1184    max: Option<f64>,
1185}
1186
1187impl DoubleRange {
1188    const ANY: Self = Self {
1189        min: None,
1190        max: None,
1191    };
1192
1193    fn matches(self, value: f64) -> bool {
1194        self.min
1195            .is_none_or(|minimum| minimum.partial_cmp(&value) != Some(Ordering::Greater))
1196            && self
1197                .max
1198                .is_none_or(|maximum| maximum.partial_cmp(&value) != Some(Ordering::Less))
1199    }
1200}
1201
1202fn parse_double_range(tag: &NbtTag) -> Option<DoubleRange> {
1203    if let Some(value) = numeric_f64(tag) {
1204        return Some(DoubleRange {
1205            min: Some(value),
1206            max: Some(value),
1207        });
1208    }
1209    let NbtTag::Compound(compound) = tag else {
1210        return None;
1211    };
1212    let min = parse_optional_field(compound, "min", numeric_f64).ok()?;
1213    let max = parse_optional_field(compound, "max", numeric_f64).ok()?;
1214    if min.zip(max).is_some_and(|(min, max)| min > max) {
1215        return None;
1216    }
1217    Some(DoubleRange { min, max })
1218}
1219
1220fn numeric_f64(tag: &NbtTag) -> Option<f64> {
1221    match tag {
1222        NbtTag::Byte(value) => Some(f64::from(*value)),
1223        NbtTag::Short(value) => Some(f64::from(*value)),
1224        NbtTag::Int(value) => Some(f64::from(*value)),
1225        NbtTag::Long(value) => Some(*value as f64),
1226        NbtTag::Float(value) => Some(f64::from(*value)),
1227        NbtTag::Double(value) => Some(*value),
1228        _ => None,
1229    }
1230}
1231
1232fn parse_optional_field<T>(
1233    compound: &NbtCompound,
1234    key: &str,
1235    parser: impl FnOnce(&NbtTag) -> Option<T>,
1236) -> Result<Option<T>, ()> {
1237    match compound.get(key) {
1238        Some(tag) => parser(tag).map(Some).ok_or(()),
1239        None => Ok(None),
1240    }
1241}
1242
1243fn parse_identifier_with_default_namespace(value: &str) -> Option<Identifier> {
1244    let (namespace, path) = value.split_once(':').map_or(
1245        (Identifier::VANILLA_NAMESPACE, value),
1246        |(namespace, path)| {
1247            if namespace.is_empty() {
1248                (Identifier::VANILLA_NAMESPACE, path)
1249            } else {
1250                (namespace, path)
1251            }
1252        },
1253    );
1254    (!namespace.is_empty() && !path.is_empty() && Identifier::validate(namespace, path))
1255        .then(|| Identifier::new(namespace.to_owned(), path.to_owned()))
1256}
1257
1258pub(super) fn suggest_item_predicate(builder: &mut SuggestionsBuilder<'_>) {
1259    let input = builder.remaining();
1260    let Some(component_start) = input.find('[') else {
1261        suggest_item_targets(input, builder);
1262        if valid_target(input) {
1263            builder.suggest(format!("{input}["));
1264        }
1265        return;
1266    };
1267    if !valid_target(input[..component_start].trim_end()) {
1268        return;
1269    }
1270
1271    let Some((current_start, current)) = current_term(&input[component_start + 1..]) else {
1272        return;
1273    };
1274    if current.contains(['=', '~']) {
1275        return;
1276    }
1277    let trimmed = current.trim_start();
1278    let whitespace = current.len() - trimmed.len();
1279    let prefix_end = component_start + 1 + current_start + whitespace;
1280    let prefix = &input[..prefix_end];
1281    let resource_prefix = trimmed.strip_prefix('!').unwrap_or(trimmed);
1282
1283    for entry in
1284        (0..REGISTRY.data_components.len()).filter_map(|id| REGISTRY.data_components.by_id(id))
1285    {
1286        if entry.is_persistent() && resource_matches(resource_prefix, &entry.key) {
1287            builder.suggest(format!("{prefix}{}", entry.key));
1288        }
1289    }
1290    let count = Identifier::vanilla_static("count");
1291    if resource_matches(resource_prefix, &count) {
1292        builder.suggest(format!("{prefix}{count}"));
1293    }
1294    for path in VANILLA_DATA_COMPONENT_PREDICATE_KEYS {
1295        let key = Identifier::vanilla_static(path);
1296        if resource_matches(resource_prefix, &key) {
1297            builder.suggest(format!("{prefix}{key}"));
1298        }
1299    }
1300}
1301
1302fn suggest_item_targets(input: &str, builder: &mut SuggestionsBuilder<'_>) {
1303    if let Some(prefix) = input.strip_prefix('#') {
1304        for key in REGISTRY.items.tag_keys() {
1305            if resource_matches(prefix, key) {
1306                builder.suggest(format!("#{key}"));
1307            }
1308        }
1309        return;
1310    }
1311    for (_, item) in REGISTRY.items.iter() {
1312        if resource_matches(input, &item.key) {
1313            builder.suggest(item.key.to_string());
1314        }
1315    }
1316    if "*".starts_with(input) {
1317        builder.suggest("*");
1318    }
1319}
1320
1321fn valid_target(input: &str) -> bool {
1322    if input == "*" {
1323        return true;
1324    }
1325    if let Some(tag) = input.strip_prefix('#') {
1326        return parse_identifier_text(tag)
1327            .is_some_and(|tag| REGISTRY.items.get_tag(&tag).is_some());
1328    }
1329    parse_identifier_text(input).is_some_and(|key| REGISTRY.items.by_key(&key).is_some())
1330}
1331
1332fn parse_identifier_text(input: &str) -> Option<Identifier> {
1333    let mut reader = StringReader::new(input);
1334    let key = parse_identifier(&mut reader).ok()?;
1335    (!reader.can_read()).then_some(key)
1336}
1337
1338fn resource_matches(pattern: &str, key: &Identifier) -> bool {
1339    let pattern = pattern.strip_prefix("minecraft:").unwrap_or(pattern);
1340    if pattern.contains(':') {
1341        return matches_substring(pattern, &key.to_string());
1342    }
1343    matches_substring(pattern, key.namespace.as_ref())
1344        || matches_substring(pattern, key.path.as_ref())
1345}
1346
1347fn current_term(conditions: &str) -> Option<(usize, &str)> {
1348    let mut depth = 0usize;
1349    let mut quote = None;
1350    let mut escaped = false;
1351    let mut start = 0usize;
1352
1353    for (index, character) in conditions.char_indices() {
1354        if let Some(terminator) = quote {
1355            if escaped {
1356                escaped = false;
1357            } else if character == '\\' {
1358                escaped = true;
1359            } else if character == terminator {
1360                quote = None;
1361            }
1362            continue;
1363        }
1364        match character {
1365            '"' | '\'' => quote = Some(character),
1366            '{' | '[' => depth += 1,
1367            '}' | ']' if depth > 0 => depth -= 1,
1368            ']' => return None,
1369            ',' | '|' if depth == 0 => start = index + character.len_utf8(),
1370            '!' if depth == 0 && conditions[start..index].trim().is_empty() => {
1371                start = index + character.len_utf8();
1372            }
1373            _ => {}
1374        }
1375    }
1376    Some((start, &conditions[start..]))
1377}