@@ -1076,81 +1076,6 @@ impl Parser {
10761076 }
10771077
10781078
1079− // --- @svg (inline SVG from file) ---
1080−
1081− if let Some(rest) = content.strip_prefix("@svg ") {
1082− let rest = rest.trim();
1083− // @svg file.svg OR @svg [attrs] file.svg
1084− let (attrs_part, filename) = if rest.starts_with('[') {
1085− if let Some(bracket_end) = rest.find(']') {
1086− let attrs_str = &rest[..=bracket_end];
1087− let file = rest[bracket_end + 1..].trim();
1088− (Some(attrs_str.to_string()), file.to_string())
1089− } else {
1090− (None, rest.to_string())
1091− }
1092− } else {
1093− (None, rest.to_string())
1094− };
1095−
1096− let filename = substitute_vars(&filename, &ctx.variables);
1097− let resolved = match &ctx.base_path {
1098− Some(base) => base.join(&filename),
1099− None => PathBuf::from(&filename),
1100− };
1101−
1102− match std::fs::read_to_string(&resolved) {
1103− Ok(svg_content) => {
1104− let mut svg = svg_content.trim().to_string();
1105− // Apply attributes (width, height, color/fill, class)
1106− if let Some(ref attrs_str) = attrs_part {
1107− let (attrs, _) = parse_attr_brackets(attrs_str, line_num, ctx)?;
1108− for attr in &attrs {
1109− match attr.key.as_str() {
1110− "width" => {
1111− if let Some(ref val) = attr.value {
1112− svg = set_svg_attr(&svg, "width", val);
1113− }
1114− }
1115− "height" => {
1116− if let Some(ref val) = attr.value {
1117− svg = set_svg_attr(&svg, "height", val);
1118− }
1119− }
1120− "color" | "fill" => {
1121− if let Some(ref val) = attr.value {
1122− svg = set_svg_attr(&svg, "fill", val);
1123− }
1124− }
1125− "class" => {
1126− if let Some(ref val) = attr.value {
1127− svg = set_svg_attr(&svg, "class", val);
1128− }
1129− }
1130− "id" => {
1131− if let Some(ref val) = attr.value {
1132− svg = set_svg_attr(&svg, "id", val);
1133− }
1134− }
1135− _ => {}
1136− }
1137− }
1138− }
1139− return Ok(Some(vec![Node::Raw(svg)]));
1140− }
1141− Err(e) => {
1142− ctx.diagnostics.push(Diagnostic {
1143− line: line_num,
1144− column: None,
1145− message: format!("cannot load SVG '{}': {}", filename, e),
1146− severity: Severity::Error,
1147− source_line: Some(content.clone()),
1148− });
1149− return Ok(None);
1150− }
1151− }
1152− }
1153−
11541079
11551080 // --- @data (load JSON file into variables) ---
11561081
@@ -1778,7 +1703,7 @@ impl Parser {
17781703
17791704 if content.starts_with('@') || content.starts_with('[') {
17801705 let node = self.parse_element_line(&content, current_indent, line_num, ctx)?;
1781− return Ok(Some(vec![node]));
1706+ return Ok(Some(vec![inline_svg(node, line_num, ctx)]));
17821707 }
17831708
17841709 // --- Bare text ---
@@ -2279,7 +2204,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
22792204 "assert",
22802205 "markdown",
22812206 "data",
2282− "svg",
22832207];
22842208
22852209fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
@@ -2340,6 +2264,7 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
23402264 ("@base", "use `@page [base ...] Title`"),
23412265 ("@og", "use `@meta og:NAME VALUE`"),
23422266 ("@debug", "use `@warn`"),
2267+ ("@svg", "use `@image [inline] file.svg`"),
23432268 ("@match", "use `@if $x == a` / `@else if $x == b` / `@else`"),
23442269 ("@case", "use `@if $x == a` / `@else if $x == b` / `@else`"),
23452270 ("@default", "use `@else` in an `@if` chain"),
@@ -3856,6 +3781,51 @@ fn substitute_vars(input: &str, vars: &HashMap<String, String>) -> String {
38563781 result
38573782}
38583783
3784+/// `@image [inline] icon.svg` becomes the SVG's markup, with `width`,
3785+/// `height`, `color` / `fill`, `class=` and `id=` applied to the `<svg>`
3786+/// tag. Other nodes are returned unchanged.
3787+fn inline_svg(node: Node, line_num: usize, ctx: &mut ParseContext) -> Node {
3788+ let Node::Element(elem) = &node else {
3789+ return node;
3790+ };
3791+ let is_inline_svg = elem.kind == ElementKind::Image
3792+ && elem.attrs.iter().any(|a| a.key == "inline" && !a.html)
3793+ && elem.argument.as_deref().is_some_and(|src| src.ends_with(".svg"));
3794+ if !is_inline_svg {
3795+ return node;
3796+ }
3797+ let filename = elem.argument.clone().unwrap_or_default();
3798+ let resolved = match &ctx.base_path {
3799+ Some(base) => base.join(&filename),
3800+ None => PathBuf::from(&filename),
3801+ };
3802+ let mut svg = match std::fs::read_to_string(&resolved) {
3803+ Ok(text) => text.trim().to_string(),
3804+ Err(e) => {
3805+ ctx.diagnostics.push(Diagnostic {
3806+ line: line_num,
3807+ column: None,
3808+ message: format!("cannot load SVG '{}': {}", filename, e),
3809+ severity: Severity::Error,
3810+ source_line: None,
3811+ });
3812+ return node;
3813+ }
3814+ };
3815+ ctx.included_files.push(resolved);
3816+ for attr in &elem.attrs {
3817+ let Some(value) = &attr.value else { continue };
3818+ let target = match (attr.key.as_str(), attr.html) {
3819+ ("width", false) | ("height", false) => attr.key.as_str(),
3820+ ("color", false) | ("fill", false) => "fill",
3821+ ("class", true) | ("id", true) => attr.key.as_str(),
3822+ _ => continue,
3823+ };
3824+ svg = set_svg_attr(&svg, target, value);
3825+ }
3826+ Node::Raw(svg)
3827+}
3828+
38593829/// The first `$name|filter` (old filter syntax) in `text`, as (name, filter).
38603830fn old_filter_syntax(text: &str) -> Option<(String, String)> {
38613831 const FILTERS: &[&str] = &[