@@ -798,8 +798,12 @@ impl Evaluator {
801− let var_warnings =
801+ let var_warnings = check_undefined_vars(
802− check_undefined_vars(&content, &ctx.variables, line_num, current_indent);
802+ &protect_escapes(&content),
803 ctx.diagnostics.extend(var_warnings);
807 ctx.diagnostics.extend(var_warnings);
804 track_var_refs(&content, &mut ctx.used_variables);
808 track_var_refs(&content, &mut ctx.used_variables);
805 let segments = parse_text_segments(&content, ctx);
809 let segments = parse_text_segments(&content, ctx);
@@ -1356,18 +1360,18 @@ fn parse_single_element(
1356 let argument = if rest.is_empty() {
1360 let argument = if rest.is_empty() {
1358 } else if kind == ElementKind::Link {
1362 } else if kind == ElementKind::Link {
1359− let rest_sub = substitute_vars(&rest, &ctx.variables);
1363+ let rest_sub = substitute_vars(&protect_escapes(&rest), &ctx.variables);
1360 if let Some((url, text)) = rest_sub.split_once(' ') {
1364 if let Some((url, text)) = rest_sub.split_once(' ') {
1361 let text = text.trim();
1365 let text = text.trim();
1362 if !text.is_empty() {
1366 if !text.is_empty() {
1363 children.push(Node::Text(parse_text_segments(text, ctx)));
1367 children.push(Node::Text(parse_text_segments(text, ctx)));
1365− Some(url.to_string())
1369+ Some(restore_escapes(url))
1366 } else {
1370 } else {
1367− Some(rest_sub)
1371+ Some(restore_escapes(&rest_sub))
1369 } else {
1373 } else {
1370− Some(substitute_vars(&rest, &ctx.variables))
1374+ Some(restore_escapes(&substitute_vars(&protect_escapes(&rest), &ctx.variables)))
1373 // For @slot, the argument is the slot name
1377 // For @slot, the argument is the slot name
@@ -2433,20 +2437,50 @@ fn split_chain(content: &str) -> Vec<String> {
2433// Text segment parsing (inline {...} elements)
2437// Text segment parsing (inline {...} elements)
2434// ---------------------------------------------------------------------------
2438// ---------------------------------------------------------------------------
2440+/// Escapes in text: `\@`, `\$`, `\{`, `\--` and `\\` stand for the
2441+/// character(s) themselves. While a text is processed, each is a private-use
2442+/// placeholder, so it can't start a variable, an inline element or a line.
2443+const ESCAPES: &[(&str, char, &str)] = &[
2444+ ("\\\\", '\u{E000}', "\\"),
2445+ ("\\$", '\u{E001}', "$"),
2446+ ("\\{", '\u{E002}', "{"),
2447+ ("\\@", '\u{E003}', "@"),
2448+ ("\\--", '\u{E004}', "--"),
2451+fn protect_escapes(text: &str) -> String {
2452+ if !text.contains('\\') {
2453+ return text.to_string();
2455+ let mut out = text.to_string();
2456+ for (escape, placeholder, _) in ESCAPES {
2457+ out = out.replace(escape, &placeholder.to_string());
2462+fn restore_escapes(text: &str) -> String {
2463+ let mut out = text.to_string();
2464+ for (_, placeholder, literal) in ESCAPES {
2465+ out = out.replace(*placeholder, literal);
2436fn parse_text_segments(input: &str, ctx: &mut ParseContext) -> Vec<TextSegment> {
2470fn parse_text_segments(input: &str, ctx: &mut ParseContext) -> Vec<TextSegment> {
2437 let mut segments = Vec::new();
2471 let mut segments = Vec::new();
2438 let mut current_text = String::new();
2472 let mut current_text = String::new();
2439− let chars: Vec<char> = input.chars().collect();
2473+ let chars: Vec<char> = protect_escapes(input).chars().collect();
2440 let mut i = 0;
2474 let mut i = 0;
2442 while i < chars.len() {
2476 while i < chars.len() {
2443 if chars[i] == '{' && i + 1 < chars.len() && chars[i + 1] == '@' {
2477 if chars[i] == '{' && i + 1 < chars.len() && chars[i + 1] == '@' {
2444 // Flush accumulated plain text
2478 // Flush accumulated plain text
2445 if !current_text.is_empty() {
2479 if !current_text.is_empty() {
2446− segments.push(TextSegment::Plain(substitute_vars(
2480+ segments.push(TextSegment::Plain(restore_escapes(&substitute_vars(
2447 ¤t_text,
2481 ¤t_text,
2448 &ctx.variables,
2482 &ctx.variables,
2450 current_text.clear();
2484 current_text.clear();
@@ -2498,25 +2532,15 @@ fn parse_text_segments(input: &str, ctx: &mut ParseContext) -> Vec<TextSegment>
2500 if !current_text.is_empty() {
2534 if !current_text.is_empty() {
2501− segments.push(TextSegment::Plain(substitute_vars(
2535+ segments.push(TextSegment::Plain(restore_escapes(&substitute_vars(
2502 ¤t_text,
2536 ¤t_text,
2503 &ctx.variables,
2537 &ctx.variables,
2507 segments
2541 segments
2510−// ---------------------------------------------------------------------------
2511−// Variable substitution
2512−// ---------------------------------------------------------------------------
2514−// ---------------------------------------------------------------------------
2515−// Condition evaluation for @if
2516−// ---------------------------------------------------------------------------
2518−/// Inclusive integer range from `start` to `end` (counting down when
2519−/// `start > end`), stepping by `step`. Stops instead of overflowing.
2520/// The items of a text list: a range `A..B [step N]`, or comma-separated.
2544/// The items of a text list: a range `A..B [step N]`, or comma-separated.
2521fn text_list_items(list: &str) -> Vec<String> {
2545fn text_list_items(list: &str) -> Vec<String> {
2522 if let Some((start, rest)) = list.split_once("..") {
2546 if let Some((start, rest)) = list.split_once("..") {
@@ -2552,6 +2576,8 @@ fn bind_item(vars: &mut HashMap<String, String>, source: &str, target: &str) {
2552 vars.entry(target.to_string()).or_default();
2576 vars.entry(target.to_string()).or_default();
2579+/// Inclusive integer range from `start` to `end` (counting down when
2580+/// `start > end`), stepping by `step`. Stops instead of overflowing.
2555fn numeric_range(start: i64, end: i64, step: i64) -> Vec<String> {
2581fn numeric_range(start: i64, end: i64, step: i64) -> Vec<String> {
2556 let mut items = Vec::new();
2582 let mut items = Vec::new();
2557 let mut n = start;
2583 let mut n = start;