@@ -1076,81 +1076,6 @@ impl Parser {
1079− // --- @svg (inline SVG from file) ---
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())
1090− (None, rest.to_string())
1093− (None, rest.to_string())
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),
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() {
1111− if let Some(ref val) = attr.value {
1112− svg = set_svg_attr(&svg, "width", val);
1116− if let Some(ref val) = attr.value {
1117− svg = set_svg_attr(&svg, "height", val);
1120− "color" | "fill" => {
1121− if let Some(ref val) = attr.value {
1122− svg = set_svg_attr(&svg, "fill", val);
1126− if let Some(ref val) = attr.value {
1127− svg = set_svg_attr(&svg, "class", val);
1131− if let Some(ref val) = attr.value {
1132− svg = set_svg_attr(&svg, "id", val);
1139− return Ok(Some(vec![Node::Raw(svg)]));
1142− ctx.diagnostics.push(Diagnostic {
1145− message: format!("cannot load SVG '{}': {}", filename, e),
1146− severity: Severity::Error,
1147− source_line: Some(content.clone()),
1155 // --- @data (load JSON file into variables) ---
1080 // --- @data (load JSON file into variables) ---
@@ -1778,7 +1703,7 @@ impl Parser {
1779 if content.starts_with('@') || content.starts_with('[') {
1704 if content.starts_with('@') || content.starts_with('[') {
1780 let node = self.parse_element_line(&content, current_indent, line_num, ctx)?;
1705 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)]));
1784 // --- Bare text ---
1709 // --- Bare text ---
@@ -2279,7 +2204,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
2279 "assert",
2204 "assert",
2280 "markdown",
2205 "markdown",
2285fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
2209fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseError> {
@@ -2340,6 +2264,7 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
2340 ("@base", "use `@page [base ...] Title`"),
2264 ("@base", "use `@page [base ...] Title`"),
2341 ("@og", "use `@meta og:NAME VALUE`"),
2265 ("@og", "use `@meta og:NAME VALUE`"),
2342 ("@debug", "use `@warn`"),
2266 ("@debug", "use `@warn`"),
2267+ ("@svg", "use `@image [inline] file.svg`"),
2343 ("@match", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2268 ("@match", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2344 ("@case", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2269 ("@case", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2345 ("@default", "use `@else` in an `@if` chain"),
2270 ("@default", "use `@else` in an `@if` chain"),
@@ -3856,6 +3781,51 @@ fn substitute_vars(input: &str, vars: &HashMap<String, String>) -> String {
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 {
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 {
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),
3802+ let mut svg = match std::fs::read_to_string(&resolved) {
3803+ Ok(text) => text.trim().to_string(),
3805+ ctx.diagnostics.push(Diagnostic {
3808+ message: format!("cannot load SVG '{}': {}", filename, e),
3809+ severity: Severity::Error,
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(),
3824+ svg = set_svg_attr(&svg, target, value);
3859/// The first `$name|filter` (old filter syntax) in `text`, as (name, filter).
3829/// The first `$name|filter` (old filter syntax) in `text`, as (name, filter).
3860fn old_filter_syntax(text: &str) -> Option<(String, String)> {
3830fn old_filter_syntax(text: &str) -> Option<(String, String)> {
3861 const FILTERS: &[&str] = &[
3831 const FILTERS: &[&str] = &[