@@ -15,21 +15,30 @@ impl fmt::Display for ParseError {
1515 }
1616}
1717
18+#[derive(Clone)]
1819enum LineContent {
1920 Normal(String),
2021 Raw(String),
2122}
2223
24+#[derive(Clone)]
2325struct Line {
2426 indent: usize,
2527 content: LineContent,
2628 line_num: usize,
2729}
2830
31+#[derive(Clone)]
32+struct FnDef {
33+ params: Vec<String>,
34+ body_lines: Vec<Line>,
35+}
36+
2937struct ParseContext {
3038 page_title: Option<String>,
3139 variables: HashMap<String, String>,
3240 defines: HashMap<String, Vec<Attribute>>,
41+ functions: HashMap<String, FnDef>,
3342}
3443
3544struct Parser {
@@ -44,6 +53,7 @@ pub fn parse(input: &str) -> Result<Document, ParseError> {
4453 page_title: None,
4554 variables: HashMap::new(),
4655 defines: HashMap::new(),
56+ functions: HashMap::new(),
4757 };
4858 let nodes = parser.parse_children(0, &mut ctx)?;
4959 Ok(Document {
@@ -150,15 +160,18 @@ impl Parser {
150160 }
151161
152162 match self.parse_line(ctx)? {
153− Some(node) => nodes.push(node),
154− None => {} // directive consumed, no node
163+ Some(new_nodes) => nodes.extend(new_nodes),
164+ None => {}
155165 }
156166 }
157167
158168 Ok(nodes)
159169 }
160170
161− fn parse_line(&mut self, ctx: &mut ParseContext) -> Result<Option<Node>, ParseError> {
171+ fn parse_line(
172+ &mut self,
173+ ctx: &mut ParseContext,
174+ ) -> Result<Option<Vec<Node>>, ParseError> {
162175 let line_num = self.lines[self.pos].line_num;
163176 let current_indent = self.lines[self.pos].indent;
164177
@@ -166,7 +179,7 @@ impl Parser {
166179 if let LineContent::Raw(s) = &self.lines[self.pos].content {
167180 let content = s.clone();
168181 self.pos += 1;
169− return Ok(Some(Node::Raw(content)));
182+ return Ok(Some(vec![Node::Raw(content)]));
170183 }
171184
172185 // Normal content — clone to release borrow
@@ -208,17 +221,123 @@ impl Parser {
208221 return Ok(None);
209222 }
210223
224+ // --- Function definition ---
225+
226+ if let Some(rest) = content.strip_prefix("@fn ") {
227+ let parts: Vec<&str> = rest.split_whitespace().collect();
228+ if parts.is_empty() {
229+ return Err(ParseError {
230+ line: line_num,
231+ message: "@fn requires a name".to_string(),
232+ });
233+ }
234+ let name = parts[0].to_string();
235+ let params: Vec<String> = parts[1..]
236+ .iter()
237+ .map(|p| p.strip_prefix('$').unwrap_or(p).to_string())
238+ .collect();
239+
240+ // Collect body lines (all lines indented deeper than @fn)
241+ let mut body_lines = Vec::new();
242+ while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
243+ body_lines.push(self.lines[self.pos].clone());
244+ self.pos += 1;
245+ }
246+
247+ ctx.functions.insert(name, FnDef { params, body_lines });
248+ return Ok(None);
249+ }
250+
251+ // --- Function call ---
252+
253+ if content.starts_with('@') {
254+ let name = extract_element_name(&content);
255+ if ctx.functions.contains_key(name) {
256+ let nodes =
257+ self.expand_fn_call(name, &content, current_indent, line_num, ctx)?;
258+ return Ok(Some(nodes));
259+ }
260+ }
261+
211262 // --- Elements ---
212263
213264 if content.starts_with('@') || content.starts_with('[') {
214265 let node = self.parse_element_line(&content, current_indent, line_num, ctx)?;
215− return Ok(Some(node));
266+ return Ok(Some(vec![node]));
216267 }
217268
218269 // --- Bare text ---
219270
220271 let segments = parse_text_segments(&content, ctx);
221− Ok(Some(Node::Text(segments)))
272+ Ok(Some(vec![Node::Text(segments)]))
273+ }
274+
275+ fn expand_fn_call(
276+ &mut self,
277+ name: &str,
278+ content: &str,
279+ current_indent: usize,
280+ line_num: usize,
281+ ctx: &mut ParseContext,
282+ ) -> Result<Vec<Node>, ParseError> {
283+ // Parse [param value, ...] arguments
284+ let rest = &content[1 + name.len()..];
285+ let rest = rest.trim_start();
286+
287+ let args = if rest.starts_with('[') {
288+ let (attrs, _) = parse_attr_brackets(rest, line_num, ctx)?;
289+ attrs
290+ } else {
291+ Vec::new()
292+ };
293+
294+ // Clone function definition (releases borrow on ctx)
295+ let fn_def = ctx.functions.get(name).unwrap().clone();
296+
297+ // Parse caller's children
298+ let caller_children = self.parse_children(current_indent + 1, ctx)?;
299+
300+ // Save variable state, inject function parameters
301+ let saved_vars = ctx.variables.clone();
302+ for (i, param) in fn_def.params.iter().enumerate() {
303+ let value = args
304+ .iter()
305+ .find(|a| a.key == *param)
306+ .and_then(|a| a.value.clone())
307+ .or_else(|| args.get(i).and_then(|a| a.value.clone()))
308+ .unwrap_or_default();
309+ ctx.variables.insert(param.clone(), value);
310+ }
311+
312+ // Normalize body indentation so it parses from indent 0
313+ let min_indent = fn_def
314+ .body_lines
315+ .iter()
316+ .map(|l| l.indent)
317+ .min()
318+ .unwrap_or(0);
319+ let adjusted: Vec<Line> = fn_def
320+ .body_lines
321+ .iter()
322+ .map(|l| Line {
323+ indent: l.indent - min_indent,
324+ content: l.content.clone(),
325+ line_num: l.line_num,
326+ })
327+ .collect();
328+
329+ // Parse body with params in scope
330+ let mut body_parser = Parser {
331+ lines: adjusted,
332+ pos: 0,
333+ };
334+ let body_nodes = body_parser.parse_children(0, ctx)?;
335+
336+ // Restore variables
337+ ctx.variables = saved_vars;
338+
339+ // Replace @children with caller's children
340+ Ok(replace_children_nodes(body_nodes, &caller_children))
222341 }
223342
224343 fn parse_element_line(
@@ -251,10 +370,39 @@ impl Parser {
251370 }
252371}
253372
373+// ---------------------------------------------------------------------------
374+// @children replacement
375+// ---------------------------------------------------------------------------
376+
377+fn replace_children_nodes(nodes: Vec<Node>, caller_children: &[Node]) -> Vec<Node> {
378+ let mut result = Vec::new();
379+ for node in nodes {
380+ match node {
381+ Node::Element(elem) if elem.kind == ElementKind::Children => {
382+ result.extend(caller_children.iter().cloned());
383+ }
384+ Node::Element(mut elem) => {
385+ elem.children = replace_children_nodes(elem.children, caller_children);
386+ result.push(Node::Element(elem));
387+ }
388+ other => result.push(other),
389+ }
390+ }
391+ result
392+}
393+
254394// ---------------------------------------------------------------------------
255395// Element parsing
256396// ---------------------------------------------------------------------------
257397
398+fn extract_element_name(content: &str) -> &str {
399+ let without_at = &content[1..];
400+ match without_at.find(|c: char| c == ' ' || c == '[') {
401+ Some(i) => &without_at[..i],
402+ None => without_at,
403+ }
404+}
405+
258406fn parse_single_element(
259407 content: &str,
260408 line_num: usize,
@@ -329,6 +477,7 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
329477 "paragraph" | "p" => Ok(ElementKind::Paragraph),
330478 "image" | "img" => Ok(ElementKind::Image),
331479 "link" => Ok(ElementKind::Link),
480+ "children" => Ok(ElementKind::Children),
332481 _ => Err(ParseError {
333482 line: line_num,
334483 message: format!("unknown element @{}", s),
@@ -560,7 +709,9 @@ fn substitute_vars(input: &str, vars: &HashMap<String, String>) -> String {
560709 let mut i = 0;
561710
562711 while i < chars.len() {
563− if chars[i] == '$' && i + 1 < chars.len() && (chars[i + 1].is_alphanumeric() || chars[i + 1] == '_')
712+ if chars[i] == '$'
713+ && i + 1 < chars.len()
714+ && (chars[i + 1].is_alphanumeric() || chars[i + 1] == '_')
564715 {
565716 let start = i + 1;
566717 let mut end = start;