visitLeaf() {
}
@SuppressWarnings("unused")
- class SchemaError {
+ private static final class SchemaError {
private final String path;
private final String msg;
diff --git a/java-src/io/github/erdos/stencil/exceptions/EvalException.java b/java-src/io/github/erdos/stencil/exceptions/EvalException.java
index 46860669..f896e277 100644
--- a/java-src/io/github/erdos/stencil/exceptions/EvalException.java
+++ b/java-src/io/github/erdos/stencil/exceptions/EvalException.java
@@ -5,6 +5,10 @@
*/
public final class EvalException extends RuntimeException {
+ public EvalException(String message, Exception cause) {
+ super(message, cause);
+ }
+
private EvalException(Exception cause) {
super(cause);
}
@@ -12,8 +16,4 @@ private EvalException(Exception cause) {
private EvalException(String message) {
super(message);
}
-
- public static EvalException wrapping(Exception cause) {
- return new EvalException(cause);
- }
}
diff --git a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
index 638776ed..cfb8bc3f 100644
--- a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
+++ b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java
@@ -37,7 +37,7 @@ else if (expr != null && expr.equals(value))
},
/**
- * Returns the first non-null an non-empty value.
+ * Returns the first non-null a non-empty value.
*
* Accepts any arguments. Skips null values, empty strings and empty collections.
*/
diff --git a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java
index 4c08c731..fbf14e5a 100644
--- a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java
+++ b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java
@@ -10,7 +10,7 @@ public enum LocaleFunctions implements Function {
/**
- * Formats number as localized currency. An optional second argument can be used to specify locale code.
+ * Formats number as a localized monetary amount with currency. An optional second argument can be used to specify locale code.
* Returns a string.
*
* Usage: currency(34) or currency(34, "HU")
@@ -23,7 +23,7 @@ public Object call(Object... arguments) throws IllegalArgumentException {
},
/**
- * Formats number as localized percent. An optional second argument can be used to specify locale code.
+ * Formats number as a localized percentage value. An optional second argument can be used to specify locale code.
* Returns a string.
*
* Usage: percent(34) or percent(34, "HU")
diff --git a/java-src/io/github/erdos/stencil/functions/StringFunctions.java b/java-src/io/github/erdos/stencil/functions/StringFunctions.java
index b4b9d695..ca2bb6d8 100644
--- a/java-src/io/github/erdos/stencil/functions/StringFunctions.java
+++ b/java-src/io/github/erdos/stencil/functions/StringFunctions.java
@@ -2,7 +2,6 @@
import java.util.Arrays;
import java.util.Collection;
-import java.util.IllegalFormatException;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -58,7 +57,7 @@ public Object call(Object... arguments) throws IllegalArgumentException {
StringBuilder builder = new StringBuilder();
for (Object argument : arguments) {
if (argument != null)
- builder.append(argument.toString());
+ builder.append(argument);
}
return builder.toString();
}
diff --git a/java-src/io/github/erdos/stencil/impl/DirWatcherTemplateFactory.java b/java-src/io/github/erdos/stencil/impl/DirWatcherTemplateFactory.java
index 41516acf..3370d870 100644
--- a/java-src/io/github/erdos/stencil/impl/DirWatcherTemplateFactory.java
+++ b/java-src/io/github/erdos/stencil/impl/DirWatcherTemplateFactory.java
@@ -180,7 +180,7 @@ public PreparedTemplate prepareTemplateFile(File inputTemplateFile, PrepareOptio
.orElseThrow(() -> new IllegalArgumentException("Can not build template file: " + inputTemplateFile));
}
- private final class DelayedContainer implements Delayed {
+ private static final class DelayedContainer implements Delayed {
private final long expiration;
private final X contents;
diff --git a/java-src/io/github/erdos/stencil/impl/FileHelper.java b/java-src/io/github/erdos/stencil/impl/FileHelper.java
index 75d929d5..1a429fd0 100644
--- a/java-src/io/github/erdos/stencil/impl/FileHelper.java
+++ b/java-src/io/github/erdos/stencil/impl/FileHelper.java
@@ -51,7 +51,7 @@ public static String removeExtension(File f) {
* @return a new file object pointing to a non-existing file in temp directory.
*/
public static File createNonexistentTempFile(String prefix, String suffix) {
- return new File(TEMP_DIRECTORY, prefix + UUID.randomUUID().toString() + suffix);
+ return new File(TEMP_DIRECTORY, prefix + UUID.randomUUID() + suffix);
}
/**
@@ -101,7 +101,7 @@ public static void forceDelete(final File file) {
* @param file to delete, not null
* @throws NullPointerException on null or invalid file
*/
- @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"})
+ @SuppressWarnings({"ConstantConditions"})
public static void forceDeleteOnExit(final File file) {
file.deleteOnExit();
if (file.isDirectory()) {
@@ -113,7 +113,7 @@ public static void forceDeleteOnExit(final File file) {
/**
* Returns a string representation of path with unix separators ("/") instead of the
- * system-dependent separators (which is backslash on windows.)
+ * system-dependent separators (which is backslash on Windows).
*
* @param path not null path object
* @return string of path with slash separators
@@ -128,7 +128,7 @@ public static String toUnixSeparatedString(Path path) {
// on unix systems
return path.toString();
} else {
- // on windows systems we replace backslash with slashes
+ // on Windows systems we replace backslash with slashes
return path.toString().replaceAll(Pattern.quote(separator), "/");
}
}
diff --git a/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java b/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java
index 13c07411..9cae6606 100644
--- a/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java
+++ b/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java
@@ -2,6 +2,7 @@
import clojure.lang.AFunction;
import clojure.lang.IFn;
+import clojure.lang.PersistentHashMap;
import io.github.erdos.stencil.*;
import io.github.erdos.stencil.exceptions.EvalException;
import io.github.erdos.stencil.functions.FunctionEvaluator;
@@ -49,13 +50,15 @@ public EvaluatedDocument render(PreparedTemplate template, Map "Starting document rendering for template " + template.getTemplateFile());
final IFn fn = ClojureHelper.findFunction("eval-template");
- final Map argsMap = makeArgsMap(template.getSecretObject(), fragments, data.getData());
+ final Object argsMap = makeArgsMap(template.getSecretObject(), fragments, data.getData());
final Map result;
try {
result = (Map) fn.invoke(argsMap);
+ } catch (EvalException e) {
+ throw e;
} catch (Exception e) {
- throw EvalException.wrapping(e);
+ throw new EvalException("Unexpected error", e);
}
final Consumer stream = resultWriter(result);
@@ -94,7 +97,7 @@ private static Consumer resultWriter(Map result) {
}
@SuppressWarnings("unchecked")
- private Map makeArgsMap(Object template, Map fragments, Object data) {
+ private Object makeArgsMap(Object template, Map fragments, Object data) {
final Map result = new HashMap();
result.put(ClojureHelper.Keywords.TEMPLATE.kw, template);
result.put(ClojureHelper.Keywords.DATA.kw, data);
@@ -103,9 +106,9 @@ private Map makeArgsMap(Object template, Map fragments
// string to clojure map
final Map kvs = fragments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().getImpl()));
- result.put(ClojureHelper.Keywords.FRAGMENTS.kw, kvs);
+ result.put(ClojureHelper.Keywords.FRAGMENTS.kw, PersistentHashMap.create(kvs));
- return result;
+ return PersistentHashMap.create(result);
}
private final class FunctionCaller extends AFunction {
diff --git a/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java b/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java
index 90fa6dae..a2d88981 100644
--- a/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java
+++ b/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java
@@ -48,18 +48,11 @@ public PreparedFragment prepareFragmentFile(final File fragmentFile, PrepareOpti
final Map prepared;
try {
- //noinspection unchecked
- prepared = (Map) prepareFunction.invoke(fragmentFile, options);
- } catch (ParsingException e) {
+ prepared = invokePrepareFunction(fragmentFile, options);
+ } catch (ParsingException | IOException e) {
throw e;
} catch (Exception e) {
- //noinspection ConstantConditions
- if (e instanceof IOException) {
- // possible because of Clojure magic :-(
- throw (IOException) e;
- } else {
- throw ParsingException.wrapping("Could not parse template file!", e);
- }
+ throw ParsingException.wrapping("Could not parse template file!", e);
}
final File zipDirResource = (File) prepared.get(ClojureHelper.Keywords.SOURCE_FOLDER.kw);
@@ -70,25 +63,31 @@ public PreparedFragment prepareFragmentFile(final File fragmentFile, PrepareOpti
return new PreparedFragment(prepared, zipDirResource);
}
+ @SuppressWarnings({"unchecked", "RedundantThrows"})
+ private static Map invokePrepareFunction(File fragmentFile, PrepareOptions options) throws Exception {
+ final IFn prepareFunction = ClojureHelper.findFunction("prepare-fragment");
+ return (Map) prepareFunction.invoke(fragmentFile, options);
+ }
+
/**
* Retrieves content of :variables keyword from map as a set.
*/
@SuppressWarnings("unchecked")
- private Set variableNames(Map prepared) {
+ private static Set variableNames(Map prepared) {
return prepared.containsKey(ClojureHelper.Keywords.VARIABLES.kw)
- ? unmodifiableSet(new HashSet((Collection) prepared.get(ClojureHelper.Keywords.VARIABLES.kw)))
+ ? unmodifiableSet(new HashSet<>((Collection) prepared.get(ClojureHelper.Keywords.VARIABLES.kw)))
: emptySet();
}
@SuppressWarnings("unchecked")
- private Set fragmentNames(Map prepared) {
+ private static Set fragmentNames(Map prepared) {
return prepared.containsKey(ClojureHelper.Keywords.FRAGMENTS.kw)
- ? unmodifiableSet(new HashSet((Collection) prepared.get(ClojureHelper.Keywords.FRAGMENTS.kw)))
+ ? unmodifiableSet(new HashSet<>((Collection) prepared.get(ClojureHelper.Keywords.FRAGMENTS.kw)))
: emptySet();
}
@SuppressWarnings("unchecked")
- private PreparedTemplate prepareTemplateImpl(TemplateDocumentFormats templateDocFormat, InputStream input, File originalFile, PrepareOptions options) {
+ private static PreparedTemplate prepareTemplateImpl(TemplateDocumentFormats templateDocFormat, InputStream input, File originalFile, PrepareOptions options) {
final IFn prepareFunction = ClojureHelper.findFunction("prepare-template");
final String format = templateDocFormat.name();
diff --git a/java-src/io/github/erdos/stencil/standalone/ArgsParser.java b/java-src/io/github/erdos/stencil/standalone/ArgsParser.java
index fdb57b4c..8fae6464 100644
--- a/java-src/io/github/erdos/stencil/standalone/ArgsParser.java
+++ b/java-src/io/github/erdos/stencil/standalone/ArgsParser.java
@@ -10,7 +10,7 @@
@SuppressWarnings("WeakerAccess")
public class ArgsParser {
- private final Set markers = new HashSet<>();
+ private final Set> markers = new HashSet<>();
public ParamMarker addParam(char shortForm, String longForm, String description, Function parser) {
final ParamMarker added = new ParamMarker<>(parser, shortForm, longForm, description, false);
@@ -20,7 +20,7 @@ public ParamMarker addParam(char shortForm, String longForm, String descr
public ParseResult parse(String... args) {
- final Map result = new HashMap<>();
+ final Map, String> result = new HashMap<>();
final List restArgs = new ArrayList<>(args.length);
@@ -39,7 +39,7 @@ public ParseResult parse(String... args) {
final String argName = parts[0];
final String argValue = parts[1];
- final Map.Entry flag = parsePair(argName, argValue);
+ final Map.Entry, String> flag = parsePair(argName, argValue);
result.put(flag.getKey(), flag.getValue());
} else {
@@ -47,21 +47,21 @@ public ParseResult parse(String... args) {
if (item.startsWith("--no-")) {
// long form
if (markerForLong(item.substring(5)).orElseThrow(() -> new IllegalArgumentException("Unexpected option: " + item)).isFlag()) {
- Map.Entry pair = parsePair(item, null);
+ Map.Entry, String> pair = parsePair(item, null);
result.put(pair.getKey(), pair.getValue());
continue;
}
} else if (item.startsWith("--")) {
// long form
if (markerForLong(item.substring(2)).orElseThrow(() -> new IllegalArgumentException("Unexpected option: " + item)).isFlag()) {
- Map.Entry pair = parsePair(item, null);
+ Map.Entry, String> pair = parsePair(item, null);
result.put(pair.getKey(), pair.getValue());
continue;
}
} else {
// short form
if (markerForShort(item.charAt(1)).orElseThrow(() -> new IllegalArgumentException("Unexpected option: " + item)).isFlag()) {
- Map.Entry pair = parsePair(item, null);
+ Map.Entry, String> pair = parsePair(item, null);
result.put(pair.getKey(), pair.getValue());
continue;
}
@@ -70,10 +70,10 @@ public ParseResult parse(String... args) {
// maybe the next item is a value
final String nextItem = i + 1 < args.length ? args[i + 1] : null;
if (nextItem == null || nextItem.startsWith("-")) {
- final Map.Entry flag = parsePair(item, null);
+ final Map.Entry, String> flag = parsePair(item, null);
result.put(flag.getKey(), flag.getValue());
} else {
- final Map.Entry flag = parsePair(item, nextItem);
+ final Map.Entry, String> flag = parsePair(item, nextItem);
result.put(flag.getKey(), flag.getValue());
}
}
@@ -88,32 +88,32 @@ public ParseResult parse(String... args) {
return new ParseResult(result, restArgs);
}
- private Map.Entry parsePair(String k, String v) {
+ private Map.Entry, String> parsePair(String k, String v) {
if (k.startsWith("--no-") && v == null) {
final String longName = k.substring(5);
- final ParamMarker marker = markerForLong(longName).get();
+ final ParamMarker> marker = markerForLong(longName).get();
return new AbstractMap.SimpleImmutableEntry<>(marker, "false");
} else if (k.startsWith("--")) {
final String longName = k.substring(2);
- final ParamMarker marker = markerForLong(longName).get();
+ final ParamMarker> marker = markerForLong(longName).get();
if (v == null) {
return new AbstractMap.SimpleImmutableEntry<>(marker, "true");
} else {
return new AbstractMap.SimpleImmutableEntry<>(marker, v);
}
} else if (k.startsWith("-") && k.length() == 2 && v == null) {
- final ParamMarker marker = markerForShort(k.charAt(1)).get();
+ final ParamMarker> marker = markerForShort(k.charAt(1)).get();
return new AbstractMap.SimpleImmutableEntry<>(marker, "true");
} else {
throw new IllegalArgumentException("Unexpected key, not a parameter: " + k);
}
}
- private Optional markerForLong(String longName) {
+ private Optional> markerForLong(String longName) {
return markers.stream().filter(x -> x.getLongName().equals(longName)).findAny();
}
- private Optional markerForShort(char shortName) {
+ private Optional> markerForShort(char shortName) {
return markers.stream().filter(x -> x.getShortForm() == shortName).findAny();
}
@@ -138,17 +138,17 @@ public ParamMarker addFlagOption(char shortName, String longName, Strin
}
public static final class ParseResult {
- private final Map args;
+ private final Map, String> args;
private final List varargs;
- private ParseResult(Map args, List varargs) {
+ private ParseResult(Map, String> args, List varargs) {
this.args = unmodifiableMap(args);
this.varargs = unmodifiableList(varargs);
}
public List getRestArgs() {
- return Collections.unmodifiableList(varargs);
+ return varargs;
}
diff --git a/java-src/io/github/erdos/stencil/standalone/JsonParser.java b/java-src/io/github/erdos/stencil/standalone/JsonParser.java
index e280f206..1f7b318d 100644
--- a/java-src/io/github/erdos/stencil/standalone/JsonParser.java
+++ b/java-src/io/github/erdos/stencil/standalone/JsonParser.java
@@ -15,7 +15,7 @@ public final class JsonParser {
/**
* Parses string and returns read object if any.
*/
- @SuppressWarnings({"unchecked", "unused", "WeakerAccess"})
+ @SuppressWarnings({"unused", "WeakerAccess"})
public static Object parse(String contents) throws IOException {
return read(new StringReader(contents));
}
@@ -89,7 +89,7 @@ static Number readNumber(PushbackReader pb) throws IOException {
static String readStr(PushbackReader pb) throws IOException {
expectWord("\"", pb);
- final StringBuffer buf = new StringBuffer();
+ final StringBuilder buf = new StringBuilder();
while (true) {
final int read = pb.read();
diff --git a/java-src/io/github/erdos/stencil/standalone/StandaloneApplication.java b/java-src/io/github/erdos/stencil/standalone/StandaloneApplication.java
index 6937a1ea..edb8d713 100644
--- a/java-src/io/github/erdos/stencil/standalone/StandaloneApplication.java
+++ b/java-src/io/github/erdos/stencil/standalone/StandaloneApplication.java
@@ -1,13 +1,11 @@
package io.github.erdos.stencil.standalone;
-import clojure.lang.IFn;
import clojure.lang.RT;
import clojure.lang.Symbol;
import io.github.erdos.stencil.EvaluatedDocument;
import io.github.erdos.stencil.PrepareOptions;
import io.github.erdos.stencil.PreparedTemplate;
import io.github.erdos.stencil.TemplateData;
-import io.github.erdos.stencil.impl.ClojureHelper;
import io.github.erdos.stencil.impl.FileHelper;
import java.io.BufferedReader;
diff --git a/java-test/io/github/erdos/stencil/IntegrationTest.java b/java-test/io/github/erdos/stencil/IntegrationTest.java
deleted file mode 100644
index 89b6da0b..00000000
--- a/java-test/io/github/erdos/stencil/IntegrationTest.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.github.erdos.stencil;
-
-
-/**
- * Annotates a test that needs a LibreOffice process in the background.
- */
-public interface IntegrationTest {
- /* category marker */
-}
diff --git a/project.clj b/project.clj
index e9ebcdcf..addf9493 100644
--- a/project.clj
+++ b/project.clj
@@ -1,4 +1,4 @@
-(defproject io.github.erdos/stencil-core "0.3.31"
+(defproject io.github.erdos/stencil-core "0.5.1-SNAPSHOT"
:url "https://github.com/erdos/stencil"
:description "Templating engine for office documents."
:license {:name "Eclipse Public License - v 2.0"
@@ -13,10 +13,11 @@
:pom-plugins [[org.apache.maven.plugins/maven-surefire-plugin "2.20"]]
:main io.github.erdos.stencil.Main
:aliases {"junit" ["with-profile" "+test" "test-out" "junit" "junit.xml"]
- "coverage" ["with-profile" "+ci"
- "cloverage" "--codecov"
+ "coverage" ["with-profile" "+ci" "cloverage" "--codecov"
"--exclude-call" "clojure.core/assert"
- "--exclude-call" "stencil.util/fail"]}
+ "--exclude-call" "stencil.util/trace"
+ "--exclude-call" "stencil.util/fail"
+ "--exclude-call" "clojure.spec.alpha/def"]}
:javadoc-opts {:package-names ["stencil"]
:additional-args ["-overview" "java-src/overview.html"
"-top" ""]}
@@ -29,16 +30,20 @@
:filespecs [{:type :bytes, :path "stencil-version", :bytes ~(-> "project.clj" slurp read-string nnext first)}]
:profiles {:uberjar {:aot :all}
:dev {:aot :all
+ :injections [(require 'stencil.spec)
+ (require '[clojure.spec.alpha :as s])]
:dependencies [[org.slf4j/slf4j-simple "1.7.32"]]
:jvm-opts ["-Dorg.slf4j.simpleLogger.defaultLogLevel=debug"]}
:test {:aot :all
- :dependencies [[junit/junit "4.12"]
+ :dependencies [[junit/junit "4.13.2"]
[org.xmlunit/xmlunit-core "2.5.1"]
[hiccup "1.0.5"]]
:plugins [[lein-test-out "0.3.1"]]
:resource-paths ["test-resources"]
:test-paths ["java-test"]
- }
+ :injections [(require 'stencil.spec)
+ (require '[clojure.spec.test.alpha :as sta])
+ (eval '(sta/instrument))]}
:ci {:plugins [[lein-javadoc "0.3.0"]
[lein-cloverage "1.2.2"]]
}})
diff --git a/scripts/extract-wordxml.clj b/scripts/extract-wordxml.clj
new file mode 100755
index 00000000..e26339a4
--- /dev/null
+++ b/scripts/extract-wordxml.clj
@@ -0,0 +1,17 @@
+#!/usr/bin/env bb
+
+(def input (first *command-line-args*))
+(assert input)
+
+(def input-file (clojure.java.io/file input))
+(assert (.exists input-file))
+
+(with-open [fistream (new java.io.FileInputStream input-file)
+ zipstream (new java.util.zip.ZipInputStream fistream)]
+ (doseq [entry (repeatedly #(.getNextEntry zipstream))
+ :while entry
+ :when (= "word/document.xml" (.getName entry))]
+ (-> zipstream
+ (clojure.data.xml/parse)
+ (clojure.data.xml/indent-str)
+ (println))))
diff --git a/service/Dockerfile b/service/Dockerfile
index a5cc0468..201ade69 100644
--- a/service/Dockerfile
+++ b/service/Dockerfile
@@ -12,7 +12,7 @@ COPY . /usr/src/myapp
RUN mv "$(lein uberjar | sed -n 's/^Created \(.*standalone\.jar\)/\1/p')" myapp-standalone.jar
RUN jlink --strip-debug --add-modules "$(jdeps --print-module-deps --ignore-missing-deps myapp-standalone.jar)" --add-modules jdk.localedata --output /java
-FROM debian:10.10-slim
+FROM debian:10-slim
ENV STENCIL_HTTP_PORT 8080
ENV STENCIL_TEMPLATE_DIR /templates
diff --git a/service/README.md b/service/README.md
index 07946072..075a5447 100644
--- a/service/README.md
+++ b/service/README.md
@@ -101,3 +101,27 @@ For example:
```
You can bind the `STENCIL_LOG_LEVEL` environment variable to change the default logging level from `info`. Acceptible values: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
+
+### Custom Functions
+
+It is possible to extend Stencil service with custom functions defined in Javascript. Add a `stencil.js` file in the template directory so it can be read and executed when the service loads.
+The functions defined in the script will be available in the templates when rendered.
+
+The [Rhino](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino) scripting engine is used for evaluating the scripts.
+
+In current version, the script file is not reloaded automatically, therefore a service restart is necessary on changes.
+
+Add a `stencil.js` file in the template directory with the following contents:
+```
+function daysBetweenTimestamps(ts1, ts2) {
+ var time1 = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochSecond(ts1), java.util.TimeZone.getDefault().toZoneId());
+ var time2 = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochSecond(ts2), java.util.TimeZone.getDefault().toZoneId());
+ var duration = java.time.Duration.between(time1, time2);
+ return duration.toDays();
+}
+```
+
+The function now can be called in the templates like the following:
+```
+ {%=daysBetweenTimestamps(1499070300, 1644956311)%}
+```
diff --git a/service/project.clj b/service/project.clj
index a38f8833..7f31cdbc 100644
--- a/service/project.clj
+++ b/service/project.clj
@@ -1,12 +1,13 @@
-(defproject io.github.erdos/stencil-service "0.3.31"
+(defproject io.github.erdos/stencil-service "0.5.1-SNAPSHOT"
:description "Web service for the Stencil templating engine"
:url "https://github.com/erdos/stencil"
:license {:name "Eclipse Public License - v 2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
- :dependencies [[org.clojure/clojure "1.10.2-alpha2"]
- [io.github.erdos/stencil-core "0.3.31"]
- [org.slf4j/slf4j-api "2.0.0-alpha5"]
+ :dependencies [[org.clojure/clojure "1.11.1"]
+ [io.github.erdos/stencil-core "0.5.1-SNAPSHOT"]
+ [org.slf4j/slf4j-api "2.0.0-alpha7"]
+ [org.mozilla/rhino-engine "1.7.14"]
[http-kit "2.5.0"]
- [ring/ring-json "0.4.0"]]
+ [ring/ring-json "0.5.0"]]
:aot :all
:main stencil.service)
diff --git a/service/src/stencil/service.clj b/service/src/stencil/service.clj
index 255f06c6..7b644923 100644
--- a/service/src/stencil/service.clj
+++ b/service/src/stencil/service.clj
@@ -3,10 +3,11 @@
(:import [java.io File])
(:require [org.httpkit.server :refer [run-server]]
[stencil.api :as api]
+ [stencil.functions]
[stencil.log :as log]
[stencil.slf4j :as slf4j]
[clojure.data :refer [diff]]
- [clojure.java.io :refer [file]]
+ [clojure.java.io :as io :refer [file]]
[ring.middleware.json :refer [wrap-json-body]]))
(set! *warn-on-reflection* true)
@@ -23,6 +24,21 @@
dir)
(throw (ex-info "Missing STENCIL_TEMPLATE_DIR property!" {}))))
+(defn eval-js-file []
+ (let [f (file (get-template-dir) "stencil.js")]
+ (when (.exists f)
+ (log/info "Evaluating stencil.js file")
+ (let [manager (new javax.script.ScriptEngineManager)
+ engine (.getEngineByName manager "rhino")
+ invocable ^javax.script.Invocable engine
+ context (.getContext engine)]
+ (with-open [r (io/reader f)]
+ (.eval engine r context))
+ (doseq [[k] (.getBindings context javax.script.ScriptContext/ENGINE_SCOPE)]
+ (log/info "Defining function created in stencil.js file: {}" k)
+ (defmethod stencil.functions/call-fn k [k & args]
+ (.invokeFunction invocable k (into-array Object args))))))))
+
(def -prepared
"Map of {file-name {timestamp prepared}}."
(doto (atom {})
@@ -47,6 +63,9 @@
(prepared template)
(throw (ex-info "Template file does not exist!" {:status 404})))))
+(defn- exception->str [e]
+ (clojure.string/join "\n" (for [e (iterate #(.getCause %) e) :while e] (.getMessage e))))
+
(defn wrap-err [handler]
(fn [request]
(try (handler request)
@@ -56,7 +75,10 @@
{:status status
:body (str "ERROR: " (.getMessage e))})
(do (log/error "Error" e)
- (throw e)))))))
+ (throw e))))
+ (catch Exception e
+ {:status 400
+ :body (exception->str e)}))))
(defn- wrap-log [handler]
(fn [req]
@@ -94,6 +116,7 @@
(defn -main [& args]
(log/info "Starting Stencil Service {}" api/version)
+ (eval-js-file)
(let [http-port (get-http-port)
template-dir ^File (get-template-dir)
server (run-server app {:port http-port})]
diff --git a/service/src/stencil/slf4j.clj b/service/src/stencil/slf4j.clj
index ab9a5372..682fd810 100644
--- a/service/src/stencil/slf4j.clj
+++ b/service/src/stencil/slf4j.clj
@@ -22,7 +22,7 @@
(defn- msg+args [^String msg args]
(if (empty? args)
msg
- (apply str (interleave (.split msg "\\{\\}" -1) (map str args)))))
+ (apply str (interleave (.split msg "\\{\\}" -1) args))))
(deftype StencilLoggerFactory []
org.slf4j.ILoggerFactory
diff --git a/src/stencil/api.clj b/src/stencil/api.clj
index 734a02bd..8ef589f0 100644
--- a/src/stencil/api.clj
+++ b/src/stencil/api.clj
@@ -2,20 +2,14 @@
"A simple public API for document generation from templates."
(:require [clojure.walk :refer [stringify-keys]]
[clojure.java.io :as io]
- [clojure.pprint :refer [simple-dispatch]]
[stencil.types])
(:import [io.github.erdos.stencil API PreparedFragment PreparedTemplate TemplateData]
- [stencil.types OpenTag CloseTag TextTag]
[java.util Map]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
-(defmethod simple-dispatch OpenTag [t] (print (str "<" (:open t) ">")))
-(defmethod simple-dispatch CloseTag [t] (print (str "" (:close t) ">")))
-(defmethod simple-dispatch TextTag [t] (print (str "'" (:text t) "'")))
-
(defn ^PreparedTemplate prepare
"Creates a prepared template instance from an input document."
[input]
diff --git a/src/stencil/cleanup.clj b/src/stencil/cleanup.clj
index 39055c4e..78336a79 100644
--- a/src/stencil/cleanup.clj
+++ b/src/stencil/cleanup.clj
@@ -8,12 +8,10 @@
valid XML String -> tokens -> Annotated Control AST -> Normalized Control AST -> Evaled AST -> Hiccup or valid XML String
"
(:require [stencil.util :refer [mod-stack-top-conj mod-stack-top-last parsing-exception stacks-difference-key]]
- [stencil.types :refer [open-tag close-tag ->CloseTag]]))
+ [stencil.types :refer [open-tag close-tag]]))
(set! *warn-on-reflection* true)
-(declare control-ast-normalize)
-
(defn- tokens->ast-step [[queue & ss0 :as stack] token]
(case (:cmd token)
(:if :for) (conj (mod-stack-top-conj stack token) [])
@@ -22,14 +20,14 @@
(if (empty? ss0)
(throw (parsing-exception (str open-tag "else" close-tag)
"Unexpected {%else%} tag, it must come right after a condition!"))
- (conj (mod-stack-top-last ss0 update :blocks (fnil conj []) {:children queue}) []))
+ (conj (mod-stack-top-last ss0 update ::blocks (fnil conj []) {::children queue}) []))
:else-if
(if (empty? ss0)
- (throw (parsing-exception (str open-tag "else" close-tag)
- "Unexpected {%else%} tag, it must come right after a condition!"))
+ (throw (parsing-exception (str open-tag "else if" close-tag)
+ "Unexpected {%else if%} tag, it must come right after a condition!"))
(-> ss0
- (mod-stack-top-last update :blocks (fnil conj []) {:children queue})
+ (mod-stack-top-last update ::blocks (fnil conj []) {::children queue})
(conj [(assoc token :cmd :if :r true)])
(conj [])))
@@ -38,7 +36,7 @@
(throw (parsing-exception (str open-tag "end" close-tag)
"Too many {%end%} tags!"))
(loop [[queue & ss0] stack]
- (let [new-stack (mod-stack-top-last ss0 update :blocks conj {:children queue})]
+ (let [new-stack (mod-stack-top-last ss0 update ::blocks conj {::children queue})]
(if (:r (peek (first new-stack)))
(recur (mod-stack-top-last new-stack dissoc :r))
new-stack))))
@@ -55,88 +53,87 @@
"Missing {%end%} tag from document!"))
(first result))))
-(defn nested-tokens-fmap-postwalk
- "Melysegi bejaras egy XML fan.
-
- https://en.wikipedia.org/wiki/Depth-first_search"
- [f-cmd-block-before f-cmd-block-after f-child nested-tokens]
- (let [update-children
- #(update % :children
- (partial nested-tokens-fmap-postwalk
- f-cmd-block-before f-cmd-block-after
- f-child))]
- (vec
- (for [token nested-tokens]
- (if (:cmd token)
- (update token :blocks
- (partial mapv
- (comp (partial f-cmd-block-after token)
- update-children
- (partial f-cmd-block-before token))))
- (f-child token))))))
+(defn- nested-tokens-fmap-postwalk
+ "Depth-first traversal of the tree."
+ [f-cmd-block-before f-cmd-block-after f-child node]
+ (assert (map? node))
+ (letfn [(children-mapper [children]
+ (mapv update-blocks children))
+ (update-children [node]
+ (update node ::children children-mapper))
+ (visit-block [block]
+ (-> block f-cmd-block-before update-children f-cmd-block-after))
+ (blocks-mapper [blocks]
+ (mapv visit-block blocks))
+ (update-blocks [node]
+ (if (:cmd node)
+ (update node ::blocks blocks-mapper)
+ (f-child node)))]
+ (update-blocks node)))
(defn annotate-environments
- "Vegigmegy minden tokenen es a parancs blokkok :before es :after kulcsaiba
- beleteszi az adott token kornyezetet."
+ "Puts the context of each element into its :before and :after keys."
[control-ast]
+ (assert (sequential? control-ast))
(let [stack (volatile! ())]
- (nested-tokens-fmap-postwalk
- (fn before-cmd-block [_ block]
- (assoc block :before @stack))
-
- (fn after-cmd-block [_ block]
- (let [stack-before (:before block)
- [a b] (stacks-difference-key :open stack-before @stack)]
- (assoc block :before a :after b)))
-
- (fn child [item]
- (cond
- (:open item)
- (vswap! stack conj item)
-
- (:close item)
- (if (= (:close item) (:open (first @stack)))
- (vswap! stack next)
- (throw (ex-info "Unexpected stack state" {:stack @stack, :item item}))))
- item)
- control-ast)))
+ (mapv (partial nested-tokens-fmap-postwalk
+ (fn before-cmd-block [block]
+ (assoc block ::before @stack))
+
+ (fn after-cmd-block [block]
+ (let [stack-before (::before block)
+ [a b] (stacks-difference-key :open stack-before @stack)]
+ (assoc block ::before a ::after b)))
+
+ (fn child [item]
+ (cond
+ (:open item)
+ (vswap! stack conj item)
+
+ (:close item)
+ (if (= (:close item) (:open (first @stack)))
+ (vswap! stack next)
+ (throw (ex-info "Unexpected stack state" {:stack @stack, :item item}))))
+ item))
+ control-ast)))
(defn stack-revert-close
- "Megfordítja a listát es az :open elemeket :close elemekre kicseréli."
- [stack] (reduce (fn [stack item] (if (:open item) (conj stack (->CloseTag (:open item))) stack)) () stack))
+ "Creates a seq of :close tags for each :open tag in the list in reverse order."
+ [stack]
+ (into () (comp (keep :open) (map #(do {:close %}))) stack))
;; egy {:cmd ...} parancs objektumot kibont:
;; a :blocks kulcs alatt levo elemeket normalizalja es specialis kulcsok alatt elhelyezi
;; igy amikor vegrehajtjuk a parancs objektumot, akkor az eredmeny is
;; valid fa lesz, tehat a nyito-bezaro tagek helyesen fognak elhelyezkedni.
-(defmulti control-ast-normalize-step :cmd)
+(defmulti control-ast-normalize :cmd)
;; Itt nincsen blokk, amit normalizálni kellene
-(defmethod control-ast-normalize-step :echo [echo-command] echo-command)
+(defmethod control-ast-normalize :echo [echo-command] echo-command)
-(defmethod control-ast-normalize-step :cmd/include [include-command]
+(defmethod control-ast-normalize :cmd/include [include-command]
(if-not (string? (:name include-command))
(throw (parsing-exception (pr-str (:name include-command))
"Parameter of include call must be a single string literal!"))
include-command))
;; A feltételes elágazásoknál mindig generálunk egy javított THEN ágat
-(defmethod control-ast-normalize-step :if [control-ast]
- (case (count (:blocks control-ast))
- 2 (let [[then else] (:blocks control-ast)
- then2 (concat (keep control-ast-normalize (:children then))
- (stack-revert-close (:before else))
- (:after else))
- else2 (concat (stack-revert-close (:before then))
- (:after then)
- (keep control-ast-normalize (:children else)))]
- (-> (dissoc control-ast :blocks)
+(defmethod control-ast-normalize :if [control-ast]
+ (case (count (::blocks control-ast))
+ 2 (let [[then else] (::blocks control-ast)
+ then2 (concat (map control-ast-normalize (::children then))
+ (stack-revert-close (::before else))
+ (::after else))
+ else2 (concat (stack-revert-close (::before then))
+ (::after then)
+ (map control-ast-normalize (::children else)))]
+ (-> (dissoc control-ast ::blocks)
(assoc :then (vec then2) :else (vec else2))))
- 1 (let [[then] (:blocks control-ast)
- else (:after then)]
- (-> (dissoc control-ast :blocks)
- (assoc :then (vec (keep control-ast-normalize (:children then))) :else else)))
+ 1 (let [[then] (::blocks control-ast)
+ else (::after then)]
+ (-> (dissoc control-ast ::blocks)
+ (assoc :then (mapv control-ast-normalize (::children then)) :else (vec else))))
;; default
(throw (parsing-exception (str open-tag "else" close-tag)
"Too many {%else%} tags in one condition!"))))
@@ -146,79 +143,73 @@
;; - body-run-once: a body resz eloszor fut le, ha a lista legalabb egy elemu
;; - body-run-next: a body resz masodik, harmadik, stb. beillesztese, haa lista legalabb 2 elemu.
;; Ezekbol az esetekbol kell futtataskor a megfelelo(ket) kivalasztani es behelyettesiteni.
-(defmethod control-ast-normalize-step :for [control-ast]
- (when-not (= 1 (count (:blocks control-ast)))
+(defmethod control-ast-normalize :for [control-ast]
+ (when-not (= 1 (count (::blocks control-ast)))
(throw (parsing-exception (str open-tag "else" close-tag)
"Unexpected {%else%} in a loop!")))
- (let [[{:keys [children before after]}] (:blocks control-ast)
- children (keep control-ast-normalize children)]
+ (let [[{::keys [children before after]}] (::blocks control-ast)
+ children (mapv control-ast-normalize children)]
(-> control-ast
- (dissoc :blocks)
+ (dissoc ::blocks)
(assoc :body-run-none (vec (concat (stack-revert-close before) after))
:body-run-once (vec children)
:body-run-next (vec (concat (stack-revert-close after) before children))))))
-(defn control-ast-normalize
- "Mélységi bejárással rekurzívan normalizálja az XML fát."
- [control-ast]
- (cond
- (vector? control-ast) (vec (flatten (keep control-ast-normalize control-ast)))
- (:text control-ast) control-ast
- (:open control-ast) control-ast
- (:close control-ast) control-ast
- (:cmd control-ast) (control-ast-normalize-step control-ast)
- (:open+close control-ast) control-ast
- :else (throw (ex-info (str "Unexpected object: " (type control-ast)) {:ast control-ast}))))
+(defmethod control-ast-normalize :default [control-ast]
+ (assert (not (::blocks control-ast)))
+ control-ast)
(defn find-variables [control-ast]
;; meg a normalizalas lepes elott
;; amikor van benne blocks
;; mapping: {Sym -> Str}
(letfn [(resolve-sym [mapping s]
- (assert (map? mapping))
- (assert (symbol? s))
- ;; megprobal egy adott szimbolumot a mapping alapjan rezolvalni.
- ;; visszaad egy stringet
- (if (.contains (name s) ".")
- (let [[p1 p2] (vec (.split (name s) "\\." 2))]
- (if-let [pt (mapping (symbol p1))]
- (str pt "." p2)
- (name s)))
- (mapping s (name s))))
- (expr [mapping rpn]
- (assert (sequential? rpn)) ;; RPN kifejezes kell legyen
- (keep (partial resolve-sym mapping) (filter symbol? rpn)))
- ;; iff rpn expr consists of 1 variable only -> resolves that one variable.
- (maybe-variable [mapping rpn]
- (when (and (= 1 (count rpn)) (symbol? (first rpn)))
- (resolve-sym mapping (first rpn))))
+ (assert (map? mapping))
+ (assert (symbol? s))
+ (mapping s (name s)))
+ (expr [mapping e]
+ (cond (symbol? e) [(resolve-sym mapping e)]
+ (not (sequential? e)) nil
+ (= :fncall (first e)) (mapcat (partial expr mapping) (nnext e))
+ (= :get (first e)) (let [[ss rest] (split-with string? (nnext e))]
+ (cons
+ (reduce (fn [root item] (str root "." item))
+ (resolve-sym mapping (second e))
+ ss)
+ (mapcat (partial expr mapping) rest)))
+ :else (mapcat (partial expr mapping) (next e))))
+ (maybe-variable [mapping e]
+ (cond (symbol? e)
+ (resolve-sym mapping e)
+ (and (sequential? e) (= :get (first e)) (symbol? (second e)) (every? string? (nnext e)))
+ (reduce (fn [a b] (str a "." b)) (resolve-sym mapping (second e)) (nnext e))))
(collect [m xs] (mapcat (partial collect-1 m) xs))
(collect-1 [mapping x]
(case (:cmd x)
:echo (expr mapping (:expression x))
:if (concat (expr mapping (:condition x))
- (collect mapping (apply concat (:blocks x))))
+ (collect mapping (apply concat (::blocks x))))
:for (let [variable (maybe-variable mapping (:expression x))
exprs (expr mapping (:expression x))
mapping (if variable
(assoc mapping (:variable x) (str variable "[]"))
mapping)]
- (concat exprs (collect mapping (apply concat (:blocks x)))))
+ (concat exprs (collect mapping (apply concat (::blocks x)))))
[]))]
(distinct (collect {} control-ast))))
(defn- find-fragments [control-ast]
;; returns a set of fragment names use in this document
- (set (for [item (tree-seq map? (comp flatten :blocks) {:blocks [control-ast]})
+ (set (for [item (tree-seq map? (comp flatten ::blocks) {::blocks [control-ast]})
:when (map? item)
:when (= :cmd/include (:cmd item))]
(:name item))))
(defn process [raw-token-seq]
(let [ast (tokens->ast raw-token-seq)
- executable (control-ast-normalize (annotate-environments ast))]
+ executable (mapv control-ast-normalize (annotate-environments ast))]
{:variables (find-variables ast)
:fragments (find-fragments ast)
:dynamic? (boolean (some :cmd executable))
diff --git a/src/stencil/eval.clj b/src/stencil/eval.clj
index 02993e69..7f239590 100644
--- a/src/stencil/eval.clj
+++ b/src/stencil/eval.clj
@@ -4,6 +4,7 @@
[stencil.infix :refer [eval-rpn]]
[stencil.types :refer [control?]]
[stencil.tokenizer :as tokenizer]
+ [stencil.util :refer [eval-exception]]
[stencil.tree-postprocess :as tree-postprocess]))
(set! *warn-on-reflection* true)
@@ -12,35 +13,45 @@
(defmethod eval-step :default [_ _ item] [item])
+(defn normal-control-ast->evaled-seq [data function items]
+ (assert (map? data))
+ (assert (ifn? function))
+ (assert (or (nil? items) (sequential? items)))
+ (eduction (mapcat (partial eval-step function data)) items))
+
+(defn- eval-rpn* [data function expr raw-expr]
+ (try (eval-rpn data function expr)
+ (catch Exception e
+ (throw (eval-exception (str "Error evaluating expression: " raw-expr) e)))))
+
(defmethod eval-step :if [function data item]
- (let [condition (eval-rpn data function (:condition item))]
+ (let [condition (eval-rpn* data function (:condition item) (:raw item))]
(log/trace "Condition {} evaluated to {}" (:condition item) condition)
- (if condition
- (mapcat (partial eval-step function data) (:then item))
- (mapcat (partial eval-step function data) (:else item)))))
+ (->> (if condition (:then item) (:else item))
+ (normal-control-ast->evaled-seq data function))))
(defmethod eval-step :echo [function data item]
- (let [value (eval-rpn data function (:expression item))]
+ (let [value (eval-rpn* data function (:expression item) (:raw item))]
(log/trace "Echoing {} as {}" (:expression item) value)
[{:text (if (control? value) value (str value))}]))
(defmethod eval-step :for [function data item]
- (let [items (seq (eval-rpn data function (:expression item)))]
+ (let [items (eval-rpn* data function (:expression item) (:raw item))]
(log/trace "Loop on {} will repeat {} times" (:expression item) (count items))
- (if (seq items)
- (let [datas (map #(assoc data (name (:variable item)) %) items)
+ (if (not-empty items)
+ (let [index-var-name (name (:index-var item))
+ loop-var-name (name (:variable item))
+ datamapper (fn [key val] (assoc data, index-var-name key, loop-var-name val))
+ datas (if (or (instance? java.util.Map items) (map? items))
+ (map datamapper (keys items) (vals items))
+ (map-indexed datamapper items))
bodies (cons (:body-run-once item) (repeat (:body-run-next item)))]
- (mapcat (fn [data body] (mapcat (partial eval-step function data) body)) datas bodies))
+ (mapcat (fn [data body] (normal-control-ast->evaled-seq data function body)) datas bodies))
(:body-run-none item))))
-(defn normal-control-ast->evaled-seq [data function items]
- (assert (map? data))
- (assert (ifn? function))
- (assert (or (nil? items) (sequential? items)))
- (mapcat (partial eval-step function data) items))
-
(defn eval-executable [part data functions]
- (assert (:executable part))
- (tree-postprocess/postprocess
- (tokenizer/tokens-seq->document
- (normal-control-ast->evaled-seq data functions (:executable part)))))
+ (->> (:executable part)
+ (#(doto % assert))
+ (normal-control-ast->evaled-seq data functions)
+ (tokenizer/tokens-seq->document)
+ (tree-postprocess/postprocess)))
diff --git a/src/stencil/functions.clj b/src/stencil/functions.clj
index 01360417..d30721b9 100644
--- a/src/stencil/functions.clj
+++ b/src/stencil/functions.clj
@@ -1,7 +1,8 @@
(ns stencil.functions
"Function definitions"
(:require [clojure.string]
- [stencil.types :refer [->HideTableColumnMarker ->HideTableRowMarker]]
+ [stencil.ooxml :as ooxml]
+ [stencil.types :refer [->HideTableColumnMarker ->HideTableRowMarker ->FragmentInvoke]]
[stencil.util :refer [fail find-first]]))
(set! *warn-on-reflection* true)
@@ -18,7 +19,7 @@
([_ x y z] (range x y z)))
(defmethod call-fn "integer" [_ n] (some-> n biginteger))
-(defmethod call-fn "decimal" [_ f] (some-> f bigdec))
+(defmethod call-fn "decimal" [_ 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
@@ -50,7 +51,7 @@
(string? value) (first value)
:else (char (int value)))
("d" "o" "x" "X") (some-> value biginteger)
- ("e" "E" "f" "g" "G" "a" "A") (some-> value bigdec)
+ ("e" "E" "f" "g" "G" "a" "A") (with-precision 8 (some-> value bigdec))
value)))
(to-array)
(String/format locale pattern-str)))))
@@ -97,7 +98,7 @@
(instance? java.util.List e)))]
(fail "Wrong data, expected sequence, got: " {:data e}))
(mapcat seq elems))
- (do (doseq [e elems :when (not (or (nil? e) (map? e)))]
+ (do (doseq [e elems :when (not (or (nil? e) (map? e) (instance? java.util.Map e)))]
(fail "Wrong data, expected map, got: " {:data e}))
(keep (partial lookup p) elems))))
data
@@ -108,3 +109,11 @@
0 ""
1 (str (first elements))
(str (clojure.string/join separator1 (butlast elements)) separator2 (last elements))))
+
+(defmethod call-fn "replace" [_ text pattern replacement]
+ (clojure.string/replace (str text) (str pattern) (str replacement)))
+
+;; 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))
diff --git a/src/stencil/grammar.clj b/src/stencil/grammar.clj
new file mode 100644
index 00000000..cf8be23b
--- /dev/null
+++ b/src/stencil/grammar.clj
@@ -0,0 +1,89 @@
+(ns stencil.grammar)
+
+(defn- guarded [pred]
+ (fn [t]
+ (when (pred (first t))
+ [(first t) (next t)])))
+
+;; left-associative chained infix expression
+(defn- chained [reader reader* reducer]
+ (fn [tokens] chained
+ (when-let [[result tokens] (reader tokens)]
+ (loop [tokens tokens
+ result result]
+ (if (empty? tokens)
+ [result nil]
+ (if-let [[fs tokens] (reader* tokens)]
+ (recur tokens (reducer result fs))
+ [result tokens]))))))
+
+(defn- read-or-throw [reader tokens]
+ (or (reader tokens)
+ (throw (ex-info (str "Invalid stencil expression!") {:reader reader :prefix tokens}))))
+
+(defn- all [condition & readers]
+ (fn [tokens]
+ (when-let [[result tokens] (condition tokens)]
+ (reduce (fn [[result tokens] reader]
+ (let [[r tokens] (read-or-throw reader tokens)]
+ [(conj result r) tokens]))
+ [[result] tokens] readers))))
+
+(defmacro ^:private grammar [bindings body]
+ `(letfn* [~@(for [[k v] (partition 2 bindings), x [k (list 'fn '[%] (list v '%))]] x)] ~body))
+
+(defn- mapping [reader mapper]
+ (fn [tokens]
+ (when-let [[result tokens] (reader tokens)]
+ [(mapper result) tokens])))
+
+(defn- parenthesed [reader]
+ (mapping (all (guarded #{:open}) reader (guarded #{:close})) second))
+
+(defn- op-chain [operand operator]
+ (chained operand (all operator operand) (fn [a [op b]] (list op a b))))
+
+(defn- op-chain-r [operand operator]
+ (mapping (chained (all operand) (all operator operand) (fn [a [op b]] (list* b op a)))
+ (fn [a] (reduce (fn [a [op c]] [op c a]) (first a) (partition 2 (next a))))))
+
+(defn- at-least-one [reader]
+ (fn [tokens]
+ (when-let [[result tokens] (reader tokens)]
+ (loop [tokens tokens, result [result]]
+ (if-let [[res tokens] (reader tokens)]
+ (recur tokens (conj result res))
+ [result tokens])))))
+
+(defn- optional [reader] ;; always matches
+ (fn [t] (or (reader t) [nil t])))
+
+(def testlang
+ (grammar [val (some-fn iden-or-fncall
+ (parenthesed expression)
+ (guarded number?)
+ (guarded string?))
+ iden (guarded symbol?)
+ dotted (mapping (all (guarded #{:dot}) iden) (comp name second))
+ bracketed (mapping (all (guarded #{:open-bracket}) expression (guarded #{:close-bracket})) second)
+ args (mapping (optional (chained (all expression) (all (guarded #{:comma}) expression) into))
+ (fn [x] (take-nth 2 x)))
+ args-suffix (parenthesed args)
+ iden-or-fncall (mapping (all iden (optional args-suffix))
+ (fn [[id xs]] (if xs (list* :fncall id xs) id)))
+ accesses (mapping (all val (optional (at-least-one (some-fn bracketed dotted))))
+ (fn [[id chain]] (if chain (list* :get id chain) id)))
+ neg (some-fn (all (guarded #{:minus}) neg) accesses)
+ not (some-fn (all (guarded #{:not}) not) neg)
+ pow (op-chain-r not (guarded #{:power}))
+ mul (op-chain pow (guarded #{:times :divide :mod}))
+ add (op-chain mul (guarded #{:plus :minus}))
+ cmp (op-chain add (guarded #{:lt :gt :lte :gte}))
+ cmpe (op-chain cmp (guarded #{:eq :neq}))
+ and (op-chain cmpe (guarded #{:and}))
+ or (op-chain and (guarded #{:or}))
+ expression or]
+ expression))
+
+(defn runlang [grammar input]
+ (ffirst (read-or-throw (all grammar {nil []}) input)))
\ No newline at end of file
diff --git a/src/stencil/infix.clj b/src/stencil/infix.clj
index 06fd94bb..5767ab6a 100644
--- a/src/stencil/infix.clj
+++ b/src/stencil/infix.clj
@@ -2,16 +2,14 @@
"Parsing and evaluating infix algebraic expressions.
https://en.wikipedia.org/wiki/Shunting-yard_algorithm"
- (:require [stencil.util :refer [fail update-peek ->int]]
- [stencil.log :as log]
- [stencil.functions :refer [call-fn]]))
+ (:require [stencil.util :refer [->int string whitespace?]]
+ [stencil.functions :refer [call-fn]]
+ [stencil.grammar :as grammar]))
(set! *warn-on-reflection* true)
(def ^:dynamic ^:private *calc-vars* {})
-(defrecord FnCall [fn-name])
-
(def ops
{\+ :plus
\- :minus
@@ -28,7 +26,10 @@
\< :lt
\> :gt
\& :and
- \| :or})
+ \| :or
+ \, :comma \; :comma
+ \. :dot
+ })
(def ops2 {[\> \=] :gte
[\< \=] :lte
@@ -43,36 +44,7 @@
(def identifier
"Characters found in an identifier"
- (set "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_.1234567890"))
-
-(def operation-tokens
- "Operator precedences.
-
- source: http://en.cppreference.com/w/cpp/language/operator_precedence
- "
- {:open -999
- ;;;:close -998
- :comma -998
- :open-bracket -999
-
- :or -21
- :and -20
-
- :eq -10 :neq -10,
-
- :lt -9 :gt -9 :lte -9 :gte -9
-
- :plus 2 :minus 2
- :times 3 :divide 4
- :power 5
- :not 6
- :neg 7})
-
-(defn- precedence [token]
- (get operation-tokens token))
-
-(defn- associativity [token]
- (if (#{:power :not :neg} token) :right :left))
+ (set "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_1234567890"))
(def ^:private quotation-marks
{\" \" ;; programmer quotes
@@ -88,178 +60,87 @@
- First elem is read string literal.
- Second elem is seq of remaining characters."
[characters]
- (let [until (quotation-marks (first characters))
- sb (new StringBuilder)]
- (loop [[c & cs] (next characters)]
- (cond (nil? c) (throw (ex-info "String parse error"
- {:reason "Unexpected end of stream"}))
- (= c until) [(.toString sb) cs]
- (= c (first "\\")) (do (.append sb (first cs)) (recur (next cs)))
- :else (do (.append sb c) (recur cs))))))
+ (when-let [until (quotation-marks (first characters))]
+ (let [sb (new StringBuilder)]
+ (loop [[c & cs] (next characters)]
+ (cond (nil? c) (throw (ex-info "String parse error"
+ {:reason "Unexpected end of stream"}))
+ (= c until) [(.toString sb) cs]
+ (= c (first "\\")) (do (.append sb (first cs)) (recur (next cs)))
+ :else (do (.append sb c) (recur cs)))))))
(defn read-number
"Reads a number literal from a sequence. Returns a tuple of read
number (Double or Long) and the sequence of remaining characters."
[characters]
- (let [content (take-while (set "1234567890._") characters)
- content ^String (apply str content)
- content (.replaceAll content "_" "")
- number (if (some #{\.} content)
- (Double/parseDouble content)
- (Long/parseLong content))]
- [number (drop (count content) characters)]))
-
-(defn tokenize
- "Returns a sequence of tokens for an input string"
- [original-string]
- (loop [[first-char & next-chars :as characters] (str original-string)
- tokens []]
- (cond
- (empty? characters)
- tokens
-
- (contains? #{\space \tab \newline} first-char)
- (recur next-chars tokens)
-
- (contains? #{\, \;} first-char)
- (recur next-chars (conj tokens :comma))
-
- (contains? ops2 [first-char (first next-chars)])
- (recur (next next-chars) (conj tokens (ops2 [first-char (first next-chars)])))
-
- (and (= \- first-char) (or (nil? (peek tokens)) (and (not= (peek tokens) :close) (keyword? (peek tokens)))))
- (recur next-chars (conj tokens :neg))
-
- (contains? ops first-char)
- (recur next-chars (conj tokens (ops first-char)))
-
- (contains? digits first-char)
- (let [[n tail] (read-number characters)]
- (recur tail (conj tokens n)))
-
- (quotation-marks first-char)
- (let [[s tail] (read-string-literal characters)]
- (recur tail (conj tokens s)))
-
- :else
- (let [content (apply str (take-while identifier characters))]
- (if (seq content)
- (let [tail (drop-while #{\space \tab} (drop (count content) characters))]
- (if (= \( (first tail))
- (recur (next tail) (conj tokens (->FnCall content)))
- (recur tail (conj tokens (symbol content)))))
- (throw (ex-info (str "Unexpected character: " first-char)
- {:character first-char})))))))
+ (when (contains? digits (first characters))
+ (let [content (string (take-while (set "1234567890._")) characters)
+ content (.replaceAll content "_" "")
+ number (if (some #{\.} content)
+ (Double/parseDouble content)
+ (Long/parseLong content))]
+ [number (drop (count content) characters)])))
-;; throws ExceptionInfo when token sequence has invalid elems
-(defn- validate-tokens [tokens]
- (cond
- (some true? (map #(and (or (symbol? %1) (number? %1) (#{:close} %1))
- (or (symbol? %2) (number? %2) (#{:open} %2)))
- tokens (next tokens)))
- (throw (ex-info "Could not parse!" {}))
+(defn- read-ops2 [chars]
+ (when-let [op (get ops2 [(first chars) (second chars)])]
+ [op (nnext chars)]))
- :else
- tokens))
+(defn- read-ops1 [chars]
+ (when-let [op (get ops (first chars))]
+ [op (next chars)]))
-(defn tokens->rpn
- "Classic Shunting-Yard Algorithm extension to handle vararg fn calls."
- [tokens]
- (loop [[e0 & next-expr :as expr] tokens ;; bemeneti token lista
- opstack () ;; stack of Shunting-Yard Algorithm
- result [] ;; Vector of output tokens
+(defn- read-iden [characters]
+ (when-let [content (not-empty (string (take-while identifier) characters))]
+ [(symbol content) (drop (count content) characters)]))
+(def token-readers (some-fn read-number read-string-literal read-iden read-ops2 read-ops1))
- parentheses 0 ;; count of open parentheses
- ;; on a function call we save function name here
- functions ()]
- (cond
- (neg? parentheses)
- (throw (ex-info "Parentheses are not balanced!" {}))
-
- (empty? expr)
- (if (zero? parentheses)
- (into result (remove #{:open} opstack))
- (throw (ex-info "Too many open parentheses!" {})))
-
- (number? e0)
- (recur next-expr opstack (conj result e0) parentheses functions)
-
- (symbol? e0)
- (recur next-expr opstack (conj result e0) parentheses functions)
-
- (string? e0)
- (recur next-expr opstack (conj result e0) parentheses functions)
-
- (= :open e0)
- (recur next-expr (conj opstack :open) result (inc parentheses) (conj functions nil))
-
- (= :open-bracket e0)
- (recur next-expr (conj opstack :open-bracket) result (inc parentheses) functions)
-
- (instance? FnCall e0)
- (recur next-expr (conj opstack :open) result
- (inc parentheses)
- (conj functions {:fn (:fn-name e0)
- :args (if (= :close (first next-expr)) 0 1)}))
- ;; (recur next-expr (conj opstack :fncall) result (conj functions {:fn e0}))
-
- (= :close-bracket e0)
- (let [[popped-ops [_ & keep-ops]]
- (split-with (partial not= :open-bracket) opstack)]
- (recur next-expr
- keep-ops
- (into result (concat popped-ops [:get]))
- (dec parentheses)
- functions))
-
- (= :close e0)
- (let [[popped-ops [_ & keep-ops]]
- (split-with (partial not= :open) opstack)]
- (recur next-expr
- keep-ops
- (into result
- (concat
- (remove #{:comma} popped-ops)
- (some-> functions first vector)))
- (dec parentheses)
- (next functions)))
-
- (empty? next-expr) ;; current is operator but without an operand
- (throw (ex-info "Missing operand!" {}))
-
- :else ;; operator
- (let [[popped-ops keep-ops]
- (split-with #(if (= :left (associativity e0))
- (<= (precedence e0) (precedence %))
- (< (precedence e0) (precedence %))) opstack)]
- (recur next-expr
- (conj keep-ops e0)
- (into result (remove #{:open :comma} popped-ops))
- parentheses
- (if (= :comma e0)
- (if (first functions)
- (update-peek functions update :args inc)
- (throw (ex-info "Unexpected ',' character!" {})))
- functions))))))
-
-(defn- reduce-step-dispatch [_ cmd]
- (cond (string? cmd) :string
- (number? cmd) :number
- (symbol? cmd) :symbol
- (keyword? cmd) cmd
- (map? cmd) FnCall
- :else (fail "Unexpected opcode!" {:opcode cmd})))
-
-(defmulti ^:private reduce-step reduce-step-dispatch)
-(defmulti ^:private action-arity (partial reduce-step-dispatch []))
-
-;; throws exception if there are operators out of place, returns input otherwise
-(defn- validate-rpn [rpn]
- (let [steps (map #(- 1 (action-arity %)) rpn)]
- (if (or (not-every? pos? (reductions + steps)) (not (= 1 (reduce + steps))))
- (throw (ex-info (str "Wrong tokens, unsupported arities: " rpn) {:rpn rpn}))
- rpn)))
+(defn tokenize
+ "Returns a sequence of tokens for an input string"
+ [text]
+ (when-let [text (seq (drop-while (comp whitespace? char) text))]
+ (if-let [[token tail] (token-readers text)]
+ (cons token (lazy-seq (tokenize tail)))
+ (throw (ex-info "Unexpected end of string" {:index (.index ^clojure.lang.IndexedSeq text)})))))
+
+(defmulti eval-tree (fn [tree] (if (sequential? tree) (first tree) (type tree))))
+
+(defmethod eval-tree java.lang.Number [tree] tree)
+(defmethod eval-tree String [s] s)
+(defmethod eval-tree clojure.lang.Symbol [s] (get-in *calc-vars* (vec (.split (name s) "\\."))))
+
+(defmethod eval-tree :eq [[_ a b]] (= (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :neq [[_ a b]] (not= (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :plus [[_ a b]]
+ (let [a (eval-tree a) b (eval-tree b)]
+ (if (or (string? a) (string? b))
+ (str a b)
+ (+ a b))))
+(defmethod eval-tree :minus [[_ a b :as expr]]
+ (if (= 2 (count expr))
+ (- (eval-tree a))
+ (- (eval-tree a) (eval-tree b))))
+(defmethod eval-tree :times [[_ a b]] (* (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :divide [[_ a b]] (with-precision 8 (/ (eval-tree a) (eval-tree b))))
+
+(defmethod eval-tree :or [[_ a b]] (or (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :and [[_ a b]] (and (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :mod [[_ a b]] (mod (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :power [[_ a b]] (Math/pow (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :not [[_ a]] (not (eval-tree a)))
+
+(defmethod eval-tree :gte [[_ a b]] (>= (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :lte [[_ a b]] (<= (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :gt [[_ a b]] (> (eval-tree a) (eval-tree b)))
+(defmethod eval-tree :lt [[_ a b]] (< (eval-tree a) (eval-tree b)))
+
+(defmethod eval-tree :get [[_ m & path]]
+ (reduce (fn [b a]
+ (cond (sequential? b) (when (number? a) (get b (->int a)))
+ (string? b) (when (number? a) (get b (->int a)))
+ (instance? java.util.List b) (when (number? a) (.get ^java.util.List b (->int a)))
+ :else (get b (str a))))
+ (eval-tree m) (map eval-tree path)))
(defmethod call-fn :default [fn-name & args-seq]
(if-let [default-fn (::functions *calc-vars*)]
@@ -270,65 +151,22 @@
;; Example: you can write data()['key1']['key2'] instead of key1.key2.
(defmethod call-fn "data" [_] *calc-vars*)
-(defmethod action-arity FnCall [{:keys [args]}] args)
-
-(defmethod reduce-step FnCall [stack {:keys [fn args]}]
- (try
- (log/trace "Calling function {} with arguments {}" fn args)
- (let [[ops new-stack] (split-at args stack)
- ops (reverse ops)
- result (apply call-fn fn ops)]
- (log/trace "Result was {}" result)
- (conj new-stack result))
- (catch clojure.lang.ArityException e
- (throw (ex-info (str "Wrong arity: " (.getMessage e))
- {:fn fn :expected args :got (count ops) :ops (vec ops)})))))
-
-(defmacro def-reduce-step [cmd args body]
- (assert (keyword? cmd))
- (assert (every? symbol? args))
- `(do (defmethod action-arity ~cmd [_#] ~(count args))
- (defmethod reduce-step ~cmd [[~@args ~'& stack#] action#]
- (let [~'+action+ action#] (conj stack# ~body)))))
-
-(def-reduce-step :string [] +action+)
-(def-reduce-step :number [] +action+)
-(def-reduce-step :symbol [] (get-in *calc-vars* (vec (.split (name +action+) "\\."))))
-
-(def-reduce-step :neg [s0] (- s0))
-(def-reduce-step :times [s0 s1] (* s0 s1))
-(def-reduce-step :divide [s0 s1] (/ s1 s0))
-(def-reduce-step :plus [s0 s1] (if (or (string? s0) (string? s1)) (str s1 s0) (+ s1 s0)))
-(def-reduce-step :minus [s0 s1] (- s1 s0))
-(def-reduce-step :eq [a b] (= a b))
-(def-reduce-step :or [a b] (or b a))
-(def-reduce-step :not [b] (not b))
-(def-reduce-step :and [a b] (and b a))
-(def-reduce-step :neq [a b] (not= a b))
-(def-reduce-step :mod [s0 s1] (mod s1 s0))
-(def-reduce-step :lt [s0 s1] (< s1 s0))
-(def-reduce-step :lte [s0 s1] (<= s1 s0))
-(def-reduce-step :gt [s0 s1] (> s1 s0))
-(def-reduce-step :gte [s0 s1] (>= s1 s0))
-(def-reduce-step :power [s0 s1] (Math/pow s1 s0))
-(def-reduce-step :get [a b]
- (cond (sequential? b) (when (number? a) (get b (->int a)))
- (string? b) (when (number? a) (get b (->int a)))
- (instance? java.util.List b) (when (number? a) (.get ^java.util.List b (->int a)))
- :else (get b (str a))))
+(defmethod eval-tree :fncall [[_ f & args]]
+ (let [args (mapv eval-tree args)]
+ (try (apply call-fn (name f) args)
+ (catch clojure.lang.ArityException e
+ (throw (ex-info (format "Function '%s' was called with a wrong number of arguments (%d)" f (count args))
+ {:fn f :args args}))))))
(defn eval-rpn
- ([bindings default-function tokens]
+ ([bindings default-function tree]
(assert (ifn? default-function))
- (eval-rpn (assoc bindings ::functions default-function) tokens))
- ([bindings tokens]
+ (eval-rpn (assoc bindings ::functions default-function) tree))
+ ([bindings tree]
(assert (map? bindings))
- (assert (seq tokens))
(binding [*calc-vars* bindings]
- (let [result (reduce reduce-step () tokens)]
- (assert (= 1 (count result)))
- (first result)))))
+ (eval-tree tree))))
-(def parse (comp validate-rpn tokens->rpn validate-tokens tokenize))
+(def parse (comp (partial grammar/runlang grammar/testlang) tokenize))
-:OK
+:OK
\ No newline at end of file
diff --git a/src/stencil/merger.clj b/src/stencil/merger.clj
index f9711eb5..ff002af0 100644
--- a/src/stencil/merger.clj
+++ b/src/stencil/merger.clj
@@ -1,11 +1,12 @@
(ns stencil.merger
"Token listaban a text tokenekbol kiszedi a parancsokat es action tokenekbe teszi."
(:require [clojure.data.xml :as xml]
+ [clojure.string :refer [index-of ends-with?]]
[stencil.postprocess.ignored-tag :as ignored-tag]
[stencil
[types :refer [open-tag close-tag]]
[tokenizer :as tokenizer]
- [util :refer [prefixes suffixes subs-last]]]))
+ [util :refer [prefixes suffixes subs-last string parsing-exception]]]))
(set! *warn-on-reflection* true)
@@ -29,17 +30,14 @@
(defn find-first-code [^String s]
(assert (string? s))
- (let [ind (.indexOf s (str open-tag))]
- (when-not (neg? ind)
- (let [after-idx (.indexOf s (str close-tag))]
- (if (neg? after-idx)
- (cond-> {:action-part (.substring s (+ ind (count open-tag)))}
- (not (zero? ind)) (assoc :before (.substring s 0 ind)))
- (cond-> {:action (.substring s (+ ind (count open-tag))
- after-idx)}
- (not (zero? ind)) (assoc :before (.substring s 0 ind))
- (not (= (+ (count close-tag) after-idx) (count s)))
- (assoc :after (.substring s (+ (count close-tag) after-idx)))))))))
+ (when-let [ind (index-of s (str open-tag))]
+ (if-let [after-idx (index-of s (str close-tag) ind)]
+ (cond-> {:action (subs s (+ ind (count open-tag)) after-idx)}
+ (pos? ind) (assoc :before (subs s 0 ind))
+ (not= (+ (count close-tag) after-idx) (count s))
+ (assoc :after (subs s (+ (count close-tag) after-idx))))
+ (cond-> {:action-part (subs s (+ ind (count open-tag)))}
+ (not (zero? ind)) (assoc :before (subs s 0 ind))))))
(defn text-split-tokens [^String s]
(assert (string? s))
@@ -57,36 +55,36 @@
{:tokens (conj output {:text s})}
{:tokens output}))))
-(declare cleanup-runs)
-
;; returns a map of {:char :stack :text-rest :rest}
(defn -find-open-tag [last-chars-count next-token-list]
(assert (integer? last-chars-count))
(assert (pos? last-chars-count))
(assert (sequential? next-token-list))
- (when (= (drop last-chars-count open-tag)
- (take (- (count open-tag) last-chars-count)
- (map :char (peek-next-text next-token-list))))
- (nth (peek-next-text next-token-list)
- (dec (- (count open-tag) last-chars-count)))))
+ (let [next-text (peek-next-text next-token-list)
+ n (- (count open-tag) last-chars-count)]
+ (when (= (drop last-chars-count open-tag)
+ (take n (map :char next-text)))
+ (nth next-text (dec n)))))
(defn -last-chars-count [sts-tokens]
(assert (sequential? sts-tokens))
- (when (:text (last sts-tokens))
- (some #(when (.endsWith
- (str (apply str (:text (last sts-tokens)))) (apply str %))
+ (when-let [last-text (some-> sts-tokens last :text string)]
+ (some #(when (ends-with? last-text (string %))
(count %))
(prefixes open-tag))))
(defn map-action-token [token]
(if-let [action (:action token)]
- (let [parsed (tokenizer/text->cmd action)]
+ (let [parsed (tokenizer/text->cmd action)
+ parsed (assoc parsed :raw (str open-tag action close-tag))]
(if (and *only-includes*
(not= :cmd/include (:cmd parsed)))
{:text (str open-tag action close-tag)}
{:action parsed}))
token))
+(declare cleanup-runs)
+
(defn cleanup-runs-1 [[first-token & rest-tokens]]
(assert (:text first-token))
(let [sts (text-split-tokens (:text first-token))]
@@ -98,19 +96,21 @@
(take (count close-tag) (map :char %)))
(suffixes (peek-next-text next-token-list)))
that (if (empty? that)
- (throw (ex-info "Tag is not closed? " {:read (first this)}))
+ (throw (parsing-exception "" (str "Stencil tag is not closed. Reading " open-tag
+ (string (comp (take 20) (map first) (map :char)) this))))
+ ;; (throw (ex-info "Tag is not closed? " {:read (first this)}))
(first (nth that (dec (count close-tag)))))
; action-content (apply str (map (comp :char first) this))
]
(concat
(map map-action-token (:tokens sts))
- (let [ap (map-action-token {:action (apply str (map (comp :char first) this))})]
+ (let [ap (map-action-token {:action (string (map (comp :char first)) this)})]
(if (:action ap)
(concat
[ap]
(reverse (:stack that))
(if (seq (:text-rest that))
- (lazy-seq (cleanup-runs-1 (cons {:text (apply str (:text-rest that))} (:rest that))))
+ (lazy-seq (cleanup-runs-1 (cons {:text (string (:text-rest that))} (:rest that))))
(lazy-seq (cleanup-runs (:rest that)))))
(list* {:text (str open-tag (:action-part sts))}
(lazy-seq (cleanup-runs rest-tokens)))))))
@@ -124,7 +124,7 @@
[{:text (apply str s)}])
(let [tail (cleanup-runs-1
- (concat [{:text (str open-tag (apply str (:text-rest this)))}]
+ (concat [{:text (apply str open-tag (:text-rest this))}]
(reverse (:stack this))
(:rest this)))]
(if (:action (first tail))
diff --git a/src/stencil/model.clj b/src/stencil/model.clj
index b575bf43..66e0a6bd 100644
--- a/src/stencil/model.clj
+++ b/src/stencil/model.clj
@@ -10,7 +10,7 @@
[stencil.model.numbering :as numbering]
[stencil.types :refer [->FragmentInvoke ->ReplaceImage]]
[stencil.postprocess.images :refer [img-data->extrafile]]
- [stencil.util :refer [unlazy-tree assoc-if-val]]
+ [stencil.util :refer [unlazy-tree assoc-if-val eval-exception]]
[stencil.model.relations :as relations]
[stencil.model.common :refer [unix-path ->xml-writer resource-copier]]
[stencil.functions :refer [call-fn]]
@@ -230,7 +230,7 @@
elem)))
-(defmethod eval/eval-step :cmd/include [_ local-data-map {frag-name :name}]
+(defmethod eval/eval-step :cmd/include [function local-data-map {frag-name :name}]
(assert (map? local-data-map))
(assert (string? frag-name))
(expect-fragment-context!
@@ -239,10 +239,10 @@
style-ids-rename (-> fragment-model :main :style :parsed (doto assert) (style/insert-styles!))
relation-ids-rename (relations/ids-rename fragment-model frag-name)
- relation-rename-map (into {} (map (juxt :old-id :new-id) relation-ids-rename))
+ relation-rename-map (into {} (map (juxt :old-id :new-id)) relation-ids-rename)
;; evaluate
- evaled (eval-template-model fragment-model local-data-map {} {})
+ evaled (eval-template-model fragment-model local-data-map function {})
;; write back
get-xml (fn [x] (or (:xml x) @(:xml-delay x)))
@@ -254,9 +254,7 @@
(swap! *inserted-fragments* conj frag-name)
(run! add-extra-file! relation-ids-rename)
[{:text (->FragmentInvoke {:frag-evaled-parts evaled-parts})}])
- (throw (ex-info "Did not find fragment for name!"
- {:fragment-name frag-name
- :all-fragment-names (set (keys *all-fragments*))})))))
+ (throw (eval-exception (str "No fragment for name: " frag-name) nil)))))
;; replaces the nearest image with the content
diff --git a/src/stencil/model/common.clj b/src/stencil/model/common.clj
index 9234134b..94ac3026 100644
--- a/src/stencil/model/common.clj
+++ b/src/stencil/model/common.clj
@@ -3,8 +3,7 @@
[java.io File]
[io.github.erdos.stencil.impl FileHelper])
(:require [clojure.data.xml :as xml]
- [clojure.java.io :as io]
- [stencil.util :refer :all]))
+ [clojure.java.io :as io]))
(defn ->xml-writer [tree]
diff --git a/src/stencil/model/relations.clj b/src/stencil/model/relations.clj
index a69f7bb8..e18d6583 100644
--- a/src/stencil/model/relations.clj
+++ b/src/stencil/model/relations.clj
@@ -4,7 +4,7 @@
[clojure.java.io :as io :refer [file]]
[stencil.ooxml :as ooxml]
[stencil.util :refer :all]
- [stencil.model.common :refer :all]))
+ [stencil.model.common :refer [->xml-writer]]))
(def tag-relationships
:xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fpackage%2F2006%2Frelationships/Relationships)
diff --git a/src/stencil/model/style.clj b/src/stencil/model/style.clj
index 927e9a6c..8a5b8cf8 100644
--- a/src/stencil/model/style.clj
+++ b/src/stencil/model/style.clj
@@ -89,7 +89,7 @@
(defn main-style-item [^File dir main-document main-document-rels]
- (when-let [main-style (some #(when (= rel-type (:stencil.model/type %)) %)
+ (when-let [main-style (find-first #(= rel-type (:stencil.model/type %))
(vals (:parsed main-document-rels)))]
(let [main-style-file (io/file (.getParentFile (io/file main-document))
(:stencil.model/target main-style))
diff --git a/src/stencil/ooxml.clj b/src/stencil/ooxml.clj
index 414c0243..171680ef 100644
--- a/src/stencil/ooxml.clj
+++ b/src/stencil/ooxml.clj
@@ -1,6 +1,6 @@
(ns stencil.ooxml
"Contains common xml element tags and attributes"
- (:refer-clojure :exclude [val name]))
+ (:refer-clojure :exclude [val name type]))
;; run and properties
(def r :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fwordprocessingml%2F2006%2Fmain/r)
@@ -25,6 +25,8 @@
;; Content Break: http://officeopenxml.com/WPtextSpecialContent-break.php
(def br :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fwordprocessingml%2F2006%2Fmain/br)
+(def type :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fwordprocessingml%2F2006%2Fmain/type)
+
(def w :xmlns.http%3A%2F%2Fschemas.openxmlformats.org%2Fwordprocessingml%2F2006%2Fmain/w)
;; XML space attribute (eg.: preserve)
diff --git a/src/stencil/postprocess/fragments.clj b/src/stencil/postprocess/fragments.clj
index 85ca9a82..a5a80379 100644
--- a/src/stencil/postprocess/fragments.clj
+++ b/src/stencil/postprocess/fragments.clj
@@ -54,7 +54,7 @@
text (:content run)
:when (map? text)
:when (= ooxml/t (:tag text))
- c (:content run)
+ c (:content text)
:when (string? c)
:when (not-empty c)] c))))
@@ -82,7 +82,7 @@
rights1 (zip/rights (zip/up chunk-loc))
;; style of run that is split
- style (some #(when (= ooxml/rPr (:tag %)) %) (:content r))
+ style (find-first #(= ooxml/rPr (:tag %)) (:content r))
->t (fn [xs] {:tag ooxml/t :content (vec xs)})
->run (fn [cts] (assoc r :content (vec (cons style cts))))]
diff --git a/src/stencil/postprocess/html.clj b/src/stencil/postprocess/html.clj
index 5fa4c25f..78469ebb 100644
--- a/src/stencil/postprocess/html.clj
+++ b/src/stencil/postprocess/html.clj
@@ -8,6 +8,8 @@
[stencil.util :refer :all]
[stencil.ooxml :as ooxml]))
+(set! *warn-on-reflection* true)
+
(defrecord HtmlChunk [content] ControlMarker)
(defmethod call-fn "html" [_ content] (->HtmlChunk content))
@@ -16,19 +18,22 @@
"Set of supported HTML tags"
#{:b :em :i :u :s :sup :sub :span :br :strong})
+(defn- kw-lowercase [kw] (-> kw name .toLowerCase keyword))
+
(defn- validate-tags
"Throws ExceptionInfo on invalid HTML tag in tree"
[xml-tree]
(->>
(fn [node]
- (if (legal-tags (:tag node))
+ (if (legal-tags (-> node :tag kw-lowercase))
node
(throw (ex-info (str "Unexpected HTML tag: " (:tag node)) {:tag (:tag node)}))))
(dfs-walk-xml xml-tree map?)))
(defn- parse-html [xml]
(-> (str xml)
- (.replaceAll "
" "
")
+ (.replaceAll "
" "
")
+ (.replaceAll "
" "
")
(xml/parse-str)
(doto (validate-tags))
(try (catch javax.xml.stream.XMLStreamException e
@@ -36,21 +41,22 @@
(defn- walk-children [xml]
(if (map? xml)
- (if (= :br (:tag xml))
+ (if (#{:br :BR} (:tag xml))
[{:text ::br}]
(for [c (:content xml)
x (walk-children c)]
- (update x :path conj (:tag xml))))
+ (update x :path conj (kw-lowercase (:tag xml)))))
[{:text xml}]))
-(defn- path->styles [path]
- (cond-> []
- (some #{:b :em :strong} path) (conj {:tag ooxml/b :attrs {ooxml/val "true"}})
- (some #{:i} path) (conj {:tag ooxml/i :attrs {ooxml/val "true"}})
- (some #{:s} path) (conj {:tag ooxml/strike :attrs {ooxml/val "true"}})
- (some #{:u} path) (conj {:tag ooxml/u :attrs {ooxml/val "single"}})
- (some #{:sup} path) (conj {:tag ooxml/vertAlign :attrs {ooxml/val "superscript"}})
- (some #{:sub} path) (conj {:tag ooxml/vertAlign :attrs {ooxml/val "subscript"}})))
+(defn- path->style [p]
+ (case p
+ (:b :em :strong) {:tag ooxml/b :attrs {ooxml/val "true"}}
+ (:i) {:tag ooxml/i :attrs {ooxml/val "true"}}
+ (:s) {:tag ooxml/strike :attrs {ooxml/val "true"}}
+ (:u) {:tag ooxml/u :attrs {ooxml/val "single"}}
+ (:sup) {:tag ooxml/vertAlign :attrs {ooxml/val "superscript"}}
+ (:sub) {:tag ooxml/vertAlign :attrs {ooxml/val "subscript"}}
+ nil))
(defn html->ooxml-runs
"Parses html string and returns a seq of ooxml run elements.
@@ -59,7 +65,7 @@
(when (seq html)
(let [ch (walk-children (parse-html (str "" html "")))]
(for [parts (partition-by :path ch)
- :let [prs (into (set base-style) (path->styles (:path (first parts))))]]
+ :let [prs (into (set base-style) (keep path->style) (:path (first parts)))]]
{:tag ooxml/r
:content (cons {:tag ooxml/rPr :content (vec prs)}
(for [{:keys [text]} parts]
@@ -69,7 +75,7 @@
(defn- current-run-style [chunk-loc]
(let [r (zip/node (zip/up (zip/up chunk-loc)))]
- (some #(when (= ooxml/rPr (:tag %)) %) (:content r))))
+ (find-first #(= ooxml/rPr (:tag %)) (:content r))))
(defn- fix-html-chunk [chunk-loc]
(assert (instance? HtmlChunk (zip/node chunk-loc)))
diff --git a/src/stencil/postprocess/list_ref.clj b/src/stencil/postprocess/list_ref.clj
index 2f07b204..cc2ff9b3 100644
--- a/src/stencil/postprocess/list_ref.clj
+++ b/src/stencil/postprocess/list_ref.clj
@@ -3,6 +3,7 @@
[stencil.ooxml :as ooxml]
[stencil.model.numbering :as numbering]
[stencil.log :as log]
+ [clojure.string]
[clojure.zip :as zip]))
(set! *warn-on-reflection* true)
@@ -28,7 +29,7 @@
(loop [buf [], n number]
(if (zero? n)
(apply str buf)
- (let [[value romnum] (some #(if (>= n (first %)) %) roman-digits)]
+ (let [[value romnum] (find-first #(>= n (first %)) roman-digits)]
(recur (conj buf romnum) (- n value))))))
(defmethod render-number "lowerRoman" [_ number]
@@ -175,7 +176,7 @@
(defn parse-instr-text [^String s]
(assert (string? s))
- (let [[type id & flags] (vec (.split (.trim s) "\\s\\\\?+"))]
+ (let [[type id & flags] (vec (.split (trim s) "\\s\\\\?+"))]
(when (= "REF" type)
{:id id
:flags (set (map keyword flags))})))
diff --git a/src/stencil/postprocess/table.clj b/src/stencil/postprocess/table.clj
index 84535100..7fda2aa1 100644
--- a/src/stencil/postprocess/table.clj
+++ b/src/stencil/postprocess/table.clj
@@ -14,15 +14,10 @@
(defn- loc-row? [loc] (some-> loc zip/node :tag name #{"tr"}))
(defn- loc-table? [loc] (some-> loc zip/node :tag name #{"tbl" "table"}))
-(defn- find-first-in-tree [pred tree]
- (assert (zipper? tree))
- (assert (fn? pred))
- (find-first (comp pred zip/node) (take-while (complement zip/end?) (iterate zip/next tree))))
-
(defn- find-last-child [pred tree]
(assert (zipper? tree))
(assert (fn? pred))
- (last (filter (comp pred zip/node) (iterations zip/right (zip/down tree)))))
+ (find-last (comp pred zip/node) (iterations zip/right (zip/down tree))))
(defn- first-right-sibling
"Finds first right sibling that matches the predicate."
diff --git a/src/stencil/postprocess/whitespaces.clj b/src/stencil/postprocess/whitespaces.clj
index e60349fe..8b566e85 100644
--- a/src/stencil/postprocess/whitespaces.clj
+++ b/src/stencil/postprocess/whitespaces.clj
@@ -1,20 +1,44 @@
(ns stencil.postprocess.whitespaces
- (:require [clojure.zip :as zip]
+ (:require [clojure.string :refer [includes? starts-with? ends-with? index-of]]
+ [clojure.zip :as zip]
[stencil.ooxml :as ooxml]
- [stencil.types :refer :all]
[stencil.util :refer :all]))
-(defn- should-fix?
- "We only fix tags where the enclosed string starts or ends with whitespace."
- [element]
- (boolean
- (when (and (map? element)
- (= ooxml/t (:tag element))
- (seq (:content element)))
- (or (.startsWith (str (first (:content element))) " ")
- (.startsWith (str (last (:content element))) " ")))))
+(defn- should-fix? [element]
+ (when (and (map? element)
+ (= ooxml/t (:tag element))
+ (not-empty (:content element)))
+ (or (starts-with? (first (:content element)) " ")
+ (ends-with? (last (:content element)) " ")
+ (some #(includes? % "\n") (:content element)))))
-(defn- fix-elem [element]
- (assoc-in element [:attrs ooxml/space] "preserve"))
+(defn- multi-replace [loc items]
+ (assert (zipper? loc))
+ (assert (not-empty items))
+ (reduce (comp zip/right zip/insert-right) (zip/replace loc (first items)) (next items)))
-(defn fix-whitespaces [xml-tree] (dfs-walk-xml xml-tree should-fix? fix-elem))
+;; (defn- lines-of [s] (enumeration-seq (java.util.StringTokenizer. s "\n" true)))
+;; (defn- lines-of [s] (remove #{""} (interpose "\n" (clojure.string/split s "\n" -1))))
+
+(defn- lines-of [s]
+ (if-let [idx (index-of s "\n")]
+ (if (zero? idx)
+ (cons "\n" (lazy-seq (lines-of (subs s 1))))
+ (list* (subs s 0 idx) "\n" (lazy-seq (lines-of (subs s (inc idx))))))
+ (if (empty? s) [] (list s))))
+
+(defn- item->elem [item]
+ (cond (= "\n" item)
+ ,,,{:tag ooxml/br}
+ (or (starts-with? item " ") (ends-with? item " "))
+ ,,,{:tag ooxml/t :content [item] :attrs {ooxml/space "preserve"}}
+ :else
+ ,,,{:tag ooxml/t :content [item]}))
+
+(defn- fix-elem-node [loc]
+ (->> (apply str (:content (zip/node loc)))
+ (lines-of)
+ (map item->elem)
+ (multi-replace loc)))
+
+(defn fix-whitespaces [xml-tree] (dfs-walk-xml-node xml-tree should-fix? fix-elem-node))
diff --git a/src/stencil/spec.clj b/src/stencil/spec.clj
new file mode 100644
index 00000000..f54ffe22
--- /dev/null
+++ b/src/stencil/spec.clj
@@ -0,0 +1,105 @@
+(ns stencil.spec
+ (:import [java.io File])
+ (:require [clojure.spec.alpha :as s]
+ [stencil.model :as m]
+ [stencil.process]))
+
+
+
+;; TODO
+(s/def :stencil.model/mode #{"External"})
+
+;; keys are either all keywords or all strings
+(s/def ::data map?)
+
+;; other types are also possible
+(s/def :stencil.model/type #{stencil.model/rel-type-footer
+ stencil.model/rel-type-header
+ stencil.model/rel-type-main
+ stencil.model/rel-type-slide})
+
+(s/def :stencil.model/path (s/and string? not-empty #(not (.startsWith ^String % "/"))))
+
+;; relationship file
+(s/def ::relations (s/keys :req [:stencil.model/path]
+ :req-un [::source-file]
+ :opt-un [:stencil.model/parsed]))
+
+(s/def :?/relations (s/nilable ::relations))
+
+(s/def ::result (s/keys :req-un [::writer]))
+
+(s/def ::style (s/keys :req [:stencil.model/path]
+ :opt-un [::result]))
+
+(s/def :stencil.model/headers+footers (s/* (s/keys :req [:stencil.model/path]
+ :req-un [::source-file :stencil.model/executable :?/relations]
+ :opt-un [::result])))
+
+(s/def ::source-folder (s/and (partial instance? java.io.File)
+ #(.isDirectory ^File %)
+ #(.exists ^File %)))
+
+(s/def ::source-file (s/and (partial instance? java.io.File)
+ #(.isFile ^File %)
+ #(.exists ^File %)))
+
+(s/def ::main (s/keys :req [:stencil.model/path]
+ :opt-un [:stencil.model/headers+footers ::result] ;; not present in fragments
+ :opt [::numbering]
+ :req-un [::source-file
+ ::executable
+ ::style
+ ::relations]))
+
+
+(s/def :stencil.model/content-types
+ (s/keys :req [:stencil.model/path]
+ :req-un [::source-file]))
+
+(s/def :stencil.model/model
+ (s/keys :req []
+ :req-un [::main ::source-folder :stencil.model/content-types ::relations]))
+
+(s/def ::parsed any?)
+
+(s/def :stencil.model/numbering (s/nilable (s/keys :req [:stencil.model/path]
+ :req-un [::source-file ::parsed])))
+
+(s/fdef stencil.model/load-template-model
+ :args (s/cat :dir ::source-folder, :opts map?)
+ :ret :stencil.model/model)
+
+(s/fdef stencil.model/load-fragment-model
+ :args (s/cat :dir ::source-folder, :opts map?)
+ :ret :stencil.model/model)
+
+(s/fdef stencil.model/eval-template-model
+ :args (s/cat :model :stencil.model/model
+ :data ::data
+ :unused/function-arg any?
+ :fragments (s/map-of string? :stencil.model/model))
+ :ret :stencil.model/model)
+
+(s/def :exec/variables (s/coll-of string? :unique true))
+(s/def :exec/dynamic? boolean?)
+(s/def :exec/executable any?) ;; seq of normalized control ast.
+(s/def :exec/fragments (s/coll-of string? :unique true)) ;; what was that?
+
+(s/def ::executable (s/keys :req-un [:exec/variables :exec/dynamic? :exec/executable :exec/fragments]))
+
+;; prepared template
+(s/def ::template any?)
+
+(s/def :eval-template/data any?) ;; map of string or keyword keys
+(s/def :eval-template/fragments (s/map-of string? any?))
+(s/def :eval-template/function fn?)
+
+(s/fdef stencil.process/eval-template
+ :args (s/cat :ps (s/keys :req-un [::template :eval-template/data :eval-template/function :eval-template/fragments])))
+
+(s/def ::writer (s/fspec :args (s/cat :writer (partial instance? java.io.Writer)) :ret nil?))
+
+(s/fdef template-model->writers-map
+ :args (s/cat :model :stencil.model/model, :data ::data, :functions any?, :fragments any?)
+ :ret (s/map-of :stencil.model/path ::writer))
\ No newline at end of file
diff --git a/src/stencil/tokenizer.clj b/src/stencil/tokenizer.clj
index 923c159a..f453af6f 100644
--- a/src/stencil/tokenizer.clj
+++ b/src/stencil/tokenizer.clj
@@ -1,15 +1,15 @@
(ns stencil.tokenizer
"Fog egy XML dokumentumot es tokenekre bontja"
(:require [clojure.data.xml :as xml]
+ [clojure.string :refer [includes? split]]
[stencil.infix :as infix]
- [stencil.types :refer [open-tag close-tag]]
- [stencil.util :refer [assoc-if-val mod-stack-top-conj mod-stack-top-last parsing-exception]]))
+ [stencil.util :refer [assoc-if-val mod-stack-top-conj mod-stack-top-last parsing-exception trim]]))
(set! *warn-on-reflection* true)
(defn- text->cmd-impl [^String text]
(assert (string? text))
- (let [text (.trim text)
+ (let [text (trim text)
pattern-elseif #"^(else\s*if|elif|elsif)(\(|\s+)"]
(cond
(#{"end" "endfor" "endif"} text) {:cmd :end}
@@ -21,12 +21,14 @@
(.startsWith text "unless ")
{:cmd :if
- :condition (conj (vec (infix/parse (.substring text 7))) :not)}
+ :condition (list :not (infix/parse (.substring text 7)))}
(.startsWith text "for ")
- (let [[v expr] (vec (.split (.substring text 4) " in " 2))]
+ (let [[v expr] (split (subs text 4) #" in " 2)
+ [idx v] (if (includes? v ",") (split v #",") ["$" v])]
{:cmd :for
- :variable (symbol (.trim ^String v))
+ :variable (symbol (trim v))
+ :index-var (symbol (trim idx))
:expression (infix/parse expr)})
(.startsWith text "=")
@@ -36,7 +38,7 @@
;; fragment inclusion
(.startsWith text "include ")
{:cmd :cmd/include
- :name (first (infix/parse (.substring text 8)))}
+ :name (infix/parse (.substring text 8))}
;; `else if` expression
(seq (re-seq pattern-elseif text))
@@ -44,12 +46,12 @@
{:cmd :else-if
:condition (infix/parse (.substring text prefix-len))})
- :else (throw (ex-info "Unexpected command" {:command text})))))
+ :else (throw (ex-info (str "Unexpected command: " text) {})))))
(defn text->cmd [text]
(try (text->cmd-impl text)
- (catch clojure.lang.ExceptionInfo e
- (throw (parsing-exception (str open-tag text close-tag) (.getMessage e))))))
+ (catch clojure.lang.ExceptionInfo e
+ (throw (parsing-exception text (.getMessage e))))))
(defn structure->seq [parsed]
(cond
@@ -63,8 +65,7 @@
[{:close (:tag parsed)}])
:else
- [(cond-> {:open+close (:tag parsed)}
- (seq (:attrs parsed)) (assoc :attrs (:attrs parsed)))]))
+ [(assoc-if-val {:open+close (:tag parsed)} :attrs (not-empty (:attrs parsed)))]))
(defn- tokens-seq-reducer [stack token]
(cond
diff --git a/src/stencil/types.clj b/src/stencil/types.clj
index 95386dc5..b31804a0 100644
--- a/src/stencil/types.clj
+++ b/src/stencil/types.clj
@@ -5,14 +5,6 @@
(def open-tag "{%")
(def close-tag "%}")
-(defrecord OpenTag [open])
-(defrecord CloseTag [close])
-(defrecord TextTag [text])
-
-(defn ->text [t] (->TextTag t))
-(defn ->close [t] (->CloseTag t))
-(def ->open ->OpenTag)
-
(defprotocol ControlMarker)
;; Invocation of a fragment by name
diff --git a/src/stencil/util.clj b/src/stencil/util.clj
index 9e82f211..26549bc7 100644
--- a/src/stencil/util.clj
+++ b/src/stencil/util.clj
@@ -1,6 +1,6 @@
(ns stencil.util
(:require [clojure.zip])
- (:import [io.github.erdos.stencil.exceptions ParsingException]))
+ (:import [io.github.erdos.stencil.exceptions ParsingException EvalException]))
(set! *warn-on-reflection* true)
@@ -14,22 +14,18 @@
[(take (- (count stack1) cnt) stack1)
(take (- (count stack2) cnt) stack2)]))
-(defn mod-stack-top-last
- "Egy stack legfelso elemenek legutolso elemet modositja.
- Ha nincs elem, IllegalStateException kivetelt dob."
- [stack f & args]
- (assert (list? stack) (str "Stack is not a list: " (pr-str stack)))
- (assert (ifn? f))
- (conj (rest stack)
- (conj (pop (first stack))
- (apply f (peek (first stack)) args))))
-
(defn update-peek
"Updates top element of a stack."
[xs f & args]
(assert (ifn? f))
(conj (pop xs) (apply f (peek xs) args)))
+(defn mod-stack-top-last
+ "Updatest last element of top elem of stack."
+ [stack f & args]
+ (assert (list? stack) (str "Stack is not a list: " (pr-str stack)))
+ (apply update-peek stack update-peek f args))
+
(defn mod-stack-top-conj
"Conjoins an element to the top item of a stack."
[stack & items]
@@ -40,10 +36,10 @@
(defn fixpt [f x] (let [fx (f x)] (if (= fx x) x (recur f fx))))
(defn zipper? [loc] (-> loc meta (contains? :zip/branch?)))
-(defn iterations [f xs] (take-while some? (iterate f xs)))
-(defn find-first [pred xs] (first (filter pred xs)))
+(defn iterations [f elem] (eduction (take-while some?) (iterate f elem)))
-;; same as (last (filter pred xs))
+;; same as (first (filter pred xs))
+(defn find-first [pred xs] (reduce (fn [_ x] (if (pred x) (reduced x))) nil xs))
(defn find-last [pred xs] (reduce (fn [a x] (if (pred x) x a)) nil xs))
(def xml-zip
@@ -75,17 +71,48 @@
(defn parsing-exception [expression message]
(ParsingException/fromMessage (str expression) (str message)))
-(defn dfs-walk-xml-node [xml-tree predicate edit-fn]
- (assert (map? xml-tree))
- (assert (fn? predicate))
- (assert (fn? edit-fn))
- (loop [loc (xml-zip xml-tree)]
+(defn eval-exception [message expression]
+ (assert (string? message))
+ (EvalException. message expression))
+
+;; return xml zipper of location that matches predicate or nil
+(defn find-first-in-tree [predicate tree-loc]
+ (assert (ifn? predicate))
+ (assert (zipper? tree-loc))
+ (letfn [(coords-of-first [node]
+ (loop [children (:content node)
+ index 0]
+ (when-let [[c & cs] (not-empty children)]
+ (if (predicate c)
+ [index]
+ (if-let [cf (coords-of-first c)]
+ (cons index cf)
+ (recur cs (inc index)))))))
+ (nth-child [loc i]
+ (loop [loc (clojure.zip/down loc), i i]
+ (if (zero? i) loc (recur (clojure.zip/right loc) (dec i)))))]
+ (if (predicate (clojure.zip/node tree-loc))
+ tree-loc
+ (when-let [coords (coords-of-first (clojure.zip/node tree-loc))]
+ (reduce nth-child tree-loc coords)))))
+
+(defn- dfs-walk-xml-node-1 [loc predicate edit-fn]
+ (assert (zipper? loc))
+ (loop [loc loc]
(if (clojure.zip/end? loc)
(clojure.zip/root loc)
(if (predicate (clojure.zip/node loc))
(recur (clojure.zip/next (edit-fn loc)))
(recur (clojure.zip/next loc))))))
+(defn dfs-walk-xml-node [xml-tree predicate edit-fn]
+ (assert (fn? predicate))
+ (assert (fn? edit-fn))
+ (assert (map? xml-tree))
+ (if-let [loc (find-first-in-tree predicate (xml-zip xml-tree))]
+ (dfs-walk-xml-node-1 loc predicate edit-fn)
+ xml-tree))
+
(defn dfs-walk-xml [xml-tree predicate edit-fn]
(assert (fn? edit-fn))
(dfs-walk-xml-node xml-tree predicate #(clojure.zip/edit % edit-fn)))
@@ -99,4 +126,26 @@
`(let [b# ~body]
(when (~pred b#) b#)))
+(defn ^String string
+ ([values] (apply str values))
+ ([xform coll] (transduce xform (fn ([^Object s] (.toString s)) ([^StringBuilder b v] (.append b v))) (StringBuilder.) coll)))
+
+(defn ^{:inline (fn [c] `(case ~c (\tab \space \newline
+ \u00A0 \u2007 \u202F ;; non-breaking spaces
+ \u000B \u000C \u000D \u001C \u001D \u001E \u001F)
+ true false))}
+ whitespace? [c] (whitespace? c))
+
+;; like clojure.string/trim but supports a wider range of whitespace characters
+(defn ^String trim [^CharSequence s]
+ (loop [right-idx (.length s)]
+ (if (zero? right-idx)
+ ""
+ (if (whitespace? (.charAt s (dec right-idx)))
+ (recur (dec right-idx))
+ (loop [left-idx 0]
+ (if (whitespace? (.charAt s left-idx))
+ (recur (inc left-idx))
+ (.toString (.subSequence s left-idx right-idx))))))))
+
:OK
diff --git a/test-resources/failures/test-eval-division.docx b/test-resources/failures/test-eval-division.docx
new file mode 100644
index 00000000..40a5d670
Binary files /dev/null and b/test-resources/failures/test-eval-division.docx differ
diff --git a/test-resources/failures/test-no-such-fn.docx b/test-resources/failures/test-no-such-fn.docx
new file mode 100644
index 00000000..d65b5a63
Binary files /dev/null and b/test-resources/failures/test-no-such-fn.docx differ
diff --git a/test-resources/failures/test-syntax-arity.docx b/test-resources/failures/test-syntax-arity.docx
new file mode 100644
index 00000000..d9d6a6a6
Binary files /dev/null and b/test-resources/failures/test-syntax-arity.docx differ
diff --git a/test-resources/failures/test-syntax-closed.docx b/test-resources/failures/test-syntax-closed.docx
new file mode 100644
index 00000000..bc28d854
Binary files /dev/null and b/test-resources/failures/test-syntax-closed.docx differ
diff --git a/test-resources/failures/test-syntax-fails.docx b/test-resources/failures/test-syntax-fails.docx
new file mode 100644
index 00000000..3984dd56
Binary files /dev/null and b/test-resources/failures/test-syntax-fails.docx differ
diff --git a/test-resources/failures/test-syntax-incomplete.docx b/test-resources/failures/test-syntax-incomplete.docx
new file mode 100644
index 00000000..fa77989a
Binary files /dev/null and b/test-resources/failures/test-syntax-incomplete.docx differ
diff --git a/test-resources/failures/test-syntax-nonclosed.docx b/test-resources/failures/test-syntax-nonclosed.docx
new file mode 100644
index 00000000..5aa24a85
Binary files /dev/null and b/test-resources/failures/test-syntax-nonclosed.docx differ
diff --git a/test-resources/failures/test-syntax-unexpected-command.docx b/test-resources/failures/test-syntax-unexpected-command.docx
new file mode 100644
index 00000000..e24be2e8
Binary files /dev/null and b/test-resources/failures/test-syntax-unexpected-command.docx differ
diff --git a/test-resources/failures/test-syntax-unexpected-elif.docx b/test-resources/failures/test-syntax-unexpected-elif.docx
new file mode 100644
index 00000000..9e9f838c
Binary files /dev/null and b/test-resources/failures/test-syntax-unexpected-elif.docx differ
diff --git a/test-resources/failures/test-syntax-unexpected-else.docx b/test-resources/failures/test-syntax-unexpected-else.docx
new file mode 100644
index 00000000..db4e40e6
Binary files /dev/null and b/test-resources/failures/test-syntax-unexpected-else.docx differ
diff --git a/test-resources/test-function-date.docx b/test-resources/test-function-date.docx
new file mode 100644
index 00000000..36367418
Binary files /dev/null and b/test-resources/test-function-date.docx differ
diff --git a/test/stencil/cleanup_test.clj b/test/stencil/cleanup_test.clj
index 68b43974..07c16f94 100644
--- a/test/stencil/cleanup_test.clj
+++ b/test/stencil/cleanup_test.clj
@@ -4,13 +4,17 @@
[cleanup :refer :all]
[types :refer :all]]))
+(defn- ->text [t] {:text t})
+(defn- ->close [t] {:close t})
+(defn- ->open [t] {:open t})
+
(deftest stack-revert-close-test
(testing "Egyszeru es ures esetek"
(is (= [] (stack-revert-close [])))
(is (= [] (stack-revert-close nil)))
(is (= [] (stack-revert-close [{:whatever 1} {:else 2}]))))
(testing "A kinyito elemeket be kell csukni"
- (is (= [(->CloseTag "a") (->CloseTag "b")]
+ (is (= [(->close "a") (->close "b")]
(stack-revert-close [{:open "b"} {:open "a"}])))))
(deftest tokens->ast-test
@@ -30,7 +34,7 @@
{:text "Then"}
{:cmd :end}]
[{:cmd :if :condition 1
- :blocks [{:children [{:text "Then"}]}]}]
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [{:text "Then"}]}]}]
;; if-then-else-fi
[{:cmd :if :condition 1}
@@ -39,50 +43,52 @@
{:text "Else"}
{:cmd :end}]
[{:cmd :if, :condition 1
- :blocks [{:children [{:text "Then"}]} {:children [{:text "Else"}]}]}])))
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [{:text "Then"}]} {:stencil.cleanup/children [{:text "Else"}]}]}])))
(deftest normal-ast-test-1
- (is (= (control-ast-normalize
+ (is (= (map control-ast-normalize
(annotate-environments
- [(->OpenTag "html")
- (->OpenTag "a")
- (->TextTag "Inka")
+ [(->open "html")
+ (->open "a")
+ (->text "Inka")
{:cmd :if
- :blocks [{:children [(->TextTag "ikarusz")
- (->CloseTag "a")
- (->TextTag "bela")
- (->OpenTag "b")
- (->TextTag "Hello")]}
- {:children [(->TextTag "Virag")
- (->CloseTag "b")
- (->TextTag "Hajdiho!")
- (->OpenTag "c")
- (->TextTag "Bogar")]}]}
- (->TextTag "Kaktusz")
- (->CloseTag "c")
- (->CloseTag "html")]))
-
- [(->OpenTag "html")
- (->OpenTag "a")
- (->TextTag "Inka")
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [
+ (->text "ikarusz")
+ (->close "a")
+ (->text "bela")
+ (->open "b")
+ (->text "Hello")]}
+ {:stencil.cleanup/children [
+ (->text "Virag")
+ (->close "b")
+ (->text "Hajdiho!")
+ (->open "c")
+ (->text "Bogar")]}]}
+ (->text "Kaktusz")
+ (->close "c")
+ (->close "html")]))
+
+ [(->open "html")
+ (->open "a")
+ (->text "Inka")
{:cmd :if
- :then [(->TextTag "ikarusz")
- (->CloseTag "a")
- (->TextTag "bela")
- (->OpenTag "b")
- (->TextTag "Hello")
- (->CloseTag "b")
- (->OpenTag "c")]
- :else [(->CloseTag "a")
- (->OpenTag "b")
- (->TextTag "Virag")
- (->CloseTag "b")
- (->TextTag "Hajdiho!")
- (->OpenTag "c")
- (->TextTag "Bogar")]}
- (->TextTag "Kaktusz")
- (->CloseTag "c")
- (->CloseTag "html")])))
+ :then [(->text "ikarusz")
+ (->close "a")
+ (->text "bela")
+ (->open "b")
+ (->text "Hello")
+ (->close "b")
+ (->open "c")]
+ :else [(->close "a")
+ (->open "b")
+ (->text "Virag")
+ (->close "b")
+ (->text "Hajdiho!")
+ (->open "c")
+ (->text "Bogar")]}
+ (->text "Kaktusz")
+ (->close "c")
+ (->close "html")])))
(def (->open "i")) (def </i> (->close "i"))
(def (->open "b")) (def </b> (->close "b"))
@@ -91,11 +97,11 @@
(deftest normal-ast-test-0
(testing "Amikor a formazas a THEN blokk kozepeig tart, akkor az ELSE blokk-ba is be kell tenni a lezaro taget."
- (is (= (control-ast-normalize
+ (is (= (map control-ast-normalize
(annotate-environments
[{:cmd :if
- :blocks [{:children [(->text "bela") (->text "Hello")]}
- {:children [(->text "Virag")]}]}
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [(->text "bela") (->text "Hello")]}
+ {:stencil.cleanup/children [(->text "Virag")]}]}
(->close "b")]))
[{:cmd :if
@@ -105,11 +111,11 @@
(deftest normal-ast-test-0-deep
(testing "Amikor a formazas a THEN blokk kozepeig tart, akkor az ELSE blokk-ba is be kell tenni a lezaro taget."
- (is (= (control-ast-normalize
+ (is (= (map control-ast-normalize
(annotate-environments
[{:cmd :if
- :blocks [{:children [(->text "bela") (->text "Hello") </i>]}
- {:children [ (->text "Virag") </j>]}]}
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [(->text "bela") (->text "Hello") </i>]}
+ {:stencil.cleanup/children [ (->text "Virag") </j>]}]}
</b>]))
[{:cmd :if
@@ -119,11 +125,11 @@
(deftest normal-ast-test-condition-only-then
(testing "Az elagazasban eredeetileg csak THEN ag volt de beszurjuk az else agat is."
- (is (= (control-ast-normalize
+ (is (= (map control-ast-normalize
(annotate-environments
[
{:cmd :if
- :blocks [{:children [(->text "bela") (->text "Hello") </i>]}]}
+ :stencil.cleanup/blocks [{:stencil.cleanup/children [(->text "bela") (->text "Hello") </i>]}]}
</b>
</a>]))
@@ -133,11 +139,11 @@
</b>
</a>]))))
-(defn >>for-loop [& children] {:cmd :for :blocks [{:children (vec children)}]})
+(defn >>for-loop [& children] {:cmd :for :stencil.cleanup/blocks [{:stencil.cleanup/children (vec children)}]})
(deftest test-normal-ast-for-loop-1
(testing "ismetleses ciklusban"
- (is (= (control-ast-normalize
+ (is (= (map control-ast-normalize
(annotate-environments
[
(->text "before")
@@ -160,27 +166,27 @@
(is (= () (find-variables [{:open "a"} {:close "a"}]))))
(testing "Variables from simple subsitutions"
- (is (= ["a"] (find-variables [{:cmd :echo :expression '[a 1 :plus]}]))))
+ (is (= ["a"] (find-variables [{:cmd :echo :expression '[:plus a 1]}]))))
(testing "Variables from if conditions"
- (is (= ["a"] (find-variables [{:cmd :if :condition '[a 1 :eq]}]))))
+ (is (= ["a"] (find-variables [{:cmd :if :condition '[:eq a 1]}]))))
(testing "Variables from if branches"
- (is (= ["x"] (find-variables [{:cmd :if :condition []
- :blocks [[] [{:cmd :echo :expression '[x]}]]}]))))
+ (is (= ["x"] (find-variables [{:cmd :if :condition '3
+ :stencil.cleanup/blocks [[] [{:cmd :echo :expression 'x}]]}]))))
(testing "Variables from loop expressions"
(is (= ["xs" "xs[]"]
- (find-variables '[{:cmd :for, :variable y, :expression [xs],
- :blocks [[{:cmd :echo, :expression [y 1 :plus]}]]}])))
+ (find-variables '[{:cmd :for, :variable y, :expression xs,
+ :stencil.cleanup/blocks [[{:cmd :echo, :expression [:plus y 1]}]]}])))
(is (= ["xs" "xs[]" "xs[][]"]
- (find-variables '[{:cmd :for, :variable y, :expression [xs]
- :blocks [[{:cmd :for :variable w :expression [y]
- :blocks [[{:cmd :echo :expression [1 w :plus]}]]}]]}])))
+ (find-variables '[{:cmd :for, :variable y, :expression xs
+ :stencil.cleanup/blocks [[{:cmd :for :variable w :expression y
+ :stencil.cleanup/blocks [[{:cmd :echo :expression [:plus 1 w]}]]}]]}])))
(is (= ["xs" "xs[].z.k"]
(find-variables
- '[{:cmd :for :variable y :expression [xs]
- :blocks [[{:cmd :echo :expression [y.z.k 1 :plus]}]]}]))))
+ '[{:cmd :for :variable y :expression xs
+ :stencil.cleanup/blocks [[{:cmd :echo :expression [:plus [:get y "z" "k"] 1]}]]}]))))
(testing "Variables from loop bindings and bodies"
;; TODO: impls this test
@@ -192,16 +198,16 @@
(testing "Nested loop bindings"
(is (= ["xs" "xs[].t" "xs[].t[].n"]
(find-variables
- '[{:cmd :for :variable a :expression [xs]
- :blocks [[{:cmd :for :variable b :expression [a.t]
- :blocks [[{:cmd :echo :expression [b.n 1 :plus]}]]}]]}])))
+ '[{:cmd :for :variable a :expression xs
+ :stencil.cleanup/blocks [[{:cmd :for :variable b :expression [:get a "t"]
+ :stencil.cleanup/blocks [[{:cmd :echo :expression [:plus [:get b "n"] 1]}]]}]]}])))
))
(deftest test-process-if-then-else
(is (=
'[{:open :body}
{:open :a}
- {:cmd :if, :condition [a],
+ {:cmd :if, :condition a,
:then [{:close :a}
{:open :a}
{:text "THEN"}
@@ -213,7 +219,7 @@
(:executable (process '({:open :body}
{:open :a}
- {:cmd :if, :condition [a]}
+ {:cmd :if, :condition a}
{:close :a}
{:open :a}
{:text "THEN"}
@@ -226,9 +232,9 @@
(deftest test-process-if-nested
(is (=
[
- {:cmd :if, :condition '[x.a],
+ {:cmd :if, :condition '[:get x "a"],
:then [</a>
- {:cmd :if, :condition '[x.b],
+ {:cmd :if, :condition '[:get x "b"],
:then [ {:text "THEN"}]
:else []}
</a>]
@@ -236,9 +242,9 @@
(:executable
(process
[
- ,,{:cmd :if, :condition '[x.a]}
+ ,,{:cmd :if, :condition '[:get x "a"]}
</a>
- {:cmd :if, :condition '[x.b]}
+ {:cmd :if, :condition '[:get x "b"]}
,,{:text "THEN"}
,,{:cmd :end}
diff --git a/test/stencil/errors.clj b/test/stencil/errors.clj
index ebb559b9..dca53c1c 100644
--- a/test/stencil/errors.clj
+++ b/test/stencil/errors.clj
@@ -1,6 +1,7 @@
(ns stencil.errors
- (:import [io.github.erdos.stencil.exceptions ParsingException])
+ (:import [io.github.erdos.stencil.exceptions ParsingException EvalException])
(:require [stencil.types :refer :all]
+ [stencil.integration :refer [test-fails]]
[clojure.test :refer [deftest is are testing]]
[stencil.model :as model]))
@@ -9,10 +10,6 @@
(->> xml-str (str) (.getBytes) (new java.io.ByteArrayInputStream) (model/->exec)))
-(defmacro ^:private throw-ex-info? [expr]
- `(is (~'thrown? clojure.lang.ExceptionInfo (test-prepare ~expr))))
-
-
(defmacro ^:private throw-ex-parsing? [expr]
`(is (~'thrown? ParsingException (test-prepare ~expr))))
@@ -54,12 +51,67 @@
(deftest test-not-closed
(testing "Expressions are not closed properly"
- (throw-ex-info? "{%=")
- (throw-ex-info? "{%=x")
- (throw-ex-info? "{%=x%")
- (throw-ex-info? "{%=x}"))
+ (throw-ex-parsing? "{%=")
+ (throw-ex-parsing? "{%=x")
+ (throw-ex-parsing? "{%=x%")
+ (throw-ex-parsing? "{%=x}"))
(testing "Middle expr is not closed"
(throw-ex-parsing? "{%=1%}{%=3{%=4%}")))
+
(deftest test-unexpected-cmd
(throw-ex-parsing? "{% echo 3 %}"))
+
+;; integration tests
+
+(deftest test-parsing-errors
+ (testing "Closing tag is missing"
+ (test-fails "test-resources/failures/test-syntax-nonclosed.docx" nil
+ ParsingException "Missing {%end%} tag from document!"))
+ (testing "Extra closing tag is present"
+ (test-fails "test-resources/failures/test-syntax-closed.docx" nil
+ ParsingException "Too many {%end%} tags!"))
+ (testing "A tag not closed until the end of document"
+ (test-fails "test-resources/failures/test-syntax-incomplete.docx" nil
+ ParsingException "Stencil tag is not closed. Reading {% if x + y"))
+ (testing "Unexpected {%else%} tag"
+ (test-fails "test-resources/failures/test-syntax-unexpected-else.docx" nil
+ ParsingException "Unexpected {%else%} tag, it must come right after a condition!"))
+ (testing "Unexpected {%else if%} tag"
+ (test-fails "test-resources/failures/test-syntax-unexpected-elif.docx" nil
+ ParsingException "Unexpected {%else if%} tag, it must come right after a condition!"))
+ (testing "Cannot parse infix expression"
+ (test-fails "test-resources/failures/test-syntax-fails.docx" nil
+ ParsingException "Invalid stencil expression!"))
+ (testing "Test unexpected command"
+ (test-fails "test-resources/failures/test-syntax-unexpected-command.docx" nil
+ ParsingException "Unexpected command: unexpected")))
+
+(deftest test-evaluation-errors
+ (testing "Division by zero"
+ (test-fails "test-resources/failures/test-eval-division.docx" {:x 1 :y 0}
+ EvalException "Error evaluating expression: {%=x/y%}"
+ java.lang.ArithmeticException "Divide by zero"))
+ (testing "NPE"
+ (test-fails "test-resources/failures/test-eval-division.docx" {:x nil :y nil}
+ EvalException "Error evaluating expression: {%=x/y%}"
+ java.lang.NullPointerException nil #_"Cannot invoke \"Object.getClass()\" because \"x\" is null"))
+ (testing "function does not exist"
+ (test-fails "test-resources/failures/test-no-such-fn.docx" {}
+ EvalException "Error evaluating expression: {%=nofun()%}"
+ java.lang.IllegalArgumentException "Did not find function for name nofun"))
+ (testing "function invoked with wrong arity"
+ (test-fails "test-resources/failures/test-syntax-arity.docx" {}
+ EvalException "Error evaluating expression: {%=decimal(1,2,3)%}"
+ clojure.lang.ExceptionInfo "Function 'decimal' was called with a wrong number of arguments (3)"))
+ (testing "Missing fragment"
+ (test-fails "test-resources/multipart/main.docx" {}
+ EvalException "No fragment for name: body"))
+ (testing "date() function has custom message"
+ (test-fails "test-resources/test-function-date.docx" {"date" "2022-01-04XXX11:22:33"}
+ EvalException "Error evaluating expression: {%=date(\"yyyy-MM-dd\", date)%}"
+ IllegalArgumentException "Could not parse date object 2022-01-04XXX11:22:33"))
+
+ (testing "function invocation error"
+ ;; TODO: invoke fn with wrong types
+ ))
\ No newline at end of file
diff --git a/test/stencil/eval_test.clj b/test/stencil/eval_test.clj
index c4b199a8..8eeea0a4 100644
--- a/test/stencil/eval_test.clj
+++ b/test/stencil/eval_test.clj
@@ -23,7 +23,7 @@
(deftest test-if
(testing "THEN branch"
(test-eval [-text1-
- {:cmd :if :condition '[truthy]
+ {:cmd :if :condition 'truthy
:then [{:text "ok"}]
:else [{:text "err"}]}
-text1-]
@@ -33,7 +33,7 @@
(testing "ELSE branch"
(test-eval [-text1-
- {:cmd :if :condition '[falsey]
+ {:cmd :if :condition 'falsey
:then [{:text "ok"}]
:else [{:text "err"}]}]
[-text1-
@@ -41,17 +41,18 @@
(deftest test-echo
(testing "Simple math expression"
- (test-eval [{:cmd :echo :expression '[1 2 :plus]}]
+ (test-eval [{:cmd :echo :expression [:plus 1 2]}]
[{:text "3"}]))
(testing "Nested data access with path"
- (test-eval [{:cmd :echo :expression '[abc.def]}]
+ (test-eval [{:cmd :echo :expression 'abc.def}]
[{:text "Okay"}])))
(deftest test-for
(testing "loop without any items"
(test-eval [{:cmd :for
:variable "index"
- :expression '[list0]
+ :index-var "i"
+ :expression 'list0
:body-run-once [{:text "xx"}]
:body-run-none [{:text "meh"}]
:body-run-next [{:text "x"}]}]
@@ -60,17 +61,29 @@
(testing "loop with exactly 1 item"
(test-eval [{:cmd :for
:variable "index"
- :expression '[list1]
- :body-run-once [{:cmd :echo :expression '[index]}]
+ :index-var "i"
+ :expression 'list1
+ :body-run-once [{:cmd :echo :expression 'index}]
:body-run-none [{:text "meh"}]
:body-run-next [{:text "x"}]}]
[{:text "1"}]))
+ (testing "loop with exactly 1 item and index var used"
+ (test-eval [{:cmd :for
+ :variable "index"
+ :index-var "i"
+ :expression 'abc
+ :body-run-once [{:cmd :echo :expression 'i} {:text "==>"} {:cmd :echo :expression 'index}]
+ :body-run-none [{:text "should-not-run"}]
+ :body-run-next [{:text "should-not-run"}]}]
+ [{:text "def"} {:text "==>"} {:text "Okay"}]))
+
(testing "loop with exactly 3 items"
(test-eval [{:cmd :for
:variable "index"
- :expression '[list3]
- :body-run-once [{:cmd :echo :expression '[index]}]
+ :index-var "i"
+ :expression 'list3
+ :body-run-once [{:cmd :echo :expression 'index}]
:body-run-none [{:text "meh"}]
- :body-run-next [{:text "x"} {:cmd :echo :expression '[index]}]}]
+ :body-run-next [{:text "x"} {:cmd :echo :expression 'index}]}]
[{:text "1"} {:text "x"} {:text "2"} {:text "x"} {:text "3"}])))
diff --git a/test/stencil/functions_test.clj b/test/stencil/functions_test.clj
index e36908e3..6bcfde63 100644
--- a/test/stencil/functions_test.clj
+++ b/test/stencil/functions_test.clj
@@ -27,6 +27,8 @@
(is (= "Hello null" (call-fn "format" "Hello %c" nil)))
(is (= "Hello x" (call-fn "format" "Hello %c" "x")))
(is (= "Hello X" (call-fn "format" "Hello %C" \x))))
+ (testing "decimal precision"
+ (is (= "0.33" (call-fn "format" "%,.2f" 1/3))))
(testing "Indexed parameters"
(is (= "hello 42 41.00" (call-fn "format" "hello %2$d %1$,.2f" 41.0 42.0))))
(is (= "hello john" (call-fn "format" "hello %s" "john")))
@@ -104,6 +106,11 @@
(is (thrown? ExceptionInfo (call-fn "map" "x" {:x 1 :y 2})))
(is (thrown? ExceptionInfo (call-fn "map" 1 [])))))
+(deftest test-coalesce
+ (is (= 1 (call-fn "coalesce" 1 2)))
+ (is (= 1 (call-fn "coalesce" nil nil 1 nil 2 nil)))
+ (is (= nil (call-fn "coalesce" nil nil nil))))
+
(deftest test-join-and
(are [expect param] (= expect (call-fn "joinAnd" param ", " " and "))
"" nil
@@ -112,6 +119,12 @@
"1 and 2" [1 2]
"1, 2 and 3" [1 2 3]))
+(deftest test-replace
+ (is (= "a1c" (call-fn "replace" "abc" "b" "1")))
+ (is (= "a1a1" (call-fn "replace" "abab" "b" "1")))
+ (is (= "a1a1" (call-fn "replace" "a.a." "." "1")))
+ (is (= "123456" (call-fn "replace" " 12 34 56 " " " ""))))
+
(import '[stencil.types ReplaceImage])
(def data-uri "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")
diff --git a/test/stencil/infix_test.clj b/test/stencil/infix_test.clj
index ed575d7a..f259dd0c 100644
--- a/test/stencil/infix_test.clj
+++ b/test/stencil/infix_test.clj
@@ -1,6 +1,6 @@
(ns stencil.infix-test
(:import [clojure.lang ExceptionInfo])
- (:require [stencil.infix :as infix :refer :all]
+ (:require [stencil.infix :as infix]
[stencil.types :refer [hide-table-column-marker?]]
[clojure.test :refer [deftest testing is are]]))
@@ -8,16 +8,24 @@
([xs] (run xs {}))
([xs args] (infix/eval-rpn args (infix/parse xs))))
+
(deftest tokenize-test
(testing "simple fn call"
- (is (= [(->FnCall "sin") 1 :plus 2 :close] (infix/tokenize " sin(1+2)"))))
+ (is (= ['sin :open 1 :plus 2 :close] (infix/tokenize " sin(1+2)"))))
(testing "comma"
- (is (= [:open 1 :comma 2 :comma 3 :close] (infix/tokenize " (1,2 ,3) ")))))
+ (is (= [:open 1 :comma 2 :comma 3 :close] (infix/tokenize " (1,2 ,3) "))))
+
+ (testing "Unexpected end of string"
+ (try (dorun (infix/tokenize "1 + 2 ##"))
+ (assert false "should have thrown")
+ (catch ExceptionInfo e
+ (is (= "Unexpected end of string" (.getMessage e)))
+ (is (= {:index 6} (ex-data e)))))))
(deftest test-read-string-literal
(testing "Incomplete string"
- (is (thrown? ExceptionInfo (read-string-literal "'alaba")))))
+ (is (thrown? ExceptionInfo (infix/read-string-literal "'alaba")))))
(deftest tokenize-string-literal
(testing "spaces are kept"
@@ -34,9 +42,9 @@
(deftest tokenize-string-fun-eq
(testing "tricky"
- (is (= ["1" :eq #stencil.infix.FnCall{:fn-name "str"} 1 :close]
+ (is (= ["1" :eq 'str :open 1 :close]
(infix/tokenize "\"1\" = str(1)")))
- (is (= ["1" 1 {:fn "str" :args 1} :eq]
+ (is (= [:eq "1" [:fncall 'str 1]]
(infix/parse "\"1\" = str(1)")))))
(deftest parse-simple
@@ -45,20 +53,20 @@
(is (thrown? ExceptionInfo (infix/parse ""))))
(testing "Simple values"
- (is (= [12] (infix/parse " 12 ") (infix/parse "12")))
- (is (= '[ax.y] (infix/parse " ax.y "))))
+ (is (= 12 (infix/parse " 12 ") (infix/parse "12")))
+ (is (= '[:get ax "y"] (infix/parse " ax.y "))))
(testing "Simple operations"
- (is (= [1 2 :plus]
+ (is (= [:plus 1 2]
(infix/parse "1 + 2")
(infix/parse "1+2 ")
(infix/parse "1+2")))
- (is (= [3 2 :times] (infix/parse "3*2"))))
+ (is (= [:times 3 2] (infix/parse "3*2"))))
(testing "Parentheses"
- (is (= [3 2 :plus 4 :times]
+ (is (= [:times [:plus 3 2] 4]
(infix/parse "(3+2)*4")))
- (is (= [3 2 :plus 4 1 :minus :times]
+ (is (= [:times [:plus 3 2] [:minus 4 1]]
(infix/parse "(3+2)*(4 - 1)")))))
(deftest all-ops-supported
@@ -66,9 +74,8 @@
(let [ops (-> #{}
(into (vals infix/ops))
(into (vals infix/ops2))
- (into (keys infix/operation-tokens))
- (disj :open :close :comma :open-bracket :close-bracket))
- known-ops (set (filter keyword? (keys (methods @#'infix/reduce-step))))]
+ (disj :open :close :comma :open-bracket :close-bracket :dot))
+ known-ops (set (filter keyword? (keys (methods @#'infix/eval-tree))))]
(is (every? known-ops ops)))))
(deftest basic-arithmetic
@@ -85,6 +92,7 @@
(is (= 3 (run "coalesce(5, 1, 0) - coalesce(2, 1)")))
(is (= 6 (run "2 * 3")))
(is (= 3 (run "6 / 2")))
+ (is (= 0.33333333M (run "1 / x" {"x" (bigdec 3)}))) ;; infinite expansion error
(is (= 36.0 (run "6 ^ 2")))
(is (= 2 (run "12 % 5"))))
@@ -131,9 +139,18 @@
(is (= 7 (run "a[a[1]+4]" {"a" {"1" 2 "6" 7}})))
(is (= 8 (run "a[1][2]" {"a" {"1" {"2" 8}}}))))
+ (testing "negation"
+ (is (= 6 (run "a[1]-2" {"a" {"1" 8}}))))
+
+ (testing "mixed lookup"
+ (is (= 7 (run "a['x'].y" {"a" {"x" {"y" 7}}})))
+ (is (= 8 (run "a.x['y']" {"a" {"x" {"y" 8}}}))))
+
(testing "syntax error"
(is (thrown? ExceptionInfo (run "[3]" {})))
(is (thrown? ExceptionInfo (run "a[[3]]" {})))
+ (is (thrown? ExceptionInfo (run "a.[1]b" {})))
+ (is (thrown? ExceptionInfo (run "a..b" {})))
(is (thrown? ExceptionInfo (run "a[1,2]" {}))))
(testing "key is missing from input"
@@ -185,6 +202,8 @@
(is (= 2 (run "a || b" {"b" 2})))
(is (nil? (run "a || b" {"a" false}))))))
+
+
(deftest operator-precedeces
(testing "Operator precedencia"
(is (= 36 (run "2*3+5*6"))))
@@ -276,13 +295,11 @@
(run "sum(map(\"x\", vals))"
{"vals" [{"x" 1} {"x" 2} {"x" 3}]})))))
-
-
(deftest test-colhide-expr
(is (hide-table-column-marker? (run "hideColumn()"))))
(deftest test-unexpected
- (is (thrown? ExceptionInfo (parse "aaaa:bbbb"))))
+ (is (thrown? ExceptionInfo (infix/parse "aaaa:bbbb"))))
(deftest tokenize-wrong-tokens
(testing "Misplaced operators and operands"
diff --git a/test/stencil/integration.clj b/test/stencil/integration.clj
index 115b30b4..6703dd38 100644
--- a/test/stencil/integration.clj
+++ b/test/stencil/integration.clj
@@ -1,6 +1,7 @@
(ns stencil.integration
"Integration test helpers"
(:require [clojure.zip :as zip]
+ [clojure.test :refer [do-report is]]
[stencil.api :as api]))
@@ -23,3 +24,25 @@
:when (= stencil.ooxml/t (:tag (zip/node node)))
c (:content (zip/node node))]
c)))))
+
+
+(defn test-fails
+ "Tests that rendering the template with the payload results in the given exception chain."
+ [template payload & bodies]
+ (assert (string? template))
+ (assert (not-empty bodies))
+ (try (with-open [template (api/prepare template)]
+ (api/render! template payload
+ :overwrite? true
+ :output (java.io.File/createTempFile "stencil" ".docx")))
+ (do-report {:type :error
+ :message "Should have thrown exception"
+ :expected (first bodies)
+ :actual nil})
+ (catch RuntimeException e
+ (let [e (reduce (fn [e [t reason]]
+ (is (instance? t e))
+ (or (= t NullPointerException) (is (= reason (.getMessage e))))
+ (.getCause e))
+ e (partition 2 bodies))]
+ (is (= nil e) "Cause must be null.")))))
\ No newline at end of file
diff --git a/test/stencil/merger_test.clj b/test/stencil/merger_test.clj
index 40f23f18..2c5c286c 100644
--- a/test/stencil/merger_test.clj
+++ b/test/stencil/merger_test.clj
@@ -25,6 +25,7 @@
"asdf{%xy%}" {:action "xy" :before "asdf"}
"{%xy%}" {:action "xy"}
"a{%xy" {:action-part "xy" :before "a"}
+ "a{%x%" {:action-part "x%" :before "a"}
"{%xy" {:action-part "xy"})))
(deftest text-split-tokens-test
@@ -105,25 +106,29 @@
[{:text "{%=1%}"}]
[{:text "{%=1%}"}]
- [{:action {:cmd :echo, :expression [1]}}]
+ [{:action {:cmd :echo, :expression 1 :raw "{%=1%}"}}]
[{:text "abc{%=1%}b"}]
[{:text "abc"} {:text "{%=1%}"} {:text "b"}]
- [{:text "abc"} {:action {:cmd :echo, :expression [1]}} {:text "b"}]
+ [{:text "abc"} {:action {:cmd :echo, :expression 1 :raw "{%=1%}"}} {:text "b"}]
[{:text "abc{%="} O1 O2 {:text "1"} O3 O4 {:text "%}b"}]
[{:text "abc"} {:text "{%="} O1 O2 {:text "1"} O3 O4 {:text "%}b"}]
- [{:text "abc"} {:action {:cmd :echo, :expression [1]}} O1 O2 O3 O4 {:text "b"}]
+ [{:text "abc"} {:action {:cmd :echo, :expression 1 :raw "{%=1%}"}} O1 O2 O3 O4 {:text "b"}]
[{:text "abc{%="} O1 O2 {:text "1%"} O3 O4 {:text "}b"}]
[{:text "abc"} {:text "{%="} O1 O2 {:text "1%"} O3 O4 {:text "}b"}]
- [{:text "abc"} {:action {:cmd :echo, :expression [1]}} O1 O2 O3 O4 {:text "b"}]
+ [{:text "abc"} {:action {:cmd :echo, :expression 1 :raw "{%=1%}"}} O1 O2 O3 O4 {:text "b"}]
[{:text "abcd{%="} O1 {:text "1"} O2 {:text "%"} O3 {:text "}"} O4 {:text "b"}]
[{:text "abcd"} {:text "{%="} O1 {:text "1"} O2 {:text "%"} O3 {:text "}"} O4 {:text "b"}]
- [{:text "abcd"} {:action {:cmd :echo, :expression [1]}} O1 O2 O3 O4{:text "b"}]
+ [{:text "abcd"} {:action {:cmd :echo, :expression 1 :raw "{%=1%}"}} O1 O2 O3 O4{:text "b"}]
[{:text "abc{"} O1 {:text "%"} O2 {:text "=1"} O3 {:text "2"} O4 {:text "%"} O5 {:text "}"} {:text "b"}]
[{:text "abc"} {:text "{"} O1 {:text "%"} O2 {:text "=1"} O3 {:text "2"} O4 {:text "%"} O5 {:text "}"} {:text "b"}]
- [{:text "abc"} {:action {:cmd :echo, :expression [12]}} O1 O2 O3 O4 O5 {:text "b"}]
+ [{:text "abc"} {:action {:cmd :echo, :expression 12 :raw "{%=12%}"}} O1 O2 O3 O4 O5 {:text "b"}]
+
+ [O1 {:text "{%if p"} O2 O3 {:text "%}one{%end%}"} O4]
+ [O1 {:text "{%if p"} O2 O3 {:text "%}one"} {:text "{%end%}"} O4]
+ [O1 {:action {:cmd :if, :condition 'p :raw "{%if p%}"}} O2 O3 {:text "one"} {:action {:cmd :end :raw "{%end%}"}} O4]
))))
diff --git a/test/stencil/postprocess/fragments_test.clj b/test/stencil/postprocess/fragments_test.clj
new file mode 100644
index 00000000..5ab0a94c
--- /dev/null
+++ b/test/stencil/postprocess/fragments_test.clj
@@ -0,0 +1,25 @@
+(ns stencil.postprocess.fragments-test
+ (:require [clojure.test :refer [deftest testing is are]]
+ [clojure.zip :as zip]
+ [stencil.ooxml :as ooxml]
+ [stencil.util :refer [find-first-in-tree xml-zip]]
+ [stencil.postprocess.fragments :refer :all]))
+
+(defn- p [& xs] {:tag ooxml/p :content (vec xs)})
+(defn- r [& xs] {:tag ooxml/r :content (vec xs)})
+(defn- t [& xs] {:tag ooxml/t :content (vec xs)})
+
+(def -split-paragraphs @#'stencil.postprocess.fragments/split-paragraphs)
+
+(deftest test-split-paragraphs-1
+ (let [data {:tag :root :content [(p (r (t "hello" :HERE) (t "vilag")))]}
+ loc (find-first-in-tree #{:HERE} (xml-zip data))]
+ (-> loc
+ (doto assert)
+ (-split-paragraphs (p (r (t "one"))) (p (r (t "two"))))
+ (zip/root)
+ (= {:tag :root
+ :content [(p (r (t "hello"))) ;; before
+ (p (r (t "one"))) (p (r (t "two"))) ;; newly inserted
+ (p (r (t) (t "vilag")))]}) ;; after
+ (is))))
\ No newline at end of file
diff --git a/test/stencil/postprocess/whitespaces_test.clj b/test/stencil/postprocess/whitespaces_test.clj
index 8705444d..28581186 100644
--- a/test/stencil/postprocess/whitespaces_test.clj
+++ b/test/stencil/postprocess/whitespaces_test.clj
@@ -26,6 +26,11 @@
"Sum: 1 pieces"
"Sum: {%=x %} pieces"
{"x" 1}))
+ (testing "newline value splits t tags"
+ (test-equals
+ "two lines: firstsecond "
+ "two lines: {%=x %} "
+ {"x" "first\nsecond"}))
(testing "existing space=preserve attributes are kept intact"
(test-equals
" Hello "
@@ -33,3 +38,20 @@
{})))
;; (test-eval "Sum: {%=x %} pieces" {"x" 1})
+
+(let [target (the-ns 'stencil.postprocess.whitespaces)]
+ (doseq [[k v] (ns-map target)
+ :when (and (var? v) (= target (.ns v)))]
+ (eval `(defn ~(symbol (str "-" k)) [~'& args#] (apply (deref ~v) args#)))))
+
+(deftest test-lines-of
+ (is (= ["ab" "\n" "\n" "bc"] (-lines-of "ab\n\nbc")))
+ (is (= ["\n" "xy" "\n"] (-lines-of "\nxy\n")))
+ (is (= () (-lines-of ""))))
+
+(deftest test-multi-replace
+ (let [tree (stencil.util/xml-zip {:tag :a :content ["x" "y" "z"]})
+ loc (clojure.zip/right (clojure.zip/down tree))
+ replaced (-multi-replace loc ["1" "2" "3"])]
+ (is (= "3" (clojure.zip/node replaced)))
+ (is (= ["x" "1" "2" "3" "z"] (:content (clojure.zip/root replaced))))))
\ No newline at end of file
diff --git a/test/stencil/process_test.clj b/test/stencil/process_test.clj
index 2e30dbce..1c738414 100644
--- a/test/stencil/process_test.clj
+++ b/test/stencil/process_test.clj
@@ -28,7 +28,7 @@
(is (= {:dynamic? true,
:executable [{:open :a}
{:open :b}
- {:blocks [], :cmd :cmd/include, :name "elefant"}
+ {:stencil.cleanup/blocks [], :cmd :cmd/include, :name "elefant" :raw "{%include \"elefant\"%}"}
{:close :b}
{:close :a}],
:fragments #{"elefant"}
diff --git a/test/stencil/tokenizer_test.clj b/test/stencil/tokenizer_test.clj
index 200f66d4..6376d3e0 100644
--- a/test/stencil/tokenizer_test.clj
+++ b/test/stencil/tokenizer_test.clj
@@ -3,7 +3,9 @@
[clojure.test :refer [deftest testing is]]))
(defn- run [s]
- (m/parse-to-tokens-seq (java.io.ByteArrayInputStream. (.getBytes (str s)))))
+ (->> (java.io.ByteArrayInputStream. (.getBytes (str s)))
+ (m/parse-to-tokens-seq)
+ (map #(dissoc % :raw))))
(deftest read-tokens-nested
(testing "Read a list of nested tokens"
@@ -28,16 +30,16 @@
(is (= (run "elotte {%=a%} utana")
[{:open :a}
{:text "elotte "}
- {:cmd :echo :expression '(a)}
+ {:cmd :echo :expression 'a}
{:text " utana"}
{:close :a}]))))
(deftest read-tokens-if-then
(testing "Simple conditional with THEN branch only"
- (is (= (run "elotte {% if x%} akkor {% end %} utana")
+ (is (= (run "elotte {% if x%} akkor {% end %} utana")
[{:open :a}
{:text "elotte "}
- {:cmd :if :condition '(x)}
+ {:cmd :if :condition 'x}
{:text " akkor "}
{:cmd :end}
{:text " utana"}
@@ -48,7 +50,7 @@
(is (= (run "elotte {% if x%} akkor {% else %} egyebkent {% end %} utana")
[{:open :a}
{:text "elotte "}
- {:cmd :if :condition '(x)}
+ {:cmd :if :condition 'x}
{:text " akkor "}
{:cmd :else}
{:text " egyebkent "}
@@ -61,9 +63,9 @@
(is (= (run "elotte{%if x%}akkor{%else if y%}de{%end%}utana")
'[{:open :a}
{:text "elotte"}
- {:cmd :if :condition [x]}
+ {:cmd :if :condition x}
{:text "akkor"}
- {:cmd :else-if :condition [y]}
+ {:cmd :else-if :condition y}
{:text "de"}
{:cmd :end}
{:text "utana"}
@@ -74,9 +76,9 @@
(is (= (run "elotte{%if x%}akkor{%else if y%}de{%else%}egyebkent{%end%}utana")
'[{:open :a}
{:text "elotte"}
- {:cmd :if :condition [x]}
+ {:cmd :if :condition x}
{:text "akkor"}
- {:cmd :else-if :condition [y]}
+ {:cmd :else-if :condition y}
{:text "de"}
{:cmd :else}
{:text "egyebkent"}
@@ -87,17 +89,17 @@
(deftest read-tokens-unless-then
(testing "Simple conditional with THEN branch only"
(is (= (run "{%unless x%} akkor {% end %}")
- [{:open :a} {:cmd :if :condition '(x :not)} {:text " akkor "} {:cmd :end} {:close :a}]))))
+ [{:open :a} {:cmd :if :condition '(:not x)} {:text " akkor "} {:cmd :end} {:close :a}]))))
(deftest read-tokens-unless-then-else
(testing "Simple conditional with THEN branch only"
(is (= (run "{%unless x%} akkor {%else%} egyebkent {%end %}")
- [{:open :a} {:cmd :if :condition '(x :not)} {:text " akkor "} {:cmd :else} {:text " egyebkent "} {:cmd :end} {:close :a}]))))
+ [{:open :a} {:cmd :if :condition '(:not x)} {:text " akkor "} {:cmd :else} {:text " egyebkent "} {:cmd :end} {:close :a}]))))
(deftest read-tokens-if-elif-then-else
(testing "If-else if-then-else branching"
- (is (= '({:open :a} {:text "Hello "} {:cmd :if, :condition [x]} {:text "iksz"}
- {:cmd :else-if, :condition [y]} {:text "ipszilon"} {:cmd :else}
+ (is (= '({:open :a} {:text "Hello "} {:cmd :if, :condition x} {:text "iksz"}
+ {:cmd :else-if, :condition y} {:text "ipszilon"} {:cmd :else}
{:text "egyebkent"} {:cmd :end} {:text " Hola"} {:close :a})
(run "Hello {%if x%}iksz{%else if y%}ipszilon{%else%}egyebkent{%end%} Hola")
(run "Hello {%if x%}iksz{%elseif y%}ipszilon{%else%}egyebkent{%end%} Hola")
@@ -105,8 +107,12 @@
(run "Hello {%if x%}iksz{%elif y%}ipszilon{%else%}egyebkent{%end%} Hola")))))
(deftest read-tokens-for
+ (testing "Indexed loop"
+ (is (= '[{:open :a} {:cmd :for, :expression xs, :variable x :index-var idx}
+ {:text "item"} {:cmd :end} {:close :a}]
+ (run "{%for idx, x in xs%}item{% end %}"))))
(testing "Simple loop"
- (is (= '[{:open :a} {:cmd :for, :expression [xs], :variable x}
+ (is (= '[{:open :a} {:cmd :for, :expression xs, :variable x :index-var $}
{:text "item"} {:cmd :end} {:close :a}]
(run "{%for x in xs%}item{% end %}")))))
diff --git a/test/stencil/util_test.clj b/test/stencil/util_test.clj
index 2231d625..853b59af 100644
--- a/test/stencil/util_test.clj
+++ b/test/stencil/util_test.clj
@@ -1,5 +1,5 @@
(ns stencil.util-test
- (:require [clojure.test :refer [deftest testing is]]
+ (:require [clojure.test :refer [deftest testing is are]]
[clojure.zip :as zip]
[stencil.util :refer :all]))
@@ -19,7 +19,7 @@
(deftest mod-stack-top-last-test
(testing "Invalid input"
(is (thrown? IllegalStateException (mod-stack-top-last '([]) inc)))
- (is (thrown? NullPointerException (mod-stack-top-last '() inc))))
+ (is (thrown? IllegalStateException (mod-stack-top-last '() inc))))
(testing "simple cases"
(is (= '([3]) (mod-stack-top-last '([2]) inc)))
@@ -104,3 +104,16 @@
(deftest suffixes-test
(is (= [] (suffixes []) (suffixes nil)))
(is (= [[1 2 3] [2 3] [3]] (suffixes [1 2 3]))))
+
+(deftest whitespace?-test
+ (is (= true (whitespace? \space)))
+ (is (= true (whitespace? \tab)))
+ (is (= false (whitespace? " ")))
+ (is (= false (whitespace? \A))))
+
+(deftest trim-test
+ (are [input] (= "" (trim input))
+ "", " ", "\t\t\n")
+ (are [input] (= "abc" (trim input))
+ "abc", " abc", "abc ", " \t \n abc \t")
+ (is (= "a b c" (trim " a b c \t"))))