@@ -1,12 +1,36 @@
1use std::collections::HashMap;
1use std::collections::HashMap;
2use std::fmt;
2use std::fmt;
3+use std::path::{Path, PathBuf};
4use crate::ast::*;
5use crate::ast::*;
7+// ---------------------------------------------------------------------------
9+// ---------------------------------------------------------------------------
6−#[derive(Debug)]
11+#[derive(Debug, Clone, PartialEq)]
17+#[derive(Debug, Clone)]
7−pub struct ParseError {
18+pub struct Diagnostic {
8 pub line: usize,
19 pub line: usize,
9 pub message: String,
20 pub message: String,
21+ pub severity: Severity,
24+pub struct ParseResult {
25+ pub document: Document,
26+ pub diagnostics: Vec<Diagnostic>,
27+ pub included_files: Vec<PathBuf>,
12impl fmt::Display for ParseError {
36impl fmt::Display for ParseError {
@@ -15,6 +39,10 @@ impl fmt::Display for ParseError {
42+// ---------------------------------------------------------------------------
44+// ---------------------------------------------------------------------------
18#[derive(Clone)]
46#[derive(Clone)]
19enum LineContent {
47enum LineContent {
20 Normal(String),
48 Normal(String),
@@ -39,6 +67,10 @@ struct ParseContext {
39 variables: HashMap<String, String>,
67 variables: HashMap<String, String>,
40 defines: HashMap<String, Vec<Attribute>>,
68 defines: HashMap<String, Vec<Attribute>>,
41 functions: HashMap<String, FnDef>,
69 functions: HashMap<String, FnDef>,
70+ diagnostics: Vec<Diagnostic>,
71+ base_path: Option<PathBuf>,
72+ included_files: Vec<PathBuf>,
73+ include_stack: Vec<PathBuf>,
44struct Parser {
76struct Parser {
@@ -46,7 +78,11 @@ struct Parser {
46 pos: usize,
78 pos: usize,
49−pub fn parse(input: &str) -> Result<Document, ParseError> {
81+pub fn parse(input: &str) -> ParseResult {
82+ parse_with_base(input, None)
85+pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
50 let lines = preprocess(input);
86 let lines = preprocess(input);
51 let mut parser = Parser { lines, pos: 0 };
87 let mut parser = Parser { lines, pos: 0 };
52 let mut ctx = ParseContext {
88 let mut ctx = ParseContext {
@@ -54,14 +90,22 @@ pub fn parse(input: &str) -> Result<Document, ParseError> {
54 variables: HashMap::new(),
90 variables: HashMap::new(),
55 defines: HashMap::new(),
91 defines: HashMap::new(),
56 functions: HashMap::new(),
92 functions: HashMap::new(),
93+ diagnostics: Vec::new(),
94+ base_path: base_path.map(|p| p.to_path_buf()),
95+ included_files: Vec::new(),
96+ include_stack: Vec::new(),
58− let nodes = parser.parse_children(0, &mut ctx)?;
98+ let nodes = parser.parse_children(0, &mut ctx);
59− Ok(Document {
100+ document: Document {
60− page_title: ctx.page_title,
101+ page_title: ctx.page_title,
61− variables: ctx.variables,
102+ variables: ctx.variables,
62− defines: ctx.defines,
103+ defines: ctx.defines,
106+ diagnostics: ctx.diagnostics,
107+ included_files: ctx.included_files,
67// ---------------------------------------------------------------------------
111// ---------------------------------------------------------------------------
@@ -130,10 +174,27 @@ fn preprocess(input: &str) -> Vec<Line> {
177+ // Join continuation lines for multi-line attribute brackets
178+ let first_line_num = i + 1;
179+ let mut full = trimmed.to_string();
180+ let mut depth: i32 = full.chars().filter(|&c| c == '[').count() as i32
181+ - full.chars().filter(|&c| c == ']').count() as i32;
182+ while depth > 0 && i + 1 < raw_lines.len() {
184+ let next = raw_lines[i].trim();
185+ if next.is_empty() || next.starts_with("--") {
189+ full.push_str(next);
190+ depth += next.chars().filter(|&c| c == '[').count() as i32;
191+ depth -= next.chars().filter(|&c| c == ']').count() as i32;
133 lines.push(Line {
194 lines.push(Line {
135− content: LineContent::Normal(trimmed.to_string()),
196+ content: LineContent::Normal(full),
136− line_num: i + 1,
197+ line_num: first_line_num,
@@ -150,7 +211,7 @@ impl Parser {
150 &mut self,
211 &mut self,
151 min_indent: usize,
212 min_indent: usize,
152 ctx: &mut ParseContext,
213 ctx: &mut ParseContext,
153− ) -> Result<Vec<Node>, ParseError> {
214+ ) -> Vec<Node> {
154 let mut nodes = Vec::new();
215 let mut nodes = Vec::new();
156 while self.pos < self.lines.len() {
217 while self.pos < self.lines.len() {
@@ -159,13 +220,26 @@ impl Parser {
162− match self.parse_line(ctx)? {
223+ match self.parse_line(ctx) {
163− Some(new_nodes) => nodes.extend(new_nodes),
224+ Ok(Some(new_nodes)) => nodes.extend(new_nodes),
164− None => {}
225+ Ok(None) => {}
227+ ctx.diagnostics.push(Diagnostic {
230+ severity: Severity::Error,
232+ // Skip forward past any deeper-indented children of the errored line
233+ while self.pos < self.lines.len()
234+ && self.lines[self.pos].indent > indent
171 fn parse_line(
245 fn parse_line(
@@ -216,9 +290,50 @@ impl Parser {
216 return Ok(None);
290 return Ok(None);
219− if content.starts_with("@include ") {
293+ if let Some(rest) = content.strip_prefix("@include ") {
220− // TODO: implement file includes
294+ let filename = substitute_vars(rest.trim(), &ctx.variables);
295+ let resolved = match &ctx.base_path {
296+ Some(base) => base.join(&filename),
297+ None => PathBuf::from(&filename),
300+ // Circular include check
301+ if ctx.include_stack.contains(&resolved) {
302+ ctx.diagnostics.push(Diagnostic {
304+ message: format!("circular include '{}'", filename),
305+ severity: Severity::Error,
310+ match std::fs::read_to_string(&resolved) {
311+ Ok(included_text) => {
312+ ctx.included_files.push(resolved.clone());
313+ ctx.include_stack.push(resolved.clone());
314+ let saved_base = ctx.base_path.clone();
315+ ctx.base_path = resolved.parent().map(|p| p.to_path_buf());
317+ let included_lines = preprocess(&included_text);
318+ let mut included_parser = Parser {
319+ lines: included_lines,
322+ let nodes = included_parser.parse_children(0, ctx);
324+ ctx.base_path = saved_base;
325+ ctx.include_stack.pop();
326+ return Ok(Some(nodes));
329+ ctx.diagnostics.push(Diagnostic {
331+ message: format!("cannot include '{}': {}", filename, e),
332+ severity: Severity::Error,
221− return Ok(None);
334+ return Ok(None);
224 // --- Function definition ---
339 // --- Function definition ---
@@ -285,7 +400,7 @@ impl Parser {
285 let rest = rest.trim_start();
400 let rest = rest.trim_start();
287 let args = if rest.starts_with('[') {
402 let args = if rest.starts_with('[') {
288− let (attrs, _) = parse_attr_brackets(rest, line_num, ctx)?;
403+ let (attrs, _) = parse_attr_brackets_no_validate(rest, line_num, ctx)?;
291 Vec::new()
406 Vec::new()
@@ -295,7 +410,7 @@ impl Parser {
295 let fn_def = ctx.functions.get(name).unwrap().clone();
410 let fn_def = ctx.functions.get(name).unwrap().clone();
297 // Parse caller's children
412 // Parse caller's children
298− let caller_children = self.parse_children(current_indent + 1, ctx)?;
413+ let caller_children = self.parse_children(current_indent + 1, ctx);
300 // Save variable state, inject function parameters
415 // Save variable state, inject function parameters
301 let saved_vars = ctx.variables.clone();
416 let saved_vars = ctx.variables.clone();
@@ -331,7 +446,7 @@ impl Parser {
331 lines: adjusted,
446 lines: adjusted,
334− let body_nodes = body_parser.parse_children(0, ctx)?;
449+ let body_nodes = body_parser.parse_children(0, ctx);
336 // Restore variables
451 // Restore variables
337 ctx.variables = saved_vars;
452 ctx.variables = saved_vars;
@@ -357,7 +472,7 @@ impl Parser {
359 // Parse indented children (belong to the innermost element)
474 // Parse indented children (belong to the innermost element)
360− let children = self.parse_children(current_indent + 1, ctx)?;
475+ let children = self.parse_children(current_indent + 1, ctx);
362 // Build chain right-to-left: rightmost gets children, each wraps the next
477 // Build chain right-to-left: rightmost gets children, each wraps the next
363 let mut current_children = children;
478 let mut current_children = children;
@@ -406,7 +521,7 @@ fn extract_element_name(content: &str) -> &str {
406fn parse_single_element(
521fn parse_single_element(
407 content: &str,
522 content: &str,
408 line_num: usize,
523 line_num: usize,
409− ctx: &ParseContext,
524+ ctx: &mut ParseContext,
410) -> Result<Element, ParseError> {
525) -> Result<Element, ParseError> {
411 let (kind, rest) = if content.starts_with('[') {
526 let (kind, rest) = if content.starts_with('[') {
412 // Implicit @el
527 // Implicit @el
@@ -489,10 +604,39 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
489// Attribute parsing
604// Attribute parsing
490// ---------------------------------------------------------------------------
605// ---------------------------------------------------------------------------
607+const KNOWN_ATTRS: &[&str] = &[
608+ "spacing", "padding", "padding-x", "padding-y",
609+ "width", "height", "min-width", "max-width", "min-height", "max-height",
610+ "center-x", "center-y", "align-left", "align-right", "align-top", "align-bottom",
611+ "background", "color", "border", "rounded",
612+ "bold", "italic", "underline",
613+ "size", "font", "transition", "cursor", "opacity",
614+ "text-align", "line-height", "overflow", "position", "z-index", "shadow",
615+ "wrap", "gap-x", "gap-y",
492fn parse_attr_brackets(
619fn parse_attr_brackets(
493 input: &str,
620 input: &str,
494 line_num: usize,
621 line_num: usize,
495− ctx: &ParseContext,
622+ ctx: &mut ParseContext,
623+) -> Result<(Vec<Attribute>, String), ParseError> {
624+ parse_attr_brackets_inner(input, line_num, ctx, true)
627+fn parse_attr_brackets_no_validate(
630+ ctx: &mut ParseContext,
631+) -> Result<(Vec<Attribute>, String), ParseError> {
632+ parse_attr_brackets_inner(input, line_num, ctx, false)
635+fn parse_attr_brackets_inner(
638+ ctx: &mut ParseContext,
496) -> Result<(Vec<Attribute>, String), ParseError> {
640) -> Result<(Vec<Attribute>, String), ParseError> {
497 let mut depth = 0;
641 let mut depth = 0;
498 let mut end_pos = 0;
642 let mut end_pos = 0;
@@ -520,12 +664,12 @@ fn parse_attr_brackets(
521 let attrs_inner = &input[1..end_pos];
665 let attrs_inner = &input[1..end_pos];
522 let remaining = input[end_pos + 1..].trim().to_string();
666 let remaining = input[end_pos + 1..].trim().to_string();
523− let attrs = parse_attr_list(attrs_inner, ctx);
667+ let attrs = parse_attr_list(attrs_inner, line_num, ctx, validate);
525 Ok((attrs, remaining))
669 Ok((attrs, remaining))
528−fn parse_attr_list(input: &str, ctx: &ParseContext) -> Vec<Attribute> {
672+fn parse_attr_list(input: &str, line_num: usize, ctx: &mut ParseContext, validate: bool) -> Vec<Attribute> {
529 let mut attrs = Vec::new();
673 let mut attrs = Vec::new();
531 for part in split_commas(input) {
675 for part in split_commas(input) {
@@ -546,17 +690,36 @@ fn parse_attr_list(input: &str, ctx: &ParseContext) -> Vec<Attribute> {
546 // Substitute variables in value
690 // Substitute variables in value
547 let part = substitute_vars(part, &ctx.variables);
691 let part = substitute_vars(part, &ctx.variables);
549− if let Some((key, value)) = part.split_once(' ') {
693+ let attr = if let Some((key, value)) = part.split_once(' ') {
550− attrs.push(Attribute {
694+ Attribute {
551 key: key.trim().to_string(),
695 key: key.trim().to_string(),
552 value: Some(value.trim().to_string()),
696 value: Some(value.trim().to_string()),
555− attrs.push(Attribute {
699+ Attribute {
556 key: part.to_string(),
700 key: part.to_string(),
557 value: None,
701 value: None,
705+ // Warn on unknown attributes
707+ let base_key = attr.key.as_str();
708+ let base_key = base_key
709+ .strip_prefix("hover:")
710+ .or_else(|| base_key.strip_prefix("active:"))
711+ .or_else(|| base_key.strip_prefix("focus:"))
712+ .unwrap_or(base_key);
713+ if !KNOWN_ATTRS.contains(&base_key) {
714+ ctx.diagnostics.push(Diagnostic {
716+ message: format!("unknown attribute '{}'", attr.key),
717+ severity: Severity::Warning,
@@ -569,8 +732,8 @@ fn split_commas(input: &str) -> Vec<&str> {
570 for (i, c) in input.char_indices() {
733 for (i, c) in input.char_indices() {
571 match c {
734 match c {
572− '[' => depth += 1,
735+ '[' | '(' => depth += 1,
573− ']' => depth -= 1,
736+ ']' | ')' => depth -= 1,
574 ',' if depth == 0 => {
737 ',' if depth == 0 => {
575 parts.push(&input[start..i]);
738 parts.push(&input[start..i]);
576 start = i + 1;
739 start = i + 1;
@@ -630,7 +793,7 @@ fn split_chain(content: &str) -> Vec<String> {
630// Text segment parsing (inline {...} elements)
793// Text segment parsing (inline {...} elements)
631// ---------------------------------------------------------------------------
794// ---------------------------------------------------------------------------
633−fn parse_text_segments(input: &str, ctx: &ParseContext) -> Vec<TextSegment> {
796+fn parse_text_segments(input: &str, ctx: &mut ParseContext) -> Vec<TextSegment> {
634 let mut segments = Vec::new();
797 let mut segments = Vec::new();
635 let mut current_text = String::new();
798 let mut current_text = String::new();
636 let chars: Vec<char> = input.chars().collect();
799 let chars: Vec<char> = input.chars().collect();