diff --git a/crates/engine/src/parser/oracle_static/restriction.rs b/crates/engine/src/parser/oracle_static/restriction.rs index 41f1ada7ec..3d53cdfd7e 100644 --- a/crates/engine/src/parser/oracle_static/restriction.rs +++ b/crates/engine/src/parser/oracle_static/restriction.rs @@ -288,11 +288,13 @@ pub(crate) fn parse_attach_only_restriction( pub(crate) fn parse_activation_exemption_suffix( input: &str, ) -> OracleResult<'_, ActivationExemption> { - // CR 605.1a: the " unless they're mana abilities" carve-out, tolerant of both - // the ASCII and U+2019 apostrophe via the shared suffix combinator. - let mut parser = opt(value( - ActivationExemption::ManaAbilities, - super::shared::parse_mana_ability_exemption_suffix, + // CR 605.1a: MTGJSON oracle text uses the typographic apostrophe (U+2019), + // so accept both forms — the adjacent "can't be activated" predicate already + // does (see shared.rs / evasion.rs). Straight-apostrophe-only here silently + // dropped the exemption and wrongly blocked mana abilities (Pithing Needle). + let mut parser = opt(preceded( + alt((tag(" unless they're "), tag(" unless they\u{2019}re "))), + value(ActivationExemption::ManaAbilities, tag("mana abilities")), )); let (rest, exemption) = parser.parse(input)?; Ok((rest, exemption.unwrap_or_default())) @@ -2876,6 +2878,48 @@ mod spend_any_color_to_activate_abilities_tests { } } +#[cfg(test)] +mod activation_exemption_apostrophe_tests { + use super::*; + + /// CR 605.1a: MTGJSON ships oracle text with the U+2019 apostrophe, so the + /// "unless they're mana abilities" exemption must parse with BOTH apostrophe + /// forms — matching the adjacent dual-apostrophe "can't be activated" + /// predicate. With only the straight apostrophe, a U+2019 printing (Pithing + /// Needle, Bound by Moonsilver, ...) silently dropped the exemption and the + /// runtime wrongly blocked the permanent's mana abilities. This is the + /// building-block test for the whole class, driving the suffix combinator + /// directly rather than one card. + #[test] + fn exemption_suffix_accepts_both_apostrophes() { + for suffix in [ + " unless they're mana abilities", + " unless they\u{2019}re mana abilities", + ] { + let (rest, exemption) = + parse_activation_exemption_suffix(suffix).expect("exemption suffix must parse"); + assert_eq!( + exemption, + ActivationExemption::ManaAbilities, + "suffix `{suffix}` must yield the mana-abilities exemption" + ); + assert!( + rest.is_empty(), + "suffix `{suffix}` left `{rest}` unconsumed" + ); + } + } + + /// Absent suffix stays `None` (the exemption is genuinely optional). + #[test] + fn exemption_suffix_absent_is_none() { + let (rest, exemption) = + parse_activation_exemption_suffix(" and other text").expect("must parse (opt)"); + assert_eq!(exemption, ActivationExemption::None); + assert_eq!(rest, " and other text"); + } +} + #[cfg(test)] mod filtered_spend_any_type_tests { use super::*; diff --git a/crates/engine/src/parser/oracle_static/shared.rs b/crates/engine/src/parser/oracle_static/shared.rs index c69b62f3a8..006062c383 100644 --- a/crates/engine/src/parser/oracle_static/shared.rs +++ b/crates/engine/src/parser/oracle_static/shared.rs @@ -799,8 +799,16 @@ fn parse_activation_compound_tail(input: &str) -> OracleResult<'_, ()> { ( tag(", and "), opt(alt((tag("its "), tag("their ")))), - parse_activated_abilities_cant_be_activated, - opt(parse_mana_ability_exemption_suffix), + alt(( + tag("activated abilities can't be activated"), + tag("activated abilities can\u{2019}t be activated"), + )), + // CR 605.1a: accept the U+2019 apostrophe too (MTGJSON's form), + // mirroring the dual-apostrophe "can't be activated" tag above. + opt(( + alt((tag(" unless they're "), tag(" unless they\u{2019}re "))), + tag("mana abilities"), + )), opt(tag(".")), ), )