Skip to content

Instantly share code, notes, and snippets.

@eerohele
Last active October 15, 2020 05:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eerohele/28e761f05aa1e68aa5191090cc35effe to your computer and use it in GitHub Desktop.
Save eerohele/28e761f05aa1e68aa5191090cc35effe to your computer and use it in GitHub Desktop.
Write XSLT with Clojure
(ns clj-xslt
(:require [clojure.data.xml :as xml])
(:import (java.time LocalDateTime)
(java.time.format DateTimeFormatter)
(java.io StringBufferInputStream StringWriter StringReader)
(javax.xml.transform.stream StreamSource)
(net.sf.saxon.s9api XsltCompiler Processor)
(net.sf.saxon Configuration)))
(def processor
(Processor. (Configuration.)))
(def ^:private builder
(doto (.newDocumentBuilder processor)))
(defprotocol XmlNode
(build [source]))
(extend-protocol XmlNode String
(build [xml-string]
(.build builder (StreamSource. (StringReader. xml-string)))))
(def ^:dynamic ^XsltCompiler *xslt-compiler* (.newXsltCompiler processor))
(xml/alias-uri 'xsl "http://www.w3.org/1999/XSL/Transform")
;; Constructor functions for XSLT elements.
(defn stylesheet
[& [attrs & xs]] [::xsl/stylesheet attrs xs])
(defn template
[& [attrs & xs]] [::xsl/template attrs xs])
(defn copy
[& [attrs & xs]] [::xsl/copy attrs xs])
(defn apply-templates
[& [attrs & xs]] [::xsl/apply-templates attrs xs])
(defn value-of
[& [attrs & xs]] [::xsl/value-of attrs xs])
(defn current-date
"Return the current date in yyyy-MM-dd format."
[]
(.format (LocalDateTime/now) (DateTimeFormatter/ofPattern "yyyy-MM-dd")))
;; Define reusable templates.
(def identity-template
"An XSLT identity template."
(template {:match "@* | node()"}
(copy (apply-templates {:select "@* | node()"}))))
;; Reduce boilerplate.
(defn xslt3-identity
[& content]
(stylesheet {:version 3.0} identity-template content))
(def first-stylesheet
(xslt3-identity
(template {:match "date"}
;; Dynamically define XSLT stylesheets. This example doesn't make much sense because XPath has the
;; `current-date()` function, but this is just illustrative.
(copy (current-date)))))
(def second-stylesheet
(xslt3-identity
(template {:match "number"}
(copy (value-of {:select "number(.) * 10"})))))
(def third-stylesheet
(xslt3-identity
(template {:match "element"}
(copy (apply-templates {:select "@*"})
[:child-element "with some content"]))))
(defn compile-xslt
[& stylesheets]
(map (fn [stylesheet]
;; There's probably a more efficient way than to serialize the Clojure stylesheet into string and then read it,
;; but this'll do for illustrative purposes.
(let [writer (xml/emit (xml/sexp-as-element stylesheet) (StringWriter.))]
(.. *xslt-compiler*
(compile (StreamSource.
(StringBufferInputStream.
(.toString writer)))))))
stylesheets))
(defn transform
[executables source]
(if (empty? executables)
source
(transform (rest executables)
(.applyTemplates (.load30 (first executables)) source))))
;; Easily create XSLT transformation pipelines.
(def pipeline (comp (partial transform (compile-xslt third-stylesheet))
(partial transform (compile-xslt second-stylesheet))
(partial transform (compile-xslt first-stylesheet))))
(def document-one (build "<root><date>2001-01-01</date><number>1</number><element x=\"1\"/></root>"))
(def document-two (build "<root><date>2002-02-02</date><number>2</number><element x=\"2\"/></root>"))
;; Run XSLT transformations in parallel
(pmap (comp println str pipeline) [document-one document-two])
;;=>
;; <root>
;; <date>2017-04-21</date>
;; <number>10</number>
;; <element x="1">
;; <child-element>with some content</child-element>
;; </element>
;; </root>
;; <root>
;; <date>2017-04-21</date>
;; <number>20</number>
;; <element x="2">
;; <child-element>with some content</child-element>
;; </element>
;; </root>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment