repos / temple.lean4.git


temple.lean4.git / Temple
Evgenii Akentev  ·  2025-08-19

Syntax.lean

  1import Parser
  2
  3namespace TempleSyntax
  4
  5inductive Term
  6| text : String -> Term
  7| var : String -> Term
  8| op : String -> List String -> Term
  9| forloop : String -> String -> List Term -> Term
 10deriving BEq, Repr, Inhabited
 11
 12partial instance : ToString Term where
 13  toString :=
 14    let rec toStr x :=
 15      have : ToString Term := toStr
 16      match x with
 17      | .text t => "\"" ++ t ++ "\""
 18      | .var name => "{{ " ++ name ++ " }}"
 19      | .op name args => "{% " ++ name ++ " " ++ (String.join $ args.intersperse " ") ++ " %}"
 20      | .forloop var collection body =>
 21        "{% for " ++ var ++ " in " ++ collection ++ " %}\n"
 22          ++ (String.join $ List.intersperse "\n" $ body.map toStr)
 23          ++ "\n{% endfor %}"
 24    toStr
 25
 26abbrev TempleParser := SimpleParser Substring Char
 27
 28namespace TempleParser
 29open Parser Char
 30
 31@[inline] def spaces : TempleParser Unit :=
 32  withErrorMessage "<spaces>" do
 33    dropMany (char ' ')
 34
 35@[inline] def spaces1 : TempleParser Unit :=
 36  withErrorMessage "<spaces1>" do
 37    dropMany1 (char ' ')
 38
 39partial def textInner (acc : String) : TempleParser String :=
 40  withErrorMessage "<text>" do
 41    try
 42      let savePos <- getPosition
 43      match <- option? (chars "{{" <|> chars "{%") with
 44        | some _ => do
 45          setPosition savePos
 46          return acc
 47        | none => do
 48          let c <- anyToken
 49          textInner $ String.push acc c
 50    catch _ => return acc
 51
 52@[inline] partial def text : TempleParser Term :=
 53  withErrorMessage "<text>" do
 54    let t <- textInner ""
 55    if t == "" then throwUnexpected else return Term.text t
 56
 57@[inline] def name : TempleParser String :=
 58  withErrorMessage "<name>" do
 59    foldl String.push "" $ ASCII.alphanum <|> char '_'
 60
 61@[inline] def var : TempleParser Term :=
 62  withErrorMessage "<var>" do
 63    let n <- spaces *> chars "{{" *> spaces *> name <* spaces <* chars "}}"
 64    return Term.var n
 65
 66@[inline] def op : TempleParser Term :=
 67  withErrorMessage "<op>" do
 68    let n <- spaces *> chars "{%" *> spaces *> name
 69    let firstArg <- spaces1 *> name
 70    let args <- spaces1 *> (sepBy1 spaces1 name) <* spaces <* chars "%}"
 71    return Term.op n (List.cons firstArg $ args.toList.filter (fun a => !a.isEmpty))
 72
 73mutual
 74  partial def forloop : TempleParser Term :=
 75    withErrorMessage "<forloop>" do
 76      let _ <- chars "{%" *> spaces *> chars "for"
 77      let v <- spaces1 *> name <* spaces1 <* chars "in"
 78      let collection <- spaces1 *> name <* spaces <* chars "%}"
 79
 80      let parsedTerms <- terms
 81
 82      let _ <- chars "{%" *> spaces *> chars "endfor" *> spaces <* chars "%}"
 83      return Term.forloop v collection parsedTerms
 84
 85  partial def terms : TempleParser (List Term) :=
 86    withErrorMessage "<term>" do
 87      let t <- withErrorMessage "<term>: expected terms"
 88        $ takeMany $ forloop <|> op <|> var <|> text
 89      return t.toList
 90end
 91
 92end TempleParser
 93
 94def parse (input : String) : IO (List Term) :=
 95  match (TempleParser.terms <* Parser.endOfInput).run input.toSubstring with
 96  | .ok _ t => return t
 97  | .error _ e => throw $ IO.userError s!"Parsing error: {e}"
 98
 99section Test
100
101#eval! show IO Bool from do
102  let t <- parse "hello worl asd a{}s[]@#}!@#!@#SADsa][c«»»{»  æыв{ { {  }} { % { { {фыÔ¨ÍÓÎÓˆd"
103  return t == [.text "hello worl asd a{}s[]@#}!@#!@#SADsa][c«»»{»  æыв{ { {  }} { % { { {фыÔ¨ÍÓÎÓˆd"]
104
105#eval! show IO Bool from do
106  let t <- parse "{{ myVariable }}"
107  return t == [.var "myVariable"]
108
109#eval! show IO Bool from do
110  let t <- parse "hello {{ myVariable }}"
111  return t == [.text "hello ", .var "myVariable"]
112
113#eval! show IO Bool from do
114  let t <- parse "{% my_operator s   d %}"
115  return t == [.op "my_operator" [ "s", "d" ]]
116
117#eval! show IO Bool from do
118  let t <- parse "{% for    x     in      fruits %}sadasd{{hello}}{% endfor %}"
119  return t == [.forloop "x" "fruits" [ .text "sadasd", .var "hello" ]]
120
121#eval! show IO Bool from do
122  let t <- parse "{% for x in fruits %}hello{% for y in fruits %}world{{x}}{{y}}{% endfor %}good{% endfor %}"
123  return t == [.forloop "x" "fruits"
124    [ .text "hello", .forloop "y" "fruits" [ .text "world", .var "x", .var "y" ], .text "good" ]]
125
126end Test
127
128end TempleSyntax