Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Dynamic links - replaceLink function #150

Merged
merged 1 commit into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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
Loading