Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

Parsecを使ってjinja2風テンプレートをパースする。

http://github.com/mopemope/clarsec2を使って以前のjinja2風のテンプレを解析する。
まずはおおまかなブロックを検出するだけだけど。

input

<html>
  <body>
    {% foo %}
      {{ variable }}
    {% endfoo %}
    <div>
      {#_
      Comment
      Comment
      #}
    </div>
  </body>
</html>

コード

(use '[clarsec])
(use '[clarsec.monad])
(require '[clojure.string :as string])
(import '(java.util.regex Pattern))

(def sp-chars #"([\\\\*+\\[\\](){}\\$.?\\^|])")

(defn- esc-regex [in]
  (string/replace in sp-chars "\\\\$1"))

(defn- build-reg [args]
  (string/join "|" (map esc-regex args)))

(defn- create-regex [tags]
  (Pattern/compile (str "^(.*?)(" (build-reg tags) ")") Pattern/DOTALL))

(defmacro make-data [k]
  `(fn [x#] {~k x#}))

(defn text-data-p [regex conv-fn]
  (make-parser
    (fn p-text-data [^String strn]
      (let [found (re-find (re-matcher (create-regex regex) strn))
            [pair text token] (or found [nil nil nil])]
        (if (and token (not= token pair))
          (consumed (conv-fn text) (.substring strn (- (.length pair) (.length token))))
          (failed))))))

(defmacro surround2 [st ed]
  `(fn [p#] (between (symb ~st) (symb ~ed) p#)))

(defn combine [p last]
   (let-bind [r p
              l last]
            (result (concat [r] l))))

;; -------------------------------------

(def tag-block ((surround2 "{%" "%}")
                  (<$> (make-data :tag-block) identifier)))

(def variant-block ((surround2 "{{" "}}")
                  (<$> (make-data :variant-block) identifier)))

(def comment-block ((surround2 "{#" "#}")
                  (<$> (make-data :comment-block) (text-data-p ["#}"] #(str %)))))


(def text-data (text-data-p ["{{" "{%" "{#"] (make-data :text-data)))

(def all
  (<$> (make-data :text-data)
     (make-parser
       (fn p-any-token [strn]
         (consumed strn "")))))

(def blocks (many1 (<|> text-data tag-block variant-block comment-block)))

(def content (slurp "/tmp/index.html"))
(parse$ (combine blocks all) content)

最後の部分はまあ適当なんですけど。
上半分はutilとかキモいパーサーとかです。
正規表現とミックスしたものが使えるので便利ですね。

肝心はココです。
なんとなくどんなものの組み合わせ構成されてるかわかりますね。

(def tag-block ((surround2 "{%" "%}")
                  (<$> (make-data :tag-block) identifier)))

(def variant-block ((surround2 "{{" "}}")
                  (<$> (make-data :variant-block) identifier)))

(def comment-block ((surround2 "{#" "#}")
                  (<$> (make-data :comment-block) (text-data-p ["#}"] #(str %)))))


(def text-data (text-data-p ["{{" "{%" "{#"] (make-data :text-data)))

(def all
  (<$> (make-data :text-data)
     (make-parser
       (fn p-any-token [strn]
         (consumed strn "")))))

(def blocks (many1 (<|> text-data tag-block variant-block comment-block)))

(def content (slurp "/tmp/index.html"))
(parse$ (combine blocks all) content)

結果:

{:type :consumed, :value (({:text-data "\n \n "} {:tag-block "foo"} {:variant-block "variable"} {:tag-block "endfoo"} {:text-data "

\n "} {:comment-block "_\n Comment\n Comment\n "}) [:text-data "
\n \n\n\n"]), :rest ""}

一応パースできてるっぽい。
正規表現エスケープまみれになったコードよりわかりやすく(?)スッキリ(?)書けます。
速度はもちのろん出ません!!!
(<|>でtry-errorを繰り返しまくるので)