Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Template.pudu

Pudu161 lines6.0 KB

GitHub ↗
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)1112/// Deepest partial inclusion followed before rendering stops expanding partials.13const MAX_PARTIALS: Int = 161415/// A template parsed into nodes; an unclosed or mismatched section is an error.16/// `\{\{name\}\}` escapes, `\{\{\{name\}\}\}` and `\{\{& name\}\}` do not, `\{\{#name\}\}…\{\{/name\}\}`17/// repeats or shows, `\{\{^name\}\}` shows when empty, `\{\{>name\}\}` includes, `\{\{! …\}\}` is ignored.18export fn parse(text: Str) -> Result[Array[Node], Str] {19  let (nodes, _rest, _closed) = sequence(text, "") ?20  Ok(nodes)21}2223/// A template rendered against metadata; `partials` are parsed templates by name.24export fn render(nodes: &Array[Node], context: &Docgen.Meta, partials: &Map[Str, Array[Node]]) -> Str {25  fill(nodes, &[*context], partials, 0)26}2728/// Nodes up to a closing tag for `open`, with the remaining text and the name closed.29fn 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}6768/// Nodes rendered against a stack of contexts, innermost last.69fn 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}104105/// A dotted name looked up from the innermost context outwards; `.` is the innermost value.106fn 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}124125/// The value reached by following further name parts.126fn 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}139140/// Whether a value shows a section: nonempty text and lists, true, nonzero numbers, and objects.141fn 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}151152/// The text a value shows; lists and objects show nothing.153fn 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