diff --git a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
index c76c9c19..af44192c 100644
--- a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
+++ b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
@@ -35,6 +35,12 @@ else if (expr != null && expr.equals(value))
return null;
}
}
+
+ @Override
+ public String getDocumentation() {
+ return "Select a value based on the first argument.\n"
+ + "Usage: `switch(expression, case-1, value-1, case-2, value-2, ..., optional-default-value)`";
+ }
},
/**
@@ -50,6 +56,12 @@ public Object call(Object... arguments) {
return arg;
return null;
}
+
+ @Override
+ public String getDocumentation() {
+ return "Returns the first non-null or non-empty parameter.\n\n"
+ + "Accepts any number of arguments. Ignores null values, empty string and empty collections.";
+ }
},
/**
@@ -67,6 +79,12 @@ public Object call(Object... arguments) {
|| ((x instanceof Collection) && ((Collection) x).isEmpty())
|| ((x instanceof Iterable) && !((Iterable) x).iterator().hasNext());
}
+
+ @Override
+ public String getDocumentation() {
+ return "Returns true iff input is null, empty string or empty collection.\n\n"
+ + "Expects exactly 1 parameter.";
+ }
};
@Override
diff --git a/java-src/io/github/erdos/stencil/functions/Function.java b/java-src/io/github/erdos/stencil/functions/Function.java
index 1cd00821..fe6e938a 100644
--- a/java-src/io/github/erdos/stencil/functions/Function.java
+++ b/java-src/io/github/erdos/stencil/functions/Function.java
@@ -26,4 +26,8 @@ public interface Function {
* @return function identifier
*/
String getName();
+
+ default String getDocumentation() {
+ return "Documentation is not available";
+ }
}
diff --git a/scripts/generate-fun-docs.clj.sh b/scripts/generate-fun-docs.clj.sh
new file mode 100755
index 00000000..246c8835
--- /dev/null
+++ b/scripts/generate-fun-docs.clj.sh
@@ -0,0 +1,38 @@
+#!/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
+
+(require 'stencil.functions)
+
+(defn get-java-functions []
+ (for [f (.listFunctions (new io.github.erdos.stencil.functions.FunctionEvaluator))]
+ {:name (.getName f)
+ :docs (.getDocumentation f)}))
+
+(defn get-clj-functions []
+ (for [[k v] (methods stencil.functions/call-fn)]
+ {:name k
+ :docs (:stencil.functions/docs (meta v))}))
+
+(def all-functions
+ (sort-by :name (concat (get-java-functions) (get-clj-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)
+
+;; Table of Contents
+(doseq [f all-functions]
+ (printf "- [%s](#%s)\n" (:name f) (:name f)))
+
+(println)
+
+(doseq [f all-functions]
+ (printf "## %s\n\n" (:name f))
+ (println (:docs f))
+ (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)
diff --git a/test/stencil/functions_test.clj b/test/stencil/functions_test.clj
index 781f5011..8235cb8c 100644
--- a/test/stencil/functions_test.clj
+++ b/test/stencil/functions_test.clj
@@ -136,4 +136,15 @@
(is (thrown? ExceptionInfo (call-fn "replaceImage" "not data uri")))
(is (thrown? ExceptionInfo (call-fn "replaceImage" "data:image/unknown;base64,XXXXXXX")))
(is (thrown? ExceptionInfo (call-fn "replaceImage" "data:image/png;lalala")))
- (is (thrown? ExceptionInfo (call-fn "replaceImage" "data:image/png;lalala,XXXXXXX")))))
\ No newline at end of file
+ (is (thrown? ExceptionInfo (call-fn "replaceImage" "data:image/png;lalala,XXXXXXX")))))
+
+(deftest test-pageBreak
+ (is (stencil.types/control? (call-fn "pageBreak"))))
+
+(deftest test-xml
+ (is (stencil.types/control? (call-fn "xml" "text")))
+ #_(is (thrown? ExceptionInfo (call-fn "xml" "invalid xml"))))
+
+(deftest test-html
+ (is (stencil.types/control? (call-fn "html" "bold text")))
+ (is (stencil.types/control? (call-fn "html" "one two three four"))))