
Template.pudu
Pudu161 lines6.0 KB
1/** @Docgen.Template.Module — logic-less page templates filled from metadata */2module PuduLangDocgen.Template34import Std.Map as Map5import PuduLangDocgen as Docgen6import PuduLangDocgen.Meta as Meta7import PuduLangDocgen.Paths as Paths89/** @Docgen.Template.Node — one piece of a parsed template */10export type Node = Literal(Str) | Value(Str, Bool) | Section(Str, Bool, Array[Node]) | Partial(Str)111213const MAX_PARTIALS: Int = 161415161718export fn parse(text: Str) -> Result[Array[Node], Str] {19 let (nodes, _rest, _closed) = sequence(text, "") ?20 Ok(nodes)21}222324export fn render(nodes: &Array[Node], context: &Docgen.Meta, partials: &Map[Str, Array[Node]]) -> Str {25 fill(nodes, &[*context], partials, 0)26}272829fn sequence(text: Str, open: Str) -> Result[(Array[Node], Str, Str), Str] {30 var nodes: Array[Node] = []31 var rest = text32 while !rest.isEmpty() {33 let at = rest.indexOf("\{\{")34 if at < 0 {35 nodes = nodes.push(Literal(rest))36 rest = ""37 break38 }39 if at > 0 { nodes = nodes.push(Literal(rest.take(at))) }40 let triple = rest.drop(at).startsWith("\{\{\{")41 let closer = if triple { "\}\}\}" } else { "\}\}" }42 let start = at + (if triple { 3 } else { 2 })43 let end = rest.drop(start).indexOf(closer)44 if end < 0 { return Err("tag is not closed") }45 let tag = rest.drop(start).take(end).trim()46 rest = rest.drop(start + end + closer.length())47 if triple { nodes = nodes.push(Value(tag, false)) }48 else if tag.startsWith("!") {}49 else if tag.startsWith("&") { nodes = nodes.push(Value(tag.drop(1).trim(), false)) }50 else if tag.startsWith(">") { nodes = nodes.push(Partial(tag.drop(1).trim())) }51 else if tag.startsWith("#") || tag.startsWith("^") {52 let name = tag.drop(1).trim()53 let (children, after, closed) = sequence(rest, name) ?54 if closed != name { return Err("section " + name + " is not closed") }55 nodes = nodes.push(Section(name, tag.startsWith("^"), children))56 rest = after57 } else if tag.startsWith("/") {58 let name = tag.drop(1).trim()59 if open.isEmpty() { return Err("section " + name + " is closed without being opened") }60 if name != open { return Err("section " + open + " is closed by " + name) }61 return Ok((nodes, rest, name))62 } else { nodes = nodes.push(Value(tag, true)) }63 }64 if !open.isEmpty() { return Err("section " + open + " is not closed") }65 Ok((nodes, "", ""))66}676869fn fill(nodes: &Array[Node], stack: &Array[Docgen.Meta], partials: &Map[Str, Array[Node]], depth: Int) -> Str {70 var pieces: Array[Str] = []71 for node in *nodes {72 match node {73 case Literal(text) => { pieces = pieces.push(text) }74 case Value(name, escaped) => {75 let text = scalar(&lookup(stack, name))76 pieces = pieces.push(if escaped { Paths.escape(text) } else { text })77 }78 case Section(name, inverted, children) => {79 let value = lookup(stack, name)80 if inverted {81 if !truthy(&value) { pieces = pieces.push(fill(&children, stack, partials, depth)) }82 } else {83 match value {84 case Docgen.Items(listed) => {85 for item in listed { pieces = pieces.push(fill(&children, &stack.push(item), partials, depth)) }86 }87 case Docgen.Fields(_fields) => { pieces = pieces.push(fill(&children, &stack.push(value), partials, depth)) }88 case other => { if truthy(&other) { pieces = pieces.push(fill(&children, stack, partials, depth)) } }89 }90 }91 }92 case Partial(name) => {93 if depth < MAX_PARTIALS {94 match Map.get(partials, name) {95 case Some(found) => { pieces = pieces.push(fill(&found, stack, partials, depth + 1)) }96 case None => {}97 }98 }99 }100 }101 }102 pieces.join("")103}104105106fn lookup(stack: &Array[Docgen.Meta], name: Str) -> Docgen.Meta {107 if name == "." { return if stack.isEmpty() { Docgen.Nothing } else { stack[stack.length() - 1] } }108 let parts = name.split(".")109 var index = stack.length() - 1110 while index >= 0 {111 match stack[index] {112 case Docgen.Fields(fields) => {113 match Meta.get(&fields, parts[0]) {114 case Some(found) => { return descend(found, &parts.slice(1, parts.length())) }115 case None => {}116 }117 }118 case _ => {}119 }120 index = index - 1121 }122 Docgen.Nothing123}124125126fn descend(value: Docgen.Meta, parts: &Array[Str]) -> Docgen.Meta {127 var current = value128 for part in *parts {129 current = match current {130 case Docgen.Fields(fields) => match Meta.get(&fields, part) {131 case Some(found) => found132 case None => Docgen.Nothing133 }134 case _ => Docgen.Nothing135 }136 }137 current138}139140141fn truthy(value: &Docgen.Meta) -> Bool {142 match value {143 case Docgen.Nothing => false144 case Docgen.Flag(held) => held145 case Docgen.Text(written) => !written.isEmpty()146 case Docgen.Whole(count) => count != 0147 case Docgen.Items(listed) => !listed.isEmpty()148 case Docgen.Fields(_fields) => true149 }150}151152153fn scalar(value: &Docgen.Meta) -> Str {154 match value {155 case Docgen.Text(written) => written156 case Docgen.Whole(count) => show(count)157 case Docgen.Flag(held) => if held { "true" } else { "false" }158 case _ => ""159 }160}161