Skip to content

Commit

Permalink
feat: refactor function definitions in clj
Browse files Browse the repository at this point in the history
  • Loading branch information
erdos committed Jul 28, 2024
1 parent 3c1d2f5 commit e6f8323
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 35 deletions.
28 changes: 28 additions & 0 deletions scripts/generate-fun-docs.clj.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env sh
test ; # file is both a valid SH and a valid CLJ file at the same time.

test ; set -e && cd "$(dirname "$0")/.." && clojure -M -i scripts/generate-fun-docs.clj.sh > target/Functions.md && exit 0

;; file is a regular CLJ script from now on

(println "HALI")

(require 'stencil.functions)
(println "# Functions")
(println)
(println "You can call functions from within the template files and embed the call result easily by writing `{%=functionName(arg1, arg2, arg3, ...)%}` expression in the document template.")
(println "This page contains a short description of the functions implemented in Stencil.")
(println)
(doseq [k (sort (keys (methods stencil.functions/call-fn)))]
(printf "- [%s](#%s)\n" k k))
(println)
(doseq [[k f] (sort (methods stencil.functions/call-fn))]
(printf "## %s\n\n" k)
(when-let [docs (:stencil.functions/docs (meta f))]
(println docs)
(println))
(println))
102 changes: 81 additions & 21 deletions src/stencil/functions.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,27 @@
The rest of the arguments are the function call parameters."
(fn [function-name & args-seq] function-name))

(defmethod call-fn "range"
([_ x] (range x))
([_ x y] (range x y))
([_ x y z] (range x y z)))
(defmacro def-stencil-fn [name docs & bodies]
(assert (string? name))
(assert (string? docs))
`(.addMethod ^clojure.lang.MultiFn call-fn ~name
~(with-meta `(fn [_# & args#] (apply (fn ~@bodies) args#)) {::docs docs})))

(defmethod call-fn "integer" [_ n] (some-> n biginteger))
(defmethod call-fn "decimal" [_ f] (with-precision 8 (some-> f bigdec)))
(def-stencil-fn "range"
"Creates an array of numbers between bounds, for use in iteration forms.
Parameters are start value (default 0), upper bound, step size (default 1).
Eg.: range(4) = [0, 1, 2, 3], range(2,4) = [2, 3], range(2, 10, 2) = [2, 4, 8]"
([x] (range x))
([x y] (range x y))
([x y z] (range x y z)))

(def-stencil-fn "integer"
"Converts parameter to integer number. Returns null for missing value."
[n] (some-> n biginteger))

(def-stencil-fn "decimal"
"Converts parameter to decimal number. Returns null for missing value."
[f] (with-precision 8 (some-> f bigdec)))

;; The format() function calls java.lang.String.format()
;; but it predicts the argument types from the format string and
Expand All @@ -36,7 +50,14 @@
get-types (fn [p] (or (some (fn [[k v]] (when (= k p) v)) @cache)
(doto (get-types p)
(->> (swap! cache (fn [c t] (take cache-size (cons [p t] c))))))))]
(defmethod call-fn "formatWithLocale" [_ locale pattern-str & args]
(def-stencil-fn "formatWithLocale"
"Similar to `format()` but first parameter is an IETF Language Tag.
**Usage:** `formatWithLocale('hu', '%,.2f', number)`
**Example:**
To format the value of price as a price string: {%=format('$ %(,.2f', price) %}. It may output $ (6,217.58)."
[locale pattern-str & args]
(when-not (string? pattern-str)
(fail "Format pattern must be a string!" {:pattern pattern-str}))
(when (empty? args)
Expand All @@ -55,28 +76,57 @@
(to-array)
(String/format locale pattern-str)))))

(defmethod call-fn "format" [_ pattern-str & args]
(def-stencil-fn "format"
"Calls String.format function."
[pattern-str & args]
(apply call-fn "formatWithLocale" (java.util.Locale/getDefault) pattern-str args))

;; finds first nonempy argument
(defmethod call-fn "coalesce" [_ & args-seq]
(def-stencil-fn "coalesce"
"Accepts any number of arguments, returns the first not-empty value."
[& args-seq]
(find-first (some-fn number? true? false? not-empty) args-seq))

(defmethod call-fn "length" [_ items] (count items))
(def-stencil-fn "length"
"The `length(x)` function returns the length of the value in `x`:
- Returns the number of characters when `x` is a string.
- Returns the number of elements the `x` is a list/array.
- Returns the number of key/value pairs when `x` is an object/map.
- Returns zero when `x` is `null`."
[items] (count items))

(defmethod call-fn "contains" [_ item items]
(boolean (some #{(str item)} (map str items))))
(def-stencil-fn "contains"
"Expects two arguments: a value and a list. Checks if list contains the value.
Usage: contains('myValue', myList)"
[item items] (boolean (some #{(str item)} (map str items))))

(defmethod call-fn "sum" [_ items]
(reduce + items))
(def-stencil-fn "sum"
"Expects one number argument containing a list with numbers. Sums up the numbers and returns result.
Usage: sum(myList)"
[items] (reduce + items))

(defmethod call-fn "list" [_ & elements] (vec elements))
(def-stencil-fn "list"
"Creates a list collection from the supplied arguments.
Intended to be used with other collections functions."
[& elements] (vec elements))

(defn- lookup [column data]
(second (or (find data column)
(find data (keyword column)))))

(defmethod call-fn "map" [_ ^String column data]
(def-stencil-fn "map"
"Selects values under a given key in a sequence of maps.
The first parameter is a string which contains what key to select:
- It can be a single key name
- It can be a nested key, separated by `.` character. For example: `outerkey.innerkey`
- It can be used for selecting from multidimensional arrays: `outerkey..innerkey`
Example use cases with data: `{'items': [{'price': 10, 'name': 'Wood'}, {'price': '20', 'name': 'Stone'}]}`
- `join(map('name', items), ',')`: to create a comma-separated string of item names. Prints `Wood, Stone`.
- `sum(map('price', items))`: to write the sum of item prices. Prints `30`.
"
[^String column data]
(when-not (string? column)
(fail "First parameter of map() must be a string!" {}))
(reduce (fn [elems p]
Expand All @@ -92,11 +142,21 @@
data
(.split column "\\.")))

(defmethod call-fn "joinAnd" [_ elements ^String separator1 ^String separator2]
(def-stencil-fn "joinAnd"
"Joins a list of items using two separators.
The first separator is used to join the items except for the last item.
The second separator is used to join the last item.
When two items are supplied, then only the second separator is used.
**Example:** call `joinAnd(xs, ', ', ' and ')` to get `'1, 2, 3 and 4'`."
[elements ^String separator1 ^String separator2]
(case (count elements)
0 ""
1 (str (first elements))
(str (clojure.string/join separator1 (butlast elements)) separator2 (last elements))))
0 ""
1 (str (first elements))
(str (clojure.string/join separator1 (butlast elements)) separator2 (last elements))))

(defmethod call-fn "replace" [_ text pattern replacement]
(def-stencil-fn "replace"
"The replace(text, pattern, replacement) function replaces all occurrences
of pattern in text by replacement."
[text pattern replacement]
(clojure.string/replace (str text) (str pattern) (str replacement)))
6 changes: 4 additions & 2 deletions src/stencil/infix.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
https://en.wikipedia.org/wiki/Shunting-yard_algorithm"
(:require [stencil.util :refer [->int string whitespace?]]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [call-fn def-stencil-fn]]
[stencil.grammar :as grammar]))

(set! *warn-on-reflection* true)
Expand Down Expand Up @@ -147,7 +147,9 @@

;; Gives access to whole input payload. Useful when top level keys contain strange characters.
;; Example: you can write data()['key1']['key2'] instead of key1.key2.
(defmethod call-fn "data" [_] *calc-vars*)
(def-stencil-fn "data"
"The function returns the original whole template data object."
[] *calc-vars*)

(defmethod eval-tree :fncall [[_ f & args]]
(let [args (mapv eval-tree args)]
Expand Down
12 changes: 9 additions & 3 deletions src/stencil/model/fragments.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns stencil.model.fragments
(:require [stencil.util :refer [eval-exception]]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [def-stencil-fn]]
[stencil.types :refer [ControlMarker]]
[stencil.ooxml :as ooxml]
[clojure.data.xml :as xml]))
Expand Down Expand Up @@ -33,12 +33,18 @@
(defrecord FragmentInvoke [result] ControlMarker)

;; custom XML content
(defmethod call-fn "xml" [_ content]
(def-stencil-fn "xml"
"Inserts OOXML fragment into the document from the parameter of this function call.
Usage: `{%=xml(ooxml)%}`"
[content]
(assert (string? content))
(let [content (:content (xml/parse-str (str "<a>" content "</a>")))]
(->FragmentInvoke {:frag-evaled-parts content})))

;; inserts a page break at the current run.
(let [br {:tag ooxml/br :attrs {ooxml/type "page"}}
page-break (->FragmentInvoke {:frag-evaled-parts [br]})]
(defmethod call-fn "pageBreak" [_] page-break))
(def-stencil-fn "pageBreak"
"Inserts page break into the document where the return value of this function is used.
Usage: `{%=pageBreak()%}`"
[] page-break))

Check warning on line 50 in src/stencil/model/fragments.clj

View check run for this annotation

Codecov / codecov/patch

src/stencil/model/fragments.clj#L50

Added line #L50 was not covered by tests
20 changes: 18 additions & 2 deletions src/stencil/postprocess/html.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"Replaces results of html() calls with external part relationships."
(:require [clojure.zip :as zip]
[clojure.data.xml :as xml]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [def-stencil-fn]]
[stencil.postprocess.fragments :as fragments]
[stencil.types :refer [ControlMarker]]
[stencil.util :refer [find-first dfs-walk-xml dfs-walk-xml-node]]
Expand All @@ -12,7 +12,23 @@

(defrecord HtmlChunk [content] ControlMarker)

(defmethod call-fn "html" [_ content] (->HtmlChunk content))
(def-stencil-fn "html"
"It is possible to embed text with basic dynamic formatting using HTML notation.
The HTML code will be converted to OOXML and inserted in the document.
Stencil uses a simple parsing algorithm to convert between the formats. At the moment only a limited set of basic formatting is implemented. You can use the following HTML tags:

Check warning on line 19 in src/stencil/postprocess/html.clj

View check run for this annotation

Codecov / codecov/patch

src/stencil/postprocess/html.clj#L19

Added line #L19 was not covered by tests
- b, em, strong for bold text.
- i for italics.
- u for underlined text.
- s for strikethrough text.
- sup for superscript and sub for subscript.
- span elements have no effects.
- br tags can be used to insert line breaks.
The rendering throws an exception on invalid HTML input or unexpected HTML tags.
**Usage:** `{%=html(x) %}`"
[content] (->HtmlChunk content))

Check warning on line 31 in src/stencil/postprocess/html.clj

View check run for this annotation

Codecov / codecov/patch

src/stencil/postprocess/html.clj#L31

Added line #L31 was not covered by tests

(def legal-tags
"Set of supported HTML tags"
Expand Down
6 changes: 4 additions & 2 deletions src/stencil/postprocess/images.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(ns stencil.postprocess.images
(:require [clojure.java.io :as io]
[clojure.zip :as zip]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [def-stencil-fn]]
[stencil.log :as log]
[stencil.ooxml :as ooxml]
[stencil.model.relations :as relations]
Expand Down Expand Up @@ -87,7 +87,9 @@
:writer (bytes->writer bytes)}))

;; replaces the nearest image with the content
(defmethod call-fn "replaceImage" [_ data]
(def-stencil-fn "replaceImage"
""
[data]
(let [extra-file (img-data->extrafile data)]
(relations/add-extra-file! extra-file)
(->ReplaceImage (:new-id extra-file))))
7 changes: 5 additions & 2 deletions src/stencil/postprocess/links.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns stencil.postprocess.links
(:require [clojure.zip :as zip]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [def-stencil-fn]]
[stencil.log :as log]
[stencil.ooxml :as ooxml]
[stencil.model.relations :as relations]
Expand Down Expand Up @@ -47,7 +47,10 @@
:stencil.model/mode "External"}))

;; replaces the nearest link's URK with the parameter value
(defmethod call-fn "replaceLink" [_ url]
(def-stencil-fn "replaceLink"
"Replaces the link URL in the hyperlink preceding this expression.
It should be placed immediately after the link we want to modify."
[url]
(let [new-relation (link-url->relation (str url))]
(relations/add-extra-file! new-relation)
(->ReplaceLink (:new-id new-relation))))
10 changes: 7 additions & 3 deletions src/stencil/postprocess/table.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(ns stencil.postprocess.table
"XML fa utofeldolgozasat vegzo kod."
(:require [clojure.zip :as zip]
[stencil.functions :refer [call-fn]]
[stencil.functions :refer [def-stencil-fn]]
[stencil.ooxml :as ooxml]
[stencil.types :refer [ControlMarker]]
[stencil.util :refer [find-first find-last fixpt iterations ->int find-first-in-tree xml-zip zipper?]]))
Expand All @@ -24,7 +24,9 @@
(defrecord HideTableRowMarker [] ControlMarker)
(defn hide-table-row-marker? [x] (instance? HideTableRowMarker x))

(defmethod call-fn "hideColumn" [_ & args]
(def-stencil-fn "hideColumn"
"Stencil will remove the column of the table where the value produced by this function call is inserted."
[& args]
(case (first args)
("cut") (->HideTableColumnMarker :cut)
("resize-last" "resizeLast" "resize_last") (->HideTableColumnMarker :resize-last)
Expand All @@ -33,7 +35,9 @@
;; default
(->HideTableColumnMarker)))

(defmethod call-fn "hideRow" [_] (->HideTableRowMarker))
(def-stencil-fn "hideRow"
"Stencil will remove the row of the table where the value produced by this function call is inserted."
[] (->HideTableRowMarker))

;; columns narrower that this are goig to be removed
(def min-col-width 20)
Expand Down

0 comments on commit e6f8323

Please sign in to comment.