Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions crates/engine/src/parser/oracle_static/restriction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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::*;
Expand Down
12 changes: 10 additions & 2 deletions crates/engine/src/parser/oracle_static/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(".")),
),
)
Expand Down
Loading