@@ -2766,6 +2766,15 @@ fn parse_attr_list(
27662766
27672767 for part in split_commas(input) {
27682768 let part = part.trim();
2769+ // A whole attribute `if(cond, a, b)` is the chosen branch's text.
2770+ let chosen;
2771+ let part = match choose_if(part, ctx, line_num) {
2772+ Some(branch) => {
2773+ chosen = branch;
2774+ chosen.as_str()
2775+ }
2776+ None => part,
2777+ };
27692778 if part.is_empty() {
27702779 continue;
27712780 }
@@ -2781,19 +2790,24 @@ fn parse_attr_list(
27812790
27822791 track_var_refs(part, &mut ctx.used_variables);
27832792
2784− // Conditional attribute: `key if condition` or `key value if condition`.
2785− // Conditions are evaluated on the source text, before substitution.
2786− let part = match split_trailing_if(part) {
2787− (attr, Some(condition)) => {
2788− if !ctx.condition(condition, line_num) {
2789− continue;
2790− }
2791− attr
2792− }
2793− (attr, None) => attr,
2793+ if let Some((attr, condition)) = split_trailing_if(part) {
2794+ ctx.diagnostics.push(Diagnostic {
2795+ line: line_num,
2796+ column: None,
2797+ message: format!(
2798+ "`KEY if CONDITION` was removed: write `if({}, {})`",
2799+ condition, attr
2800+ ),
2801+ severity: Severity::Error,
2802+ source_line: None,
2803+ });
2804+ continue;
2805+ }
2806+ // A value `if(cond, a, b)` picks `a` or `b` (free text) by `cond`;
2807+ // an empty choice leaves the attribute out.
2808+ let Some(part) = choose_if_value(part, ctx, line_num) else {
2809+ continue;
27942810 };
2795− // A value `if(cond, a, b)` picks `a` or `b` (free text) by `cond`.
2796− let part = choose_if_value(part, ctx, line_num);
27972811 let part = substitute_vars(&part, &ctx.variables);
27982812
27992813 let attr = if let Some((key, value)) = split_html_attribute(&part) {
@@ -3108,53 +3122,92 @@ fn numeric_range(start: i64, end: i64, step: i64) -> Vec<String> {
31083122
31093123
31103124
3111−/// Evaluate `if(condition, true_val, false_val)` expressions in attribute values.
3112−/// Split `key value if condition` into the attribute and its condition
3113−/// (ignoring ` if ` inside parentheses).
3114−fn split_trailing_if(part: &str) -> (&str, Option<&str>) {
3125+/// Find a leftover `key if condition` (ignoring ` if ` inside parentheses
3126+/// or quotes), to point at `if()`.
3127+fn split_trailing_if(part: &str) -> Option<(&str, &str)> {
31153128 let mut depth = 0;
3129+ let mut quote = None;
31163130 for (i, c) in part.char_indices() {
31173131 match c {
3132+ '"' | '\'' if quote == Some(c) => quote = None,
3133+ '"' | '\'' if quote.is_none() => quote = Some(c),
3134+ _ if quote.is_some() => {}
31183135 '(' => depth += 1,
31193136 ')' => depth -= 1,
31203137 _ if depth == 0 && part[i..].starts_with(" if ") => {
3121− return (&part[..i], Some(part[i + 4..].trim()));
3138+ return Some((&part[..i], part[i + 4..].trim()));
31223139 }
31233140 _ => {}
31243141 }
31253142 }
3126− (part, None)
3143+ None
31273144}
31283145
3129−/// Resolve a value written `if(cond, a, b)`: evaluate `cond` and keep the
3130−/// chosen branch's text. Other attributes are returned unchanged.
3131−fn choose_if_value(part: &str, ctx: &mut ParseContext, line: usize) -> String {
3132− let (head, value) = match part.find(['=', ' ']) {
3133− Some(pos) => part.split_at(pos + 1),
3134− None => return part.to_string(),
3135− };
3136− let value = value.trim();
3137− let Some(inner) = value.strip_prefix("if(").and_then(|v| v.strip_suffix(')')) else {
3138− return part.to_string();
3139− };
3146+/// Evaluate `if(cond, a)` or `if(cond, a, b)` when it is all of `text`,
3147+/// returning the chosen branch (empty when `cond` fails and there is no `b`).
3148+fn choose_if(text: &str, ctx: &mut ParseContext, line: usize) -> Option<String> {
3149+ let inner = text.strip_prefix("if(")?.strip_suffix(')')?;
31403150 let args = split_if_args(inner);
3141− if args.len() != 3 {
3142− return part.to_string();
3151+ // `if(a) (b)` isn't one call
3152+ if split_trailing_paren(inner) || !(2..=3).contains(&args.len()) {
3153+ return None;
31433154 }
31443155 let branch = if ctx.condition(args[0].trim(), line) {
31453156 args[1]
31463157 } else {
3147− args[2]
3158+ args.get(2).copied().unwrap_or("")
3159+ };
3160+ Some(branch.trim().to_string())
3161+}
3162+
3163+/// Whether the parentheses in `inner` close before its end, as in the
3164+/// inside of `if(a)(b)`.
3165+fn split_trailing_paren(inner: &str) -> bool {
3166+ let mut depth = 0i32;
3167+ let mut quote = None;
3168+ for c in inner.chars() {
3169+ match c {
3170+ '"' | '\'' if quote == Some(c) => quote = None,
3171+ '"' | '\'' if quote.is_none() => quote = Some(c),
3172+ _ if quote.is_some() => {}
3173+ '(' => depth += 1,
3174+ ')' => {
3175+ depth -= 1;
3176+ if depth < 0 {
3177+ return true;
3178+ }
3179+ }
3180+ _ => {}
3181+ }
3182+ }
3183+ false
3184+}
3185+
3186+/// Resolve a value written `if(cond, a, b)`: evaluate `cond` and keep the
3187+/// chosen branch's text, or `None` when the choice is empty. Other
3188+/// attributes are returned unchanged.
3189+fn choose_if_value(part: &str, ctx: &mut ParseContext, line: usize) -> Option<String> {
3190+ let Some(pos) = part.find(['=', ' ']) else {
3191+ return Some(part.to_string());
31483192 };
3149− format!("{}{}", head, branch.trim())
3193+ let (head, value) = part.split_at(pos + 1);
3194+ match choose_if(value.trim(), ctx, line) {
3195+ Some(branch) if branch.is_empty() => None,
3196+ Some(branch) => Some(format!("{}{}", head, branch)),
3197+ None => Some(part.to_string()),
3198+ }
31503199}
31513200
31523201fn split_if_args(input: &str) -> Vec<&str> {
31533202 let mut parts = Vec::new();
31543203 let mut start = 0;
31553204 let mut depth = 0;
3205+ let mut quote = None;
31563206 for (i, c) in input.char_indices() {
31573207 match c {
3208+ '"' | '\'' if quote == Some(c) => quote = None,
3209+ '"' | '\'' if quote.is_none() => quote = Some(c),
3210+ _ if quote.is_some() => {}
31583211 '(' => depth += 1,
31593212 ')' => depth -= 1,
31603213 ',' if depth == 0 => {