diff --git a/docs/Links.md b/docs/Links.md new file mode 100644 index 00000000..d5b1dcc8 --- /dev/null +++ b/docs/Links.md @@ -0,0 +1,11 @@ +# Dynamic Links + +You can replace hyperlinks in the template file with dynamic links by using the `replaceLink` function after a placeholder link in the document: + + +{%=replaceLink(url)%} + + +The value of `url` is not validated, it is converted to string if needed. + +The expression replaces the link URL in the hyperlink preceding this expression, therefore, it should be placed immediately after the link, we want to modify. diff --git a/docs/index.md b/docs/index.md index 144af8ec..1c80df60 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ You can find the project on [Stencil's GitHub](https://github.com/erdos/stencil) - Reuse parts of templates with [Fragments](Fragments.md) - Run it in [Standalone Mode](Standalone.md) for batch processing - [Dynamic images](Images.md) +- [Dynamic links](Links.md) ## For Programmers diff --git a/examples/links/README.md b/examples/links/README.md new file mode 100644 index 00000000..0de24dff --- /dev/null +++ b/examples/links/README.md @@ -0,0 +1,4 @@ +# Replace links in template + +In this example, you can see how to replace hyperlinks in a `for` loop in the template document. +The `urls` array in the input JSON map contains the URLs generated into the result. diff --git a/examples/links/data.json b/examples/links/data.json new file mode 100644 index 00000000..6fee4445 --- /dev/null +++ b/examples/links/data.json @@ -0,0 +1,6 @@ +{ + "urls": [ + "https://stencil.erdos.dev", + "https://httpbin.org/get?data=1&data2=2" + ] +} \ No newline at end of file diff --git a/examples/links/template.docx b/examples/links/template.docx new file mode 100644 index 00000000..c6d1abed Binary files /dev/null and b/examples/links/template.docx differ diff --git a/src/stencil/ooxml.clj b/src/stencil/ooxml.clj index e71a56b9..37126787 100644 --- a/src/stencil/ooxml.clj +++ b/src/stencil/ooxml.clj @@ -121,4 +121,6 @@ "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" "xr10"}) ;; drawing, binary large image or picture -(def blip :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fdrawingml%2F2006%2Fmain/blip) \ No newline at end of file +(def blip :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fdrawingml%2F2006%2Fmain/blip) +;; hyperlinks +(def hyperlink :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fwordprocessingml%2F2006%2Fmain/hyperlink) \ No newline at end of file diff --git a/src/stencil/postprocess/links.clj b/src/stencil/postprocess/links.clj new file mode 100644 index 00000000..3b731896 --- /dev/null +++ b/src/stencil/postprocess/links.clj @@ -0,0 +1,53 @@ +(ns stencil.postprocess.links + (:require [clojure.zip :as zip] + [stencil.functions :refer [call-fn]] + [stencil.log :as log] + [stencil.ooxml :as ooxml] + [stencil.model.relations :as relations] + [stencil.types :refer [ControlMarker]] + [stencil.util :refer [fail find-first iterations dfs-walk-xml-node]])) + +(set! *warn-on-reflection* true) + +;; Tells if the reference of an adjacent hyperlink node should be replaced in postprocess step. +(defrecord ReplaceLink [relation] ControlMarker) + +(defn- update-link [link-node, ^ReplaceLink data] + (assert (= ooxml/hyperlink (:tag link-node))) + (assert (instance? ReplaceLink data)) + (let [current-rel (-> link-node :attrs ooxml/r-id) + new-val (-> data .relation)] + (assert new-val) + (log/debug "Replacing hyperlink relation {} by {}" current-rel new-val) + (assoc-in link-node [:attrs ooxml/r-id] new-val))) + +(defn- replace-link [marker-loc] + (if-let [link-loc (->> (zip/remove marker-loc) + (iterations zip/prev) + (find-first (comp #{ooxml/hyperlink} :tag zip/node)))] + (zip/edit link-loc update-link (zip/node marker-loc)) + (fail "Did not find hyperlink to replace. The location of target link must precede the replaceLink() function call location." {}))) + +(defn replace-links [xml-tree] + (dfs-walk-xml-node + xml-tree + (partial instance? ReplaceLink) + replace-link)) + +;; This duplicates both stencil.postprocess.image/->relation-id, +;; and stencil.model.relations/->relation-id +;; TODO: maybe make stencil.model.relations/->relation-id public +(defn- ->relation-id [] (str (gensym "srel"))) + +(defn- link-url->relation [url] + (let [new-rel (->relation-id)] + {:new-id new-rel + :stencil.model/type relations/rel-type-hyperlink + :stencil.model/target url + :stencil.model/mode "External"})) + +;; replaces the nearest link's URK with the parameter value +(defmethod call-fn "replaceLink" [_ url] + (let [new-relation (link-url->relation (str url))] + (relations/add-extra-file! new-relation) + (->ReplaceLink (:new-id new-relation)))) \ No newline at end of file diff --git a/src/stencil/tree_postprocess.clj b/src/stencil/tree_postprocess.clj index 83bb1e97..2ae7450c 100644 --- a/src/stencil/tree_postprocess.clj +++ b/src/stencil/tree_postprocess.clj @@ -4,6 +4,7 @@ [stencil.postprocess.whitespaces :refer :all] [stencil.postprocess.ignored-tag :refer :all] [stencil.postprocess.images :refer :all] + [stencil.postprocess.links :refer :all] [stencil.postprocess.list-ref :refer :all] [stencil.postprocess.fragments :refer :all] [stencil.postprocess.html :refer :all])) @@ -28,5 +29,7 @@ #'replace-images + #'replace-links + ;; call this first. includes fragments and evaluates them too. #'unpack-fragments)) diff --git a/test-resources/test-link-1.docx b/test-resources/test-link-1.docx new file mode 100644 index 00000000..52e07732 Binary files /dev/null and b/test-resources/test-link-1.docx differ diff --git a/test/stencil/api_test.clj b/test/stencil/api_test.clj index d9a35826..061b6efc 100644 --- a/test/stencil/api_test.clj +++ b/test/stencil/api_test.clj @@ -133,6 +133,12 @@ (with-open [template (prepare "test-resources/test-image-1.docx")] (render! template data :output f :overwrite? true)))) +(deftest test-link + (let [data {"url" "https://stencil.erdos.dev/?data=1&data2=2"} + f (java.io.File/createTempFile "stencil" ".docx")] + (with-open [template (prepare "test-resources/test-link-1.docx")] + (render! template data :output f :overwrite? true)))) + (deftest test-multipart (let [template (prepare "test-resources/multipart/main.docx") body (fragment "test-resources/multipart/body.docx")