jinja2風なテンプレートをパースする
こんにちは、高校生です。
Parserはいつも人の使ったり、ragel先輩に吐かせたり楽してたのですがシンプルなものなら書けるかなと思い書いてみました。
(import '(java.util.regex Pattern)) (require '[clojure.string :as string]) (def sp-chars #"([\\\\*+\\[\\](){}\\$.?\\^|])") (def ^:dynamic *bt* "{%") (def ^:dynamic *bv* "{{") (def ^:dynamic *bc* "{#") (def ^:dynamic *et* "%}") (def ^:dynamic *ev* "}}") (def ^:dynamic *ec* "#}") (defrecord Text [data]) (defrecord Tag [data]) (defrecord Variable [data]) (defrecord Comment [data]) (defn- esc [hin] (string/replace in sp-chars "\\\\$1")) (defn- build-reg [args] (string/join "|" (map esc args))) (defn- create-regex [tags] (Pattern/compile (str "^(.*?)(" (build-reg tags) ")") Pattern/DOTALL)) (def bregex (create-regex [*bt* *bv* *bc*])) (def eregex (create-regex [*et* *ev* *ec*])) (def reg {true bregex false eregex}) (def content (slurp "/tmp/index.html")) (defn- get-node [token text] (condp = token *bt* (Tag. (string/trim text)) *bv* (Variable. (string/trim text)) *bc* (Comment. text) (Text. text))) (defn- parse-tmpl [input] (loop [input input mode true next-token nil ast []] (let [found (re-find (re-matcher (reg mode) input)) [pair text token] (or found [input input nil]) ast (if (empty? text) ast (conj ast (get-node next-token text)))] ; (println "pair" pair) ; (println "text" text ) ; (println "token" token) (if token (let [rest (.substring input (.length pair)) mode (not mode)] (recur rest mode token ast)) ast)))) (parse-tmpl content)
思ったより普通ですね。
テンプレート
<html> <body> {% foo %} {{ variable }} {% endfoo %} <div> {#_ Comment Comment #} </div> </body> </html>
結果
[#user.Text{:data "\n
\n "} #user.Tag{:data "foo"} #user.Text{:data "\n "} #user.Variable{:data "variable"} #user.Text{:data "\n "} #user.Tag{:data "endfoo"} #user.Text{:data "\n\n "} #user.Comment{:data " \n Comment \n Comment\n "} #user.Text{:data "\n\n \n\n"}]
とまあシンプルなASTができたっぽい。
Nodeをdefrecordで作ってるので組み立てる際にprotocolを定義してなめればそれなりのテンプレートエンジンもすぐ作れそうですね。