From e6f83235f791a6d3e5a64747102d0c84f060586a Mon Sep 17 00:00:00 2001 From: janos erdos Date: Sun, 28 Jul 2024 12:48:58 +0200 Subject: [PATCH] feat: refactor function definitions in clj --- scripts/generate-fun-docs.clj.sh | 28 ++++++++ src/stencil/functions.clj | 102 +++++++++++++++++++++++------ src/stencil/infix.clj | 6 +- src/stencil/model/fragments.clj | 12 +++- src/stencil/postprocess/html.clj | 20 +++++- src/stencil/postprocess/images.clj | 6 +- src/stencil/postprocess/links.clj | 7 +- src/stencil/postprocess/table.clj | 10 ++- 8 files changed, 156 insertions(+), 35 deletions(-) create mode 100644 scripts/generate-fun-docs.clj.sh diff --git a/scripts/generate-fun-docs.clj.sh b/scripts/generate-fun-docs.clj.sh new file mode 100644 index 00000000..8a8fed72 --- /dev/null +++ b/scripts/generate-fun-docs.clj.sh @@ -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)) diff --git a/src/stencil/functions.clj b/src/stencil/functions.clj index f4c45a42..92d3f09c 100644 --- a/src/stencil/functions.clj +++ b/src/stencil/functions.clj @@ -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 @@ -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) @@ -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] @@ -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))) diff --git a/src/stencil/infix.clj b/src/stencil/infix.clj index 32c35d83..76567a78 100644 --- a/src/stencil/infix.clj +++ b/src/stencil/infix.clj @@ -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) @@ -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)] diff --git a/src/stencil/model/fragments.clj b/src/stencil/model/fragments.clj index 4b834c70..0f047132 100644 --- a/src/stencil/model/fragments.clj +++ b/src/stencil/model/fragments.clj @@ -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])) @@ -33,7 +33,10 @@ (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 "" content "")))] (->FragmentInvoke {:frag-evaled-parts content}))) @@ -41,4 +44,7 @@ ;; 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)) diff --git a/src/stencil/postprocess/html.clj b/src/stencil/postprocess/html.clj index ae8bc31b..3d680081 100644 --- a/src/stencil/postprocess/html.clj +++ b/src/stencil/postprocess/html.clj @@ -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]] @@ -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: + - 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)) (def legal-tags "Set of supported HTML tags" diff --git a/src/stencil/postprocess/images.clj b/src/stencil/postprocess/images.clj index 89a7ec30..5e1063ef 100644 --- a/src/stencil/postprocess/images.clj +++ b/src/stencil/postprocess/images.clj @@ -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] @@ -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)))) \ No newline at end of file diff --git a/src/stencil/postprocess/links.clj b/src/stencil/postprocess/links.clj index 3b731896..73e5f0fc 100644 --- a/src/stencil/postprocess/links.clj +++ b/src/stencil/postprocess/links.clj @@ -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] @@ -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)))) \ No newline at end of file diff --git a/src/stencil/postprocess/table.clj b/src/stencil/postprocess/table.clj index fea03eb6..405a296e 100644 --- a/src/stencil/postprocess/table.clj +++ b/src/stencil/postprocess/table.clj @@ -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?]])) @@ -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) @@ -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)