@@ -85,7 +85,6 @@ struct ParseContext {
8585 css_vars: Vec<(String, String)>,
8686 custom_css: Vec<String>,
8787 og_tags: Vec<(String, String)>,
88− custom_breakpoints: Vec<(String, String)>,
8988 diagnostics: Vec<Diagnostic>,
9089 base_path: Option<PathBuf>,
9190 included_files: Vec<PathBuf>,
@@ -104,20 +103,10 @@ struct ParseContext {
104103 fn_lines: HashMap<String, usize>,
105104 /// Line numbers of attribute bundle definitions (name -> line)
106105 define_lines: HashMap<String, usize>,
107− /// Deprecated functions: name -> deprecation message
108− deprecated_fns: HashMap<String, String>,
109− /// Theme tokens: (name, value) pairs from @theme
110− theme_tokens: Vec<(String, String)>,
111106 /// Canonical URL
112107 canonical: Option<String>,
113108 /// Base URL for relative links
114109 base_url: Option<String>,
115− /// @font-face declarations: (font_name, url)
116− font_faces: Vec<(String, String)>,
117− /// @json-ld blocks
118− json_ld_blocks: Vec<String>,
119− /// @manifest configuration
120− manifest: Option<crate::ast::ManifestConfig>,
121110 /// Track @import paths for circular dependency detection
122111 import_stack: Vec<PathBuf>,
123112}
@@ -197,7 +186,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
197186 css_vars: Vec::new(),
198187 custom_css: Vec::new(),
199188 og_tags: Vec::new(),
200− custom_breakpoints: Vec::new(),
201189 diagnostics: Vec::new(),
202190 base_path: base_path.map(|p| p.to_path_buf()),
203191 included_files: Vec::new(),
@@ -210,13 +198,8 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
210198 let_lines: HashMap::new(),
211199 fn_lines: HashMap::new(),
212200 define_lines: HashMap::new(),
213− deprecated_fns: HashMap::new(),
214− theme_tokens: Vec::new(),
215201 canonical: None,
216202 base_url: None,
217− font_faces: Vec::new(),
218− json_ld_blocks: Vec::new(),
219− manifest: None,
220203 import_stack: Vec::new(),
221204 };
222205 load_prelude(&mut ctx);
@@ -236,13 +219,8 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
236219 css_vars: ctx.css_vars,
237220 custom_css: ctx.custom_css,
238221 og_tags: ctx.og_tags,
239− custom_breakpoints: ctx.custom_breakpoints,
240− theme_tokens: ctx.theme_tokens,
241222 canonical: ctx.canonical,
242223 base_url: ctx.base_url,
243− font_faces: ctx.font_faces,
244− json_ld_blocks: ctx.json_ld_blocks,
245− manifest: ctx.manifest,
246224 preload_hints: collect_image_preload_hints(&nodes),
247225 nodes,
248226 },
@@ -744,17 +722,6 @@ impl Parser {
744722 }
745723
746724
747− if let Some(rest) = content.strip_prefix("@breakpoint ") {
748− let rest = rest.trim();
749− if let Some((name, value)) = rest.split_once(' ') {
750− ctx.custom_breakpoints.push((
751− name.trim().to_string(),
752− substitute_vars(value.trim(), &ctx.variables),
753− ));
754− }
755− return Ok(None);
756− }
757−
758725 if content == "@head" || content.starts_with("@head ") {
759726 // Collect indented body lines as raw head content
760727 let mut head_content = String::new();
@@ -851,61 +818,6 @@ impl Parser {
851818 }
852819 }
853820
854− // --- @manifest (PWA web manifest) ---
855− if content.trim() == "@manifest" || content.starts_with("@manifest ") {
856− let mut name = content
857− .strip_prefix("@manifest")
858− .unwrap_or("")
859− .trim()
860− .to_string();
861− if name.is_empty() {
862− name = ctx.page_title.clone().unwrap_or_else(|| "App".to_string());
863− }
864− let name = substitute_vars(&name, &ctx.variables);
865− let mut manifest = crate::ast::ManifestConfig {
866− name: name.clone(),
867− short_name: None,
868− start_url: "/".to_string(),
869− display: "standalone".to_string(),
870− background_color: None,
871− theme_color: None,
872− description: None,
873− icons: Vec::new(),
874− };
875− while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
876− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
877− let trimmed = s.trim();
878− if let Some((key, value)) = trimmed.split_once(' ') {
879− let value = substitute_vars(value.trim(), &ctx.variables);
880− match key.trim() {
881− "short_name" | "short-name" => manifest.short_name = Some(value),
882− "start_url" | "start-url" => manifest.start_url = value,
883− "display" => manifest.display = value,
884− "background_color" | "background-color" => {
885− manifest.background_color = Some(value)
886− }
887− "theme_color" | "theme-color" => manifest.theme_color = Some(value),
888− "description" => manifest.description = Some(value),
889− "icon" => {
890− // icon src sizes (e.g., icon /icon-192.png 192x192)
891− let parts: Vec<&str> = value.splitn(2, ' ').collect();
892− if parts.len() == 2 {
893− manifest
894− .icons
895− .push((parts[0].to_string(), parts[1].to_string()));
896− } else {
897− manifest.icons.push((value, "192x192".to_string()));
898− }
899− }
900− _ => {}
901− }
902− }
903− }
904− self.pos += 1;
905− }
906− ctx.manifest = Some(manifest);
907− return Ok(None);
908− }
909821
910822
911823 // --- @assert directive (compile-time assertions) ---
@@ -1662,94 +1574,6 @@ impl Parser {
16621574 }
16631575
16641576
1665− // --- @match ---
1666−
1667− if let Some(rest) = content.strip_prefix("@match ") {
1668− let match_val = substitute_vars(rest.trim(), &ctx.variables);
1669− track_var_refs(rest.trim(), &mut ctx.used_variables);
1670−
1671− // Collect all child lines
1672− let mut match_lines = Vec::new();
1673− while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
1674− match_lines.push(self.lines[self.pos].clone());
1675− self.pos += 1;
1676− }
1677−
1678− if match_lines.is_empty() {
1679− return Ok(None);
1680− }
1681−
1682− let case_indent = match_lines[0].indent;
1683−
1684− // Group into cases: (Some(value), body) or (None, body) for @default
1685− let mut cases: Vec<(Option<String>, Vec<Line>)> = Vec::new();
1686− let mut mi = 0;
1687− while mi < match_lines.len() {
1688− if match_lines[mi].indent == case_indent {
1689− if let LineContent::Normal(ref s) = match_lines[mi].content {
1690− let trimmed = s.trim();
1691− if let Some(case_val) = trimmed.strip_prefix("@case ") {
1692− let case_val = substitute_vars(case_val.trim(), &ctx.variables);
1693− mi += 1;
1694− let mut body = Vec::new();
1695− while mi < match_lines.len() && match_lines[mi].indent > case_indent {
1696− body.push(match_lines[mi].clone());
1697− mi += 1;
1698− }
1699− cases.push((Some(case_val), body));
1700− } else if trimmed == "@default" {
1701− mi += 1;
1702− let mut body = Vec::new();
1703− while mi < match_lines.len() && match_lines[mi].indent > case_indent {
1704− body.push(match_lines[mi].clone());
1705− mi += 1;
1706− }
1707− cases.push((None, body));
1708− } else {
1709− mi += 1;
1710− }
1711− } else {
1712− mi += 1;
1713− }
1714− } else {
1715− mi += 1;
1716− }
1717− }
1718−
1719− // Find first matching case or @default
1720− let body_lines = cases
1721− .into_iter()
1722− .find(|(case_val, _)| match case_val {
1723− Some(v) => *v == match_val,
1724− None => true,
1725− })
1726− .map(|(_, lines)| lines)
1727− .unwrap_or_default();
1728−
1729− if body_lines.is_empty() {
1730− return Ok(None);
1731− }
1732−
1733− let min_indent = body_lines.iter().map(|l| l.indent).min().unwrap_or(0);
1734− let adjusted: Vec<Line> = body_lines
1735− .iter()
1736− .map(|l| Line {
1737− indent: l.indent - min_indent,
1738− content: l.content.clone(),
1739− line_num: l.line_num,
1740− })
1741− .collect();
1742−
1743− let saved_vars = ctx.variables.clone();
1744− let mut body_parser = Parser {
1745− lines: adjusted,
1746− pos: 0,
1747− };
1748− let nodes = body_parser.parse_children(0, ctx);
1749− ctx.variables = saved_vars;
1750− return Ok(Some(nodes));
1751− }
1752−
17531577
17541578 // --- @warn / @debug ---
17551579
@@ -1794,85 +1618,6 @@ impl Parser {
17941618 return Ok(None);
17951619 }
17961620
1797− // --- @theme directive ---
1798−
1799− if content.trim() == "@theme" {
1800− let mut tokens = Vec::new();
1801− while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
1802− if let LineContent::Normal(ref s) = self.lines[self.pos].content {
1803− let trimmed = s.trim();
1804− if let Some((name, value)) = trimmed.split_once(' ') {
1805− let name = name.trim().to_string();
1806− let value = value.trim().to_string();
1807− tokens.push((name.clone(), value.clone()));
1808− // Set as regular variable
1809− ctx.variables.insert(name.clone(), value.clone());
1810− // Also set as CSS custom property
1811− let css_name = format!("--{}", name);
1812− ctx.css_vars.push((css_name, value));
1813− }
1814− }
1815− self.pos += 1;
1816− }
1817− ctx.theme_tokens = tokens;
1818− return Ok(None);
1819− }
1820−
1821−
1822− // --- @font-face directive ---
1823−
1824− if let Some(rest) = content.strip_prefix("@font-face ") {
1825− let rest = rest.trim();
1826− if let Some((name, url)) = rest.split_once(' ') {
1827− ctx.font_faces.push((
1828− substitute_vars(name.trim(), &ctx.variables),
1829− substitute_vars(url.trim(), &ctx.variables),
1830− ));
1831− }
1832− return Ok(None);
1833− }
1834−
1835− // --- @json-ld block ---
1836−
1837− if content.trim() == "@json-ld" {
1838− let mut block = String::new();
1839− while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
1840− match &self.lines[self.pos].content {
1841− LineContent::Normal(s) => {
1842− block.push_str(s.trim());
1843− block.push('\n');
1844− }
1845− LineContent::Raw(s) => {
1846− block.push_str(s);
1847− block.push('\n');
1848− }
1849− }
1850− self.pos += 1;
1851− }
1852− let trimmed = block.trim().to_string();
1853− if !trimmed.is_empty() {
1854− ctx.json_ld_blocks.push(trimmed);
1855− }
1856− return Ok(None);
1857− }
1858−
1859−
1860− // --- @deprecated annotation ---
1861−
1862− if let Some(rest) = content.strip_prefix("@deprecated ") {
1863− let message = rest.trim().to_string();
1864− // Peek at the next line to get the function name
1865− if self.pos < self.lines.len()
1866− && let LineContent::Normal(ref s) = self.lines[self.pos].content
1867− && let Some(fn_rest) = s.trim().strip_prefix("@let ")
1868− {
1869− let parts: Vec<&str> = fn_rest.split_whitespace().collect();
1870− if let Some(&fn_name) = parts.first() {
1871− ctx.deprecated_fns.insert(fn_name.to_string(), message);
1872− }
1873− }
1874− return Ok(None);
1875− }
18761621
18771622 // --- @extends (template inheritance) ---
18781623
@@ -2014,16 +1759,6 @@ impl Parser {
20141759 let name = extract_element_name(&content);
20151760 if ctx.functions.contains_key(name) {
20161761 ctx.used_functions.insert(name.to_string());
2017− // Emit deprecation warning if function is marked @deprecated
2018− if let Some(msg) = ctx.deprecated_fns.get(name) {
2019− ctx.diagnostics.push(Diagnostic {
2020− line: line_num,
2021− column: None,
2022− message: format!("@{} is deprecated: {}", name, msg),
2023− severity: Severity::Warning,
2024− source_line: Some(content.clone()),
2025− });
2026− }
20271762 let nodes = self.expand_fn_call(name, &content, current_indent, line_num, ctx)?;
20281763 return Ok(Some(nodes));
20291764 }
@@ -2529,19 +2264,10 @@ const KNOWN_DIRECTIVES: &[&str] = &[
25292264 "meta",
25302265 "head",
25312266 "style",
2532− "match",
2533− "case",
2534− "default",
25352267 "warn",
2536− "breakpoint",
2537− "theme",
2538− "deprecated",
25392268 "extends",
2540− "font-face",
2541− "json-ld",
25422269 "assert",
25432270 "markdown",
2544− "manifest",
25452271 "data",
25462272 "svg",
25472273];
@@ -2604,6 +2330,21 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
26042330 ("@base", "use `@page [base ...] Title`"),
26052331 ("@og", "use `@meta og:NAME VALUE`"),
26062332 ("@debug", "use `@warn`"),
2333+ ("@match", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2334+ ("@case", "use `@if $x == a` / `@else if $x == b` / `@else`"),
2335+ ("@default", "use `@else` in an `@if` chain"),
2336+ ("@theme", "use a `@let --name value` line per token"),
2337+ (
2338+ "@json-ld",
2339+ "put a `<script type=\"application/ld+json\">` in `@head`",
2340+ ),
2341+ ("@font-face", "write the `@font-face` rule in an `@style` block"),
2342+ (
2343+ "@manifest",
2344+ "write a manifest.json file and link it from `@head`",
2345+ ),
2346+ ("@breakpoint", "write the media query in an `@style` block"),
2347+ ("@deprecated", "remove it"),
26072348 (
26082349 "@collection",
26092350 "use `@data $name dir/*.json` (each file becomes `$name.STEM.key`)",