@@ -1,12 +1,36 @@
11use std::collections::HashMap;
22use std::fmt;
3+use std::path::{Path, PathBuf};
34
45use crate::ast::*;
56
6−#[derive(Debug)]
7−pub struct ParseError {
7+// ---------------------------------------------------------------------------
8+// Public types
9+// ---------------------------------------------------------------------------
10+
11+#[derive(Debug, Clone, PartialEq)]
12+pub enum Severity {
13+ Error,
14+ Warning,
15+}
16+
17+#[derive(Debug, Clone)]
18+pub struct Diagnostic {
819 pub line: usize,
920 pub message: String,
21+ pub severity: Severity,
22+}
23+
24+pub struct ParseResult {
25+ pub document: Document,
26+ pub diagnostics: Vec<Diagnostic>,
27+ pub included_files: Vec<PathBuf>,
28+}
29+
30+#[derive(Debug)]
31+struct ParseError {
32+ line: usize,
33+ message: String,
1034}
1135
1236impl fmt::Display for ParseError {
@@ -15,6 +39,10 @@ impl fmt::Display for ParseError {
1539 }
1640}
1741
42+// ---------------------------------------------------------------------------
43+// Internal types
44+// ---------------------------------------------------------------------------
45+
1846#[derive(Clone)]
1947enum LineContent {
2048 Normal(String),
@@ -39,6 +67,10 @@ struct ParseContext {
3967 variables: HashMap<String, String>,
4068 defines: HashMap<String, Vec<Attribute>>,
4169 functions: HashMap<String, FnDef>,
70+ diagnostics: Vec<Diagnostic>,
71+ base_path: Option<PathBuf>,
72+ included_files: Vec<PathBuf>,
73+ include_stack: Vec<PathBuf>,
4274}
4375
4476struct Parser {
@@ -46,7 +78,11 @@ struct Parser {
4678 pos: usize,
4779}
4880
49−pub fn parse(input: &str) -> Result<Document, ParseError> {
81+pub fn parse(input: &str) -> ParseResult {
82+ parse_with_base(input, None)
83+}
84+
85+pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
5086 let lines = preprocess(input);
5187 let mut parser = Parser { lines, pos: 0 };
5288 let mut ctx = ParseContext {
@@ -54,14 +90,22 @@ pub fn parse(input: &str) -> Result<Document, ParseError> {
5490 variables: HashMap::new(),
5591 defines: HashMap::new(),
5692 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(),
5797 };
58− let nodes = parser.parse_children(0, &mut ctx)?;
59− Ok(Document {
60− page_title: ctx.page_title,
61− variables: ctx.variables,
62− defines: ctx.defines,
63− nodes,
64− })
98+ let nodes = parser.parse_children(0, &mut ctx);
99+ ParseResult {
100+ document: Document {
101+ page_title: ctx.page_title,
102+ variables: ctx.variables,
103+ defines: ctx.defines,
104+ nodes,
105+ },
106+ diagnostics: ctx.diagnostics,
107+ included_files: ctx.included_files,
108+ }
65109}
66110
67111// ---------------------------------------------------------------------------
@@ -130,10 +174,27 @@ fn preprocess(input: &str) -> Vec<Line> {
130174 }
131175 }
132176
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() {
183+ i += 1;
184+ let next = raw_lines[i].trim();
185+ if next.is_empty() || next.starts_with("--") {
186+ continue;
187+ }
188+ full.push(' ');
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;
192+ }
193+
133194 lines.push(Line {
134195 indent,
135− content: LineContent::Normal(trimmed.to_string()),
136− line_num: i + 1,
196+ content: LineContent::Normal(full),
197+ line_num: first_line_num,
137198 });
138199 i += 1;
139200 }
@@ -150,7 +211,7 @@ impl Parser {
150211 &mut self,
151212 min_indent: usize,
152213 ctx: &mut ParseContext,
153− ) -> Result<Vec<Node>, ParseError> {
214+ ) -> Vec<Node> {
154215 let mut nodes = Vec::new();
155216
156217 while self.pos < self.lines.len() {
@@ -159,13 +220,26 @@ impl Parser {
159220 break;
160221 }
161222
162− match self.parse_line(ctx)? {
163− Some(new_nodes) => nodes.extend(new_nodes),
164− None => {}
223+ match self.parse_line(ctx) {
224+ Ok(Some(new_nodes)) => nodes.extend(new_nodes),
225+ Ok(None) => {}
226+ Err(e) => {
227+ ctx.diagnostics.push(Diagnostic {
228+ line: e.line,
229+ message: e.message,
230+ severity: Severity::Error,
231+ });
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
235+ {
236+ self.pos += 1;
237+ }
238+ }
165239 }
166240 }
167241
168− Ok(nodes)
242+ nodes
169243 }
170244
171245 fn parse_line(
@@ -216,9 +290,50 @@ impl Parser {
216290 return Ok(None);
217291 }
218292
219− if content.starts_with("@include ") {
220− // TODO: implement file includes
221− return Ok(None);
293+ if let Some(rest) = content.strip_prefix("@include ") {
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),
298+ };
299+
300+ // Circular include check
301+ if ctx.include_stack.contains(&resolved) {
302+ ctx.diagnostics.push(Diagnostic {
303+ line: line_num,
304+ message: format!("circular include '{}'", filename),
305+ severity: Severity::Error,
306+ });
307+ return Ok(None);
308+ }
309+
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());
316+
317+ let included_lines = preprocess(&included_text);
318+ let mut included_parser = Parser {
319+ lines: included_lines,
320+ pos: 0,
321+ };
322+ let nodes = included_parser.parse_children(0, ctx);
323+
324+ ctx.base_path = saved_base;
325+ ctx.include_stack.pop();
326+ return Ok(Some(nodes));
327+ }
328+ Err(e) => {
329+ ctx.diagnostics.push(Diagnostic {
330+ line: line_num,
331+ message: format!("cannot include '{}': {}", filename, e),
332+ severity: Severity::Error,
333+ });
334+ return Ok(None);
335+ }
336+ }
222337 }
223338
224339 // --- Function definition ---
@@ -285,7 +400,7 @@ impl Parser {
285400 let rest = rest.trim_start();
286401
287402 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)?;
289404 attrs
290405 } else {
291406 Vec::new()
@@ -295,7 +410,7 @@ impl Parser {
295410 let fn_def = ctx.functions.get(name).unwrap().clone();
296411
297412 // 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);
299414
300415 // Save variable state, inject function parameters
301416 let saved_vars = ctx.variables.clone();
@@ -331,7 +446,7 @@ impl Parser {
331446 lines: adjusted,
332447 pos: 0,
333448 };
334− let body_nodes = body_parser.parse_children(0, ctx)?;
449+ let body_nodes = body_parser.parse_children(0, ctx);
335450
336451 // Restore variables
337452 ctx.variables = saved_vars;
@@ -357,7 +472,7 @@ impl Parser {
357472 }
358473
359474 // 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);
361476
362477 // Build chain right-to-left: rightmost gets children, each wraps the next
363478 let mut current_children = children;
@@ -406,7 +521,7 @@ fn extract_element_name(content: &str) -> &str {
406521fn parse_single_element(
407522 content: &str,
408523 line_num: usize,
409− ctx: &ParseContext,
524+ ctx: &mut ParseContext,
410525) -> Result<Element, ParseError> {
411526 let (kind, rest) = if content.starts_with('[') {
412527 // Implicit @el
@@ -489,10 +604,39 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
489604// Attribute parsing
490605// ---------------------------------------------------------------------------
491606
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",
616+ "id", "class",
617+];
618+
492619fn parse_attr_brackets(
493620 input: &str,
494621 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)
625+}
626+
627+fn parse_attr_brackets_no_validate(
628+ input: &str,
629+ line_num: usize,
630+ ctx: &mut ParseContext,
631+) -> Result<(Vec<Attribute>, String), ParseError> {
632+ parse_attr_brackets_inner(input, line_num, ctx, false)
633+}
634+
635+fn parse_attr_brackets_inner(
636+ input: &str,
637+ line_num: usize,
638+ ctx: &mut ParseContext,
639+ validate: bool,
496640) -> Result<(Vec<Attribute>, String), ParseError> {
497641 let mut depth = 0;
498642 let mut end_pos = 0;
@@ -520,12 +664,12 @@ fn parse_attr_brackets(
520664
521665 let attrs_inner = &input[1..end_pos];
522666 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);
524668
525669 Ok((attrs, remaining))
526670}
527671
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> {
529673 let mut attrs = Vec::new();
530674
531675 for part in split_commas(input) {
@@ -546,17 +690,36 @@ fn parse_attr_list(input: &str, ctx: &ParseContext) -> Vec<Attribute> {
546690 // Substitute variables in value
547691 let part = substitute_vars(part, &ctx.variables);
548692
549− if let Some((key, value)) = part.split_once(' ') {
550− attrs.push(Attribute {
693+ let attr = if let Some((key, value)) = part.split_once(' ') {
694+ Attribute {
551695 key: key.trim().to_string(),
552696 value: Some(value.trim().to_string()),
553− });
697+ }
554698 } else {
555− attrs.push(Attribute {
699+ Attribute {
556700 key: part.to_string(),
557701 value: None,
558− });
702+ }
703+ };
704+
705+ // Warn on unknown attributes
706+ if validate {
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 {
715+ line: line_num,
716+ message: format!("unknown attribute '{}'", attr.key),
717+ severity: Severity::Warning,
718+ });
719+ }
559720 }
721+
722+ attrs.push(attr);
560723 }
561724
562725 attrs
@@ -569,8 +732,8 @@ fn split_commas(input: &str) -> Vec<&str> {
569732
570733 for (i, c) in input.char_indices() {
571734 match c {
572− '[' => depth += 1,
573− ']' => depth -= 1,
735+ '[' | '(' => depth += 1,
736+ ']' | ')' => depth -= 1,
574737 ',' if depth == 0 => {
575738 parts.push(&input[start..i]);
576739 start = i + 1;
@@ -630,7 +793,7 @@ fn split_chain(content: &str) -> Vec<String> {
630793// Text segment parsing (inline {...} elements)
631794// ---------------------------------------------------------------------------
632795
633−fn parse_text_segments(input: &str, ctx: &ParseContext) -> Vec<TextSegment> {
796+fn parse_text_segments(input: &str, ctx: &mut ParseContext) -> Vec<TextSegment> {
634797 let mut segments = Vec::new();
635798 let mut current_text = String::new();
636799 let chars: Vec<char> = input.chars().collect();