@@ -85,7 +85,6 @@ struct ParseContext {
85 css_vars: Vec<(String, String)>,
85 css_vars: Vec<(String, String)>,
86 custom_css: Vec<String>,
86 custom_css: Vec<String>,
87 og_tags: Vec<(String, String)>,
87 og_tags: Vec<(String, String)>,
88− custom_breakpoints: Vec<(String, String)>,
89 diagnostics: Vec<Diagnostic>,
88 diagnostics: Vec<Diagnostic>,
90 base_path: Option<PathBuf>,
89 base_path: Option<PathBuf>,
91 included_files: Vec<PathBuf>,
90 included_files: Vec<PathBuf>,
@@ -104,20 +103,10 @@ struct ParseContext {
104 fn_lines: HashMap<String, usize>,
103 fn_lines: HashMap<String, usize>,
105 /// Line numbers of attribute bundle definitions (name -> line)
104 /// Line numbers of attribute bundle definitions (name -> line)
106 define_lines: HashMap<String, usize>,
105 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)>,
111 /// Canonical URL
106 /// Canonical URL
112 canonical: Option<String>,
107 canonical: Option<String>,
113 /// Base URL for relative links
108 /// Base URL for relative links
114 base_url: Option<String>,
109 base_url: Option<String>,
115− /// @font-face declarations: (font_name, url)
116− font_faces: Vec<(String, String)>,
118− json_ld_blocks: Vec<String>,
119− /// @manifest configuration
120− manifest: Option<crate::ast::ManifestConfig>,
121 /// Track @import paths for circular dependency detection
110 /// Track @import paths for circular dependency detection
122 import_stack: Vec<PathBuf>,
111 import_stack: Vec<PathBuf>,
@@ -197,7 +186,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
197 css_vars: Vec::new(),
186 css_vars: Vec::new(),
198 custom_css: Vec::new(),
187 custom_css: Vec::new(),
199 og_tags: Vec::new(),
188 og_tags: Vec::new(),
200− custom_breakpoints: Vec::new(),
201 diagnostics: Vec::new(),
189 diagnostics: Vec::new(),
202 base_path: base_path.map(|p| p.to_path_buf()),
190 base_path: base_path.map(|p| p.to_path_buf()),
203 included_files: Vec::new(),
191 included_files: Vec::new(),
@@ -210,13 +198,8 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
210 let_lines: HashMap::new(),
198 let_lines: HashMap::new(),
211 fn_lines: HashMap::new(),
199 fn_lines: HashMap::new(),
212 define_lines: HashMap::new(),
200 define_lines: HashMap::new(),
213− deprecated_fns: HashMap::new(),
214− theme_tokens: Vec::new(),
215 canonical: None,
201 canonical: None,
216 base_url: None,
202 base_url: None,
217− font_faces: Vec::new(),
218− json_ld_blocks: Vec::new(),
220 import_stack: Vec::new(),
203 import_stack: Vec::new(),
222 load_prelude(&mut ctx);
205 load_prelude(&mut ctx);
@@ -236,13 +219,8 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
236 css_vars: ctx.css_vars,
219 css_vars: ctx.css_vars,
237 custom_css: ctx.custom_css,
220 custom_css: ctx.custom_css,
238 og_tags: ctx.og_tags,
221 og_tags: ctx.og_tags,
239− custom_breakpoints: ctx.custom_breakpoints,
240− theme_tokens: ctx.theme_tokens,
241 canonical: ctx.canonical,
222 canonical: ctx.canonical,
242 base_url: ctx.base_url,
223 base_url: ctx.base_url,
243− font_faces: ctx.font_faces,
244− json_ld_blocks: ctx.json_ld_blocks,
245− manifest: ctx.manifest,
246 preload_hints: collect_image_preload_hints(&nodes),
224 preload_hints: collect_image_preload_hints(&nodes),
@@ -744,17 +722,6 @@ impl Parser {
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),
758 if content == "@head" || content.starts_with("@head ") {
725 if content == "@head" || content.starts_with("@head ") {
759 // Collect indented body lines as raw head content
726 // Collect indented body lines as raw head content
760 let mut head_content = String::new();
727 let mut head_content = String::new();
@@ -851,61 +818,6 @@ impl Parser {
854− // --- @manifest (PWA web manifest) ---
855− if content.trim() == "@manifest" || content.starts_with("@manifest ") {
856− let mut name = content
857− .strip_prefix("@manifest")
861− if name.is_empty() {
862− name = ctx.page_title.clone().unwrap_or_else(|| "App".to_string());
864− let name = substitute_vars(&name, &ctx.variables);
865− let mut manifest = crate::ast::ManifestConfig {
868− start_url: "/".to_string(),
869− display: "standalone".to_string(),
870− background_color: None,
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);
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)
887− "theme_color" | "theme-color" => manifest.theme_color = Some(value),
888− "description" => manifest.description = Some(value),
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 {
895− .push((parts[0].to_string(), parts[1].to_string()));
897− manifest.icons.push((value, "192x192".to_string()));
906− ctx.manifest = Some(manifest);
911 // --- @assert directive (compile-time assertions) ---
823 // --- @assert directive (compile-time assertions) ---
@@ -1662,94 +1574,6 @@ impl Parser {
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);
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());
1678− if match_lines.is_empty() {
1682− let case_indent = match_lines[0].indent;
1684− // Group into cases: (Some(value), body) or (None, body) for @default
1685− let mut cases: Vec<(Option<String>, Vec<Line>)> = Vec::new();
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);
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());
1699− cases.push((Some(case_val), body));
1700− } else if trimmed == "@default" {
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());
1707− cases.push((None, body));
1719− // Find first matching case or @default
1720− let body_lines = cases
1722− .find(|(case_val, _)| match case_val {
1723− Some(v) => *v == match_val,
1726− .map(|(_, lines)| lines)
1727− .unwrap_or_default();
1729− if body_lines.is_empty() {
1733− let min_indent = body_lines.iter().map(|l| l.indent).min().unwrap_or(0);
1734− let adjusted: Vec<Line> = body_lines
1737− indent: l.indent - min_indent,
1738− content: l.content.clone(),
1739− line_num: l.line_num,
1743− let saved_vars = ctx.variables.clone();
1744− let mut body_parser = Parser {
1748− let nodes = body_parser.parse_children(0, ctx);
1749− ctx.variables = saved_vars;
1750− return Ok(Some(nodes));
1754 // --- @warn / @debug ---
1578 // --- @warn / @debug ---
@@ -1794,85 +1618,6 @@ impl Parser {
1794 return Ok(None);
1618 return Ok(None);
1797− // --- @theme directive ---
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));
1817− ctx.theme_tokens = tokens;
1822− // --- @font-face directive ---
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),
1835− // --- @json-ld block ---
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());
1845− LineContent::Raw(s) => {
1852− let trimmed = block.trim().to_string();
1853− if !trimmed.is_empty() {
1854− ctx.json_ld_blocks.push(trimmed);
1860− // --- @deprecated annotation ---
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 ")
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);
1877 // --- @extends (template inheritance) ---
1622 // --- @extends (template inheritance) ---
@@ -2014,16 +1759,6 @@ impl Parser {
2014 let name = extract_element_name(&content);
1759 let name = extract_element_name(&content);
2015 if ctx.functions.contains_key(name) {
1760 if ctx.functions.contains_key(name) {
2016 ctx.used_functions.insert(name.to_string());
1761 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 {
2022− message: format!("@{} is deprecated: {}", name, msg),
2023− severity: Severity::Warning,
2024− source_line: Some(content.clone()),
2027 let nodes = self.expand_fn_call(name, &content, current_indent, line_num, ctx)?;
1762 let nodes = self.expand_fn_call(name, &content, current_indent, line_num, ctx)?;
2028 return Ok(Some(nodes));
1763 return Ok(Some(nodes));
@@ -2529,19 +2264,10 @@ const KNOWN_DIRECTIVES: &[&str] = &[
2531 "style",
2266 "style",
2539 "extends",
2268 "extends",
2542 "assert",
2269 "assert",
2543 "markdown",
2270 "markdown",
@@ -2604,6 +2330,21 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
2604 ("@base", "use `@page [base ...] Title`"),
2330 ("@base", "use `@page [base ...] Title`"),
2605 ("@og", "use `@meta og:NAME VALUE`"),
2331 ("@og", "use `@meta og:NAME VALUE`"),
2606 ("@debug", "use `@warn`"),
2332 ("@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"),
2339+ "put a `<script type=\"application/ld+json\">` in `@head`",
2341+ ("@font-face", "write the `@font-face` rule in an `@style` block"),
2344+ "write a manifest.json file and link it from `@head`",
2346+ ("@breakpoint", "write the media query in an `@style` block"),
2347+ ("@deprecated", "remove it"),
2608 "@collection",
2349 "@collection",
2609 "use `@data $name dir/*.json` (each file becomes `$name.STEM.key`)",
2350 "use `@data $name dir/*.json` (each file becomes `$name.STEM.key`)",