Skip to content

Commit

Permalink
feat: Dynamic links - replaceLink function (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbali committed Oct 11, 2023
1 parent 651b7f7 commit 7f84d1e
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 1 deletion.
11 changes: 11 additions & 0 deletions docs/Links.md
Original file line number Diff line number Diff line change
@@ -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:

<code>
{<i>%=replaceLink(url)%</i>}
</code>

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.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/links/README.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions examples/links/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"urls": [
"https://stencil.erdos.dev",
"https://httpbin.org/get?data=1&data2=2"
]
}
Binary file added examples/links/template.docx
Binary file not shown.
4 changes: 3 additions & 1 deletion src/stencil/ooxml.clj
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,6 @@
"http:https://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)
(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)
53 changes: 53 additions & 0 deletions src/stencil/postprocess/links.clj
Original file line number Diff line number Diff line change
@@ -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))))
3 changes: 3 additions & 0 deletions src/stencil/tree_postprocess.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
Expand All @@ -28,5 +29,7 @@

#'replace-images

#'replace-links

;; call this first. includes fragments and evaluates them too.
#'unpack-fragments))
Binary file added test-resources/test-link-1.docx
Binary file not shown.
6 changes: 6 additions & 0 deletions test/stencil/api_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 7f84d1e

Please sign in to comment.