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: refactor function definitions in clj #165

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
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
18 changes: 18 additions & 0 deletions java-src/io/github/erdos/stencil/functions/BasicFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)`";
}
},

/**
Expand All @@ -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.";
}
},

/**
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions java-src/io/github/erdos/stencil/functions/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ public interface Function {
* @return function identifier
*/
String getName();

default String getDocumentation() {
return "Documentation is not available";
}
}
38 changes: 38 additions & 0 deletions scripts/generate-fun-docs.clj.sh
Original file line number Diff line number Diff line change
@@ -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))
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))
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:
- 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"
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
Loading
Loading