
Template.pudu
Pudu247 lines10.0 KB
1/** @Log.Expressions.Template.Engine — output templates of expressions and directives */2module PuduLangLog.Expressions.Template34import Std.List as List5import PuduLangLog.Domain.Display as Display6import PuduLangLog.Domain.Json as Json7import PuduLangLog.Domain.Levels as Levels8import PuduLangLog.Expressions.Evaluator as Evaluator9import PuduLangLog.Expressions.Lexer as Lexer10import PuduLangLog.Expressions.Parser as Parser11import PuduLangLog.Expressions.Syntax as Syntax12import PuduLangLog.Expressions.Values as Values13import PuduLangLog as Log1415/** @Log.Expressions.Template.Node — literal text, a hole, or a directive block */16export type Node = TextNode(Str)17 | Hole(Syntax.Expression, Option[Str])18 | Choice(Array[Branch], Array[Node])19 | Each(Str, Option[Str], Syntax.Expression, Array[Node], Array[Node], Array[Node])2021/** @Log.Expressions.Template.Branch — a condition and the nodes it guards */22export type Branch = { condition: Syntax.Expression, body: Array[Node] }2324/** @Log.Expressions.Template.Piece — a scanned part before blocks are nested */25type Piece = Literal(Str) | Placeholder(Str) | Directive(Str)262728export fn parse(text: Str) -> Result[Array[Node], Str] {29 let pieces = scan(text) ?30 let (nodes, next, closer) = block(&pieces, 0) ?31 if closer != "" { return Err("unexpected `\{#" + closer + "\}`") }32 if next < pieces.length() { return Err("unexpected directive") }33 Ok(nodes)34}353637export fn expressionsIn(nodes: &Array[Node]) -> Array[Syntax.Expression] {38 var found: Array[Syntax.Expression] = []39 for node in nodes {40 found = found.concat(match node {41 case TextNode(_) => []42 case Hole(expression, _) => [expression]43 case Choice(branches, otherwise) => {44 var inner: Array[Syntax.Expression] = []45 for branch in branches { inner = inner.push(branch.condition).concat(expressionsIn(&branch.body)) }46 inner.concat(expressionsIn(&otherwise))47 }48 case Each(_, _, source, body, delimiter, empty) => [source].concat(expressionsIn(&body)).concat(expressionsIn(&delimiter)).concat(expressionsIn(&empty))49 })50 }51 found52}535455fn scan(text: Str) -> Result[Array[Piece], Str] {56 let characters = text.chars()57 var pieces: Array[Piece] = []58 var literal: Array[Str] = []59 var index = 060 while index < characters.length() {61 let character = characters[index]62 let doubled = List.get(&characters, index + 1) == Some(character)63 if (character == '\{' || character == '\}') && doubled {64 literal = literal.push(character.toText())65 index = index + 266 } else if character == '\{' {67 if !literal.isEmpty() { pieces = pieces.push(Literal(literal.join(""))) }68 literal = []69 let close = closing(&characters, index + 1) ?70 let content = text.slice(index + 1, close).trim()71 pieces = pieces.push(if content.startsWith("#") { Directive(content.drop(1).trim()) } else { Placeholder(content) })72 index = close + 173 } else {74 literal = literal.push(character.toText())75 index = index + 176 }77 }78 if !literal.isEmpty() { pieces = pieces.push(Literal(literal.join(""))) }79 Ok(pieces)80}818283fn closing(characters: &Array[Char], start: Int) -> Result[Int, Str] {84 var depth = 085 var quoted = false86 var index = start87 while index < characters.length() {88 let character = characters[index]89 if quoted {90 if character == '\'' { quoted = false }91 } else if character == '\'' {92 quoted = true93 } else if character == '\{' || character == '[' {94 depth = depth + 195 } else if character == ']' {96 depth = depth - 197 } else if character == '\}' {98 if depth == 0 { return Ok(index) }99 depth = depth - 1100 }101 index = index + 1102 }103 Err("a hole opened at " + show(start - 1) + " is not closed")104}105106107108fn block(pieces: &Array[Piece], start: Int) -> Result[(Array[Node], Int, Str), Str] {109 var nodes: Array[Node] = []110 var index = start111 while index < pieces.length() {112 match pieces[index] {113 case Literal(text) => {114 nodes = nodes.push(TextNode(text))115 index = index + 1116 }117 case Placeholder(content) => {118 nodes = nodes.push(holeOf(content) ?)119 index = index + 1120 }121 case Directive(content) => {122 if content == "end" || content == "else" || content == "delimit" || content.startsWith("else if ") { return Ok((nodes, index + 1, content)) }123 let (node, next) = if content.startsWith("if ") { choiceAt(pieces, index + 1, content.drop(3)) ? } else if content.startsWith("each ") { eachAt(pieces, index + 1, content.drop(5)) ? } else { return Err("unknown directive `\{#" + content + "\}`") }124 nodes = nodes.push(node)125 index = next126 }127 }128 }129 Ok((nodes, index, ""))130}131132133fn holeOf(content: Str) -> Result[Node, Str] {134 let found = Lexer.tokens(content) ?135 let (expression, next) = Parser.expressionAt(&found, 0) ?136 let token = found[next]137 if token.kind == Lexer.End { return Ok(Hole(expression, None)) }138 if token.kind == Lexer.Symbol && token.text == ":" { return Ok(Hole(expression, Some(content.drop(token.position + 1)))) }139 Err("unexpected `" + token.text + "` in hole `" + content + "`")140}141142143fn choiceAt(pieces: &Array[Piece], start: Int, condition: Str) -> Result[(Node, Int), Str] {144 var branches: Array[Branch] = []145 var current = Parser.parse(condition) ?146 var index = start147 loop {148 let (body, next, closer) = block(pieces, index) ?149 branches = branches.push(Branch{condition: current, body: body})150 if closer == "end" { return Ok((Choice(branches, []), next)) }151 if closer == "else" {152 let (otherwise, after, final) = block(pieces, next) ?153 if final != "end" { return Err("`\{#else\}` must be followed by `\{#end\}`") }154 return Ok((Choice(branches, otherwise), after))155 }156 if !closer.startsWith("else if ") { return Err("`\{#if\}` is not closed") }157 current = Parser.parse(closer.drop(8)) ?158 index = next159 }160}161162163fn eachAt(pieces: &Array[Piece], start: Int, header: Str) -> Result[(Node, Int), Str] {164 let parts = header.split(" in ")165 if parts.length() < 2 { return Err("`\{#each\}` needs `in`") }166 let names = parts[0].split(",").map(|name: Str| name.trim())167 let source = Parser.parse(parts.slice(1, parts.length()).join(" in ")) ?168 let second = List.get(&names, 1)169 let (body, afterBody, closer) = block(pieces, start) ?170 var delimiter: Array[Node] = []171 var empty: Array[Node] = []172 var next = afterBody173 var last = closer174 if last == "delimit" {175 let (inner, after, final) = block(pieces, next) ?176 delimiter = inner177 next = after178 last = final179 }180 if last == "else" {181 let (inner, after, final) = block(pieces, next) ?182 empty = inner183 next = after184 last = final185 }186 if last != "end" { return Err("`\{#each\}` is not closed") }187 Ok((Each(names[0], second, source, body, delimiter, empty), next))188}189190191export fn render(nodes: &Array[Node], within: &Evaluator.Scope) -> Str {192 var out: Array[Str] = []193 for node in nodes {194 out = out.push(match node {195 case TextNode(text) => text196 case Hole(expression, format) => holeText(&expression, &format, within)197 case Choice(branches, otherwise) => {198 match List.find(&branches, |branch: Branch| Values.isTrue(&Evaluator.evaluate(&branch.condition, within))) {199 case Some(branch) => render(&branch.body, within)200 case None => render(&otherwise, within)201 }202 }203 case Each(name, second, source, body, delimiter, empty) => eachText(name, &second, &source, &body, &delimiter, &empty, within)204 })205 }206 out.join("")207}208209210211fn holeText(expression: &Syntax.Expression, format: &Option[Str], within: &Evaluator.Scope) -> Str {212 if *expression == Syntax.Name("@l") { return Levels.moniker(within.event.level, format) }213 if *expression == Syntax.Name("@m") { return Display.plain(&Display.message(&within.event.template, &within.event.properties, &Some("lj"))) }214 match Evaluator.evaluate(expression, within) {215 case None => ""216 case Some(Log.Scalar(atom)) => Values.render(&Log.Scalar(atom), *format)217 case Some(held) => Json.value(&held, &Some(Display.TYPE_TAG))218 }219}220221222223fn eachText(name: Str, second: &Option[Str], source: &Syntax.Expression, body: &Array[Node], delimiter: &Array[Node], empty: &Array[Node], within: &Evaluator.Scope) -> Str {224 let bindings: Array[(Log.Value, Log.Value)] = match Evaluator.evaluate(source, within) {225 case Some(Log.Sequence(items)) => {226 var pairs: Array[(Log.Value, Log.Value)] = []227 var position = 0228 for item in items {229 pairs = pairs.push((item, Log.Scalar(Log.Integer(position))))230 position = position + 1231 }232 pairs233 }234 case Some(Log.Structure(shape)) => shape.properties.map(|member: Log.Property| (Log.Scalar(Log.Text(member.name)), member.value))235 case _ => []236 }237 if bindings.isEmpty() { return render(empty, within) }238 var parts: Array[Str] = []239 for binding in bindings {240 var bound = [(name, binding[0])]241 if let Some(other) = *second { bound = bound.push((other, binding[1])) }242 let inner = Evaluator.Scope{..*within, locals: bound.concat(within.locals)}243 parts = parts.push(render(body, &inner))244 }245 parts.join(render(delimiter, within))246}247