- Added getCustomVariable(name) and concat(...) function to CJSON
- Generalized up [docgen.insertXxx ...] tag parsing a bit
- Added unfinished implementation of [docgen.insertOutput ...] directive. This will be used to simplify inserting the output of "programs" into the documentation.
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java
new file mode 100644
index 0000000..c464b34
--- /dev/null
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Splits a bash command call to a list of arguments. Quotation and escaping is resolved in the returned arguments.
+ */
+public class BashCommandLineArgsParser {
+ private final String src;
+ private int pos;
+
+ public BashCommandLineArgsParser(String src) {
+ this.src = src;
+ }
+
+ public static List<String> parse(String s) {
+ return new BashCommandLineArgsParser(s).parse();
+ }
+
+ private List<String> parse() {
+ List<String> args = new ArrayList<>();
+ String arg;
+ while ((arg = skipWSAndFetchArg()) != null) {
+ args.add(arg);
+ }
+ return args;
+ }
+
+ private String skipWSAndFetchArg() {
+ skipWS();
+ return fetchArg();
+ }
+
+ private String fetchArg() {
+ StringBuilder arg = new StringBuilder();
+ int startPos = pos;
+ char openedQuote = 0;
+ boolean escaped = false;
+ while (pos < src.length()) {
+ char c = src.charAt(pos);
+ if (escaped) {
+ if (openedQuote == '"' && !(c == '"' || c == '\\' || c == '$')) {
+ arg.append('\\');
+ }
+ arg.append(c);
+ escaped = false;
+ } else {
+ if (c == '"' || c == '\'') {
+ if (openedQuote == 0) {
+ openedQuote = c;
+ } else if (openedQuote == c) {
+ openedQuote = 0;
+ } else {
+ arg.append(c);
+ }
+ } else if (c == '\\' && openedQuote != '\'') {
+ escaped = true;
+ } else if (openedQuote == 0 && isWS(c)) {
+ break;
+ } else {
+ arg.append(c);
+ }
+ }
+ pos++;
+ }
+ return startPos != pos ? arg.toString() : null;
+ }
+
+ private void skipWS() {
+ while (pos < src.length() && isWS(src.charAt(pos))) {
+ pos++;
+ }
+ }
+
+ private boolean isWS(char c) {
+ return c == ' ' || c == '\n' || c == '\r' || c == '\t';
+ }
+}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
index 543aea6..fc43e10 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
@@ -329,22 +329,23 @@
}
}
- /**
- * Same as <code>evalAsMap(textFromUTF8File, null, false, null)</code>.
- * The file must use UTF-8 encoding. Initial BOM is allowed.
- * @throws IOException
- * @see #evalAsMap(String, EvaluationEnvironment, boolean, String)
- */
public static Map<String, Object> evalAsMap(File f)
throws EvaluationException, IOException {
+ return evalAsMap(f, null, false);
+ }
+
+ /**
+ * Same as <code>evalAsMap(textFromUTF8File, null, false, null)</code>.
+ * Loads the file with {@link #loadCJSONFile}.
+ * @see #evalAsMap(String, EvaluationEnvironment, boolean, String)
+ */
+ public static Map<String, Object> evalAsMap(File f, EvaluationEnvironment ee, boolean forceStringValues)
+ throws EvaluationException, IOException {
String s;
- InputStream in = new FileInputStream(f);
- try {
+ try (InputStream in = new FileInputStream(f)) {
s = loadCJSONFile(in, f.getAbsolutePath());
- } finally {
- in.close();
}
- return evalAsMap(s, f.getAbsolutePath());
+ return evalAsMap(s, ee, forceStringValues, f.getAbsolutePath());
}
/**
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java
deleted file mode 100644
index 4c1e805..0000000
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.freemarker.docgen.core;
-
-import freemarker.core.Environment;
-import freemarker.template.TemplateException;
-
-/**
- * Exception thrown by docgen tag-s that are inside the XML text. As such, it's treated as the mistake of the document
- * author (as opposed to an internal error).
- */
-final class DocgenSubstitutionTemplateException extends TemplateException {
- public DocgenSubstitutionTemplateException(String description, Environment env) {
- super(description, env);
- }
-
- public DocgenSubstitutionTemplateException(String description, Exception cause, Environment env) {
- super(description, cause, env);
- }
-}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java
index c8110f0..55adcbf 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java
@@ -21,19 +21,16 @@
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -65,7 +62,9 @@
private static final String PARAM_TEXT = "text";
private static final String DOCGEN_TAG_START = "[docgen";
private static final String DOCGEN_TAG_END = "]";
+ private static final String DOCGEN_END_TAG_START = "[/docgen";
private static final String INSERT_FILE = "insertFile";
+ private static final String INSERT_OUTPUT = "insertOutput";
private final Transform transform;
@@ -144,40 +143,13 @@
insertCustomVariable(customVarName);
} else if (INSERT_FILE.equals(subvarName)) {
- skipWS();
- String pathArg = fetchRequiredString();
- String charsetArg = null;
- String fromArg = null;
- String toArg = null;
- String toIfPresentArg = null;
- Set<String> paramNamesSeen = new HashSet<>();
- while (skipWS()) {
- String paramName = fetchOptionalVariableName();
- skipRequiredToken("=");
- String paramValue = StringEscapeUtils.unescapeXml(fetchRequiredString());
- if (!paramNamesSeen.add(paramName)) {
- throw new TemplateException(
- "Duplicate " + StringUtil.jQuote(INSERT_FILE)
- + " parameter " + StringUtil.jQuote(paramName) + ".", env);
- }
- if (paramName.equals("charset")) {
- charsetArg = paramValue;
- } else if (paramName.equals("from")) {
- fromArg = paramValue;
- } else if (paramName.equals("to")) {
- toArg = paramValue;
- } else if (paramName.equals("toIfPresent")) {
- toIfPresentArg = paramValue;
- } else {
- throw new TemplateException(
- "Unsupported " + StringUtil.jQuote(INSERT_FILE)
- + " parameter " + StringUtil.jQuote(paramName) + ".", env);
- }
- }
- skipRequiredToken(DOCGEN_TAG_END);
+ InsertDirectiveArgs args = fetchInsertDirectiveArgs(subvarName, true, true, false);
lastUnprintedIdx = cursor;
-
- insertFile(pathArg, charsetArg, fromArg, toArg, toIfPresentArg);
+ insertFile(args);
+ } else if (INSERT_OUTPUT.equals(subvarName)) {
+ InsertDirectiveArgs args = fetchInsertDirectiveArgs(subvarName, false, false, true);
+ lastUnprintedIdx = cursor;
+ insertOutput(args);
} else {
throw new TemplateException(
"Unsupported docgen subvariable " + StringUtil.jQuote(subvarName) + ".", env);
@@ -239,11 +211,9 @@
}
}
- private void insertFile(String pathArg, String charsetArg, String fromArg,
- String toArg, String toIfPresentArg)
- throws TemplateException, IOException {
- int slashIndex = pathArg.indexOf("/");
- String symbolicNameStep = slashIndex != -1 ? pathArg.substring(0, slashIndex) : pathArg;
+ private void insertFile(InsertDirectiveArgs args) throws TemplateException, IOException {
+ int slashIndex = args.path.indexOf("/");
+ String symbolicNameStep = slashIndex != -1 ? args.path.substring(0, slashIndex) : args.path;
if (!symbolicNameStep.startsWith("@") || symbolicNameStep.length() < 2) {
throw newErrorInDocgenTag("Path argument must start with @<symbolicName>/, "
+ " where <symbolicName> is in " + transform.getInsertableFiles().keySet() + ".");
@@ -257,7 +227,7 @@
}
symbolicNamePath = symbolicNamePath.toAbsolutePath().normalize();
Path resolvedFilePath = slashIndex != -1
- ? symbolicNamePath.resolve(pathArg.substring(slashIndex + 1))
+ ? symbolicNamePath.resolve(args.path.substring(slashIndex + 1))
: symbolicNamePath;
resolvedFilePath = resolvedFilePath.normalize();
if (!resolvedFilePath.startsWith(symbolicNamePath)) {
@@ -270,11 +240,11 @@
}
Charset charset;
- if (charsetArg != null) {
+ if (args.charset != null) {
try {
- charset = Charset.forName(charsetArg);
+ charset = Charset.forName(args.charset);
} catch (UnsupportedCharsetException e) {
- throw newErrorInDocgenTag("Unsupported charset: " + charsetArg);
+ throw newErrorInDocgenTag("Unsupported charset: " + args.charset);
}
} else {
charset = StandardCharsets.UTF_8;
@@ -287,70 +257,29 @@
fileContent = removeFTLCopyrightComment(fileContent);
}
- if (fromArg != null) {
- boolean optional;
- String fromArgCleaned;
- if (fromArg.startsWith("?")) {
- optional = true;
- fromArgCleaned = fromArg.substring(1);
- } else {
- optional = false;
- fromArgCleaned = fromArg;
- }
- Pattern from;
- try {
- from = Pattern.compile(fromArgCleaned, Pattern.MULTILINE);
- } catch (PatternSyntaxException e) {
- throw newErrorInDocgenTag("Invalid regular expression: " + fromArgCleaned);
- }
- Matcher matcher = from.matcher(fileContent);
+ if (args.from != null) {
+ Matcher matcher = args.from.matcher(fileContent);
if (matcher.find()) {
String remaining = fileContent.substring(matcher.start());
fileContent = "[\u2026]"
+ (remaining.startsWith("\n") || remaining.startsWith("\r") ? "" : "\n")
+ remaining;
- } else {
- if (!optional) {
- throw newErrorInDocgenTag(
- "Regular expression has no match in the file content: " + fromArg);
- }
+ } else if (!args.fromOptional) {
+ throw newErrorInDocgenTag(
+ "\"from\" regular expression has no match in the file content: " + args.from);
}
}
- String toStr;
- boolean toPresenceOptional;
- if (toArg != null) {
- if (toIfPresentArg != null) {
- throw newErrorInDocgenTag(
- "Can't use both \"to\" and \"toIfPresent\" argument.");
- }
- toStr = toArg;
- toPresenceOptional = false;
- } else if (toIfPresentArg != null) {
- toStr = toIfPresentArg;
- toPresenceOptional = true;
- } else {
- toStr = null;
- toPresenceOptional = false;
- }
- if (toStr != null) {
- Pattern to;
- try {
- to = Pattern.compile(toStr, Pattern.MULTILINE);
- } catch (PatternSyntaxException e) {
- throw newErrorInDocgenTag("Invalid regular expression: " + toStr);
- }
- Matcher matcher = to.matcher(fileContent);
+ if (args.to != null) {
+ Matcher matcher = args.to.matcher(fileContent);
if (matcher.find()) {
String remaining = fileContent.substring(0, matcher.start());
fileContent = remaining
+ (remaining.endsWith("\n") || remaining.endsWith("\r") ? "" : "\n")
+ "[\u2026]";
- } else {
- if (!toPresenceOptional) {
- throw newErrorInDocgenTag(
- "Regular expression has no match in the file content: " + toStr);
- }
+ } else if (!args.toOptional) {
+ throw newErrorInDocgenTag(
+ "\"to\" regular expression has no match in the file content: " + args.to);
}
}
@@ -358,6 +287,28 @@
}
}
+ private void insertOutput(InsertDirectiveArgs args) throws TemplateException, IOException {
+ List<String> splitCmdLine = BashCommandLineArgsParser.parse(args.body);
+ if (splitCmdLine.isEmpty()) {
+ throw newErrorInDocgenTag("Command to execute was empty");
+ }
+ String cmdKey = splitCmdLine.get(0);
+ List<String> cmdArgs = splitCmdLine.subList(1, splitCmdLine.size());
+ Map<String, Transform.InsertableOutputCommandProperties> cmdPropsMap =
+ transform.getInsertableOutputCommands();
+ Transform.InsertableOutputCommandProperties cmdProps = cmdPropsMap.get(cmdKey);
+ if (cmdProps == null) {
+ throw newErrorInDocgenTag(
+ "The " + Transform.SETTING_INSERTABLE_OUTPUT_COMMANDS
+ + " configuration setting doesn't have entry with key " + StringUtil.jQuote(cmdKey)
+ + ". "
+ + (cmdPropsMap.isEmpty()
+ ? "That setting is empty."
+ : "It has these keys: " + String.join(", ", cmdPropsMap.keySet())));
+ }
+ HTMLOutputFormat.INSTANCE.output("!!T\n" + cmdProps + "\n" + cmdArgs, out);
+ }
+
private TemplateException newFormattingFailedException(String customVarName, TemplateValueFormatException e) {
return new TemplateException(
"Formatting failed for Docgen custom variable "
@@ -365,8 +316,8 @@
e, env);
}
- private int findNextDocgenTagStart(int lastUnprintedIdx) {
- int startIdx = text.indexOf(DOCGEN_TAG_START, lastUnprintedIdx);
+ private int findNextDocgenTagStart(int fromIndex) {
+ int startIdx = text.indexOf(DOCGEN_TAG_START, fromIndex);
if (startIdx == -1) {
return -1;
}
@@ -378,6 +329,25 @@
return -1;
}
+ private int findNextDocgenEndTag(int fromIndex) {
+ int startIdx = text.indexOf(DOCGEN_END_TAG_START, fromIndex);
+ if (startIdx == -1) {
+ return -1;
+ }
+ int afterTagStartIdx = startIdx + DOCGEN_END_TAG_START.length();
+ if (afterTagStartIdx < text.length()
+ && !Character.isJavaIdentifierPart(text.charAt(afterTagStartIdx))) {
+ return startIdx;
+ }
+ return -1;
+ }
+
+ private void skipRequiredWS() throws DocgenTagException {
+ if (!skipWS()) {
+ throw newUnexpectedTokenException("whitespace", env);
+ }
+ }
+
private boolean skipWS() {
boolean found = false;
while (cursor < text.length()) {
@@ -454,36 +424,139 @@
int stringStartIdx = cursor;
while (cursor < text.length() && charAt(cursor) != quoteChar) {
if (!rawString && charAt(cursor) == '\\') {
- throw new DocgenSubstitutionTemplateException(
+ throw new DocgenTagException(
"Backslash is currently not supported in string literal in Docgen tags, "
+ "except in raw strings (like r\"regular\\s+expression\").", env);
}
cursor++;
}
if (charAt(cursor) != quoteChar) {
- throw new DocgenSubstitutionTemplateException("Unclosed string literal in a Docgen tag.", env);
+ throw new DocgenTagException("Unclosed string literal in a Docgen tag.", env);
}
String result = text.substring(stringStartIdx, cursor);
cursor++;
return result;
}
+ private boolean fetchRequiredBoolean() throws TemplateException {
+ Boolean result = fetchOptionalBoolean();
+ if (result == null) {
+ throw newUnexpectedTokenException("boolean", env);
+ }
+ return result;
+ }
+
+ private Boolean fetchOptionalBoolean() throws DocgenTagException {
+ String name = fetchOptionalVariableName();
+ if (name == null) {
+ return null;
+ }
+ if (name.equals("true")) {
+ return true;
+ } else if (name.equals("false")) {
+ return false;
+ } else {
+ throw new DocgenTagException("true or false", env);
+ }
+ }
+
+
private char charAt(int index) {
return index < text.length() ? text.charAt(index) : 0;
}
- private TemplateException newUnexpectedTokenException(String expectedTokenDesc, Environment env) {
- return new DocgenSubstitutionTemplateException(
+ private DocgenTagException newUnexpectedTokenException(String expectedTokenDesc, Environment env) {
+ return new DocgenTagException(
"Expected " + expectedTokenDesc + " after this: " + text.substring(lastDocgenTagStart, cursor),
env);
}
private TemplateException newErrorInDocgenTag(String errorDetail) {
- return new DocgenSubstitutionTemplateException(
+ return new DocgenTagException(
"\nError in docgen tag: " + text.substring(lastDocgenTagStart, cursor) + "\n" + errorDetail,
env);
}
+
+ private InsertDirectiveArgs fetchInsertDirectiveArgs(
+ String subvarName, boolean hasPath, boolean allowCharsetArg, boolean hasBodyArg) throws
+ TemplateException {
+ InsertDirectiveArgs args = new InsertDirectiveArgs();
+ args.toOptional = true;
+
+ if (hasPath) {
+ skipWS();
+ args.path = fetchRequiredString();
+ }
+
+ Set<String> paramNamesSeen = new HashSet<>();
+ String paramName;
+ while (skipWS() && (paramName = fetchOptionalVariableName()) != null) {
+ skipRequiredToken("=");
+ if (!paramNamesSeen.add(paramName)) {
+ throw new DocgenTagException(
+ "Duplicate docgen." + subvarName + " parameter " + StringUtil.jQuote(paramName) + ".",
+ env);
+ }
+ if (allowCharsetArg && paramName.equals("charset")) {
+ args.charset = StringEscapeUtils.unescapeXml(fetchRequiredString());
+ } else if (paramName.equals("from")) {
+ args.from = parseRegularExpressionParam(paramName, StringEscapeUtils.unescapeXml(fetchRequiredString()));
+ } else if (paramName.equals("to")) {
+ args.to = parseRegularExpressionParam(paramName, StringEscapeUtils.unescapeXml(fetchRequiredString()));
+ } else if (paramName.equals("fromOptional")) {
+ args.fromOptional = fetchRequiredBoolean();
+ } else if (paramName.equals("toOptional")) {
+ args.toOptional = fetchRequiredBoolean();
+ } else {
+ throw new DocgenTagException(
+ "Unsupported docgen." + subvarName + " parameter " + StringUtil.jQuote(paramName) + ".",
+ env);
+ }
+ }
+
+ skipRequiredToken(DOCGEN_TAG_END);
+ int indexAfterStartTag = cursor;
+
+ if (hasBodyArg) {
+ int endTagIndex = findNextDocgenEndTag(cursor);
+ if (endTagIndex == -1) {
+ throw new DocgenTagException(
+ "Missing docgen end-tag after " + DOCGEN_TAG_START + "." + subvarName + " ...]", env);
+ }
+ lastDocgenTagStart = endTagIndex;
+
+ args.body = StringEscapeUtils.unescapeXml(text.substring(indexAfterStartTag, endTagIndex));
+
+ cursor = endTagIndex + DOCGEN_END_TAG_START.length();
+ skipRequiredToken(".");
+ String endSubvarName = fetchRequiredVariableName();
+ if (!endSubvarName.equals(subvarName)) {
+ throw new DocgenTagException(
+ "End-tag " + DOCGEN_END_TAG_START + "." + endSubvarName + "] doesn't match "
+ + DOCGEN_TAG_START + "." + subvarName + " ...] tag.", env);
+ }
+ skipRequiredToken("]");
+ }
+
+ args.indexAfterDirective = cursor;
+
+ return args;
+ }
+
+ private Pattern parseRegularExpressionParam(String paramName, String paramValue) throws TemplateException {
+ Objects.requireNonNull(paramName);
+ Objects.requireNonNull(paramValue);
+ Pattern parsedParamValue;
+ try {
+ parsedParamValue = Pattern.compile(paramValue, Pattern.MULTILINE);
+ } catch (PatternSyntaxException e) {
+ throw newErrorInDocgenTag("Invalid regular expression for parameter \"" +
+ paramName + "\": " + paramValue);
+ }
+ return parsedParamValue;
+ }
+
}
public static String removeFTLCopyrightComment(String ftl) {
@@ -534,4 +607,15 @@
return ftl.substring(0, commentFirstIdx) + ftl.substring(commentLastIdx + afterCommentNLChars + 1);
}
+ static class InsertDirectiveArgs {
+ private String path;
+ private String charset;
+ private Pattern from;
+ private boolean fromOptional;
+ private Pattern to;
+ private boolean toOptional;
+ private String body;
+ private int indexAfterDirective;
+ }
+
}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
index 37da9ab..20eaead 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
@@ -45,7 +45,6 @@
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TimeZone;
@@ -59,10 +58,7 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
@@ -158,6 +154,19 @@
static final String SETTING_NUMBERED_SECTIONS = "numberedSections";
static final String SETTING_CUSTOM_VARIABLES = "customVariables";
static final String SETTING_INSERTABLE_FILES = "insertableFiles";
+ static final String SETTING_INSERTABLE_OUTPUT_COMMANDS = "insertableOutputCommands";
+ static final String SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY = "class";
+ static final String SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY = "prependedArguments";
+ static final String SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY = "workDirectory";
+ static final Set<String> SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS;
+ static final Set<String> SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS;
+ static {
+ SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS = new LinkedHashSet<>();
+ SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY);
+ SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS = new LinkedHashSet<>();
+ SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY);
+ SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY);
+ }
static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE
= "programlistingsRequireRole";
@@ -428,6 +437,8 @@
private final Map<String, String> insertableFilesFromSettingsFile = new HashMap<>();
private final Map<String, String> insertableFilesOverrides = new HashMap<>();
+ private final Map<String, InsertableOutputCommandProperties> insertableOutputCommands = new HashMap<>();
+
private final LinkedHashMap<String, String> tabs = new LinkedHashMap<>();
private final Map<String, Map<String, String>> secondaryTabs = new LinkedHashMap<>();
@@ -537,10 +548,9 @@
if (cfgFile.exists()) {
Map<String, Object> cfg;
try {
- cfg = CJSONInterpreter.evalAsMap(cfgFile);
+ cfg = CJSONInterpreter.evalAsMap(cfgFile, new DocgenCJSONEvaluationEnvironment(), false);
} catch (CJSONInterpreter.EvaluationException e) {
- throw new DocgenException(e.getMessage(),
- e.getCause());
+ throw new DocgenException(e.getMessage(), e.getCause());
}
for (Entry<String, Object> cfgEnt : cfg.entrySet()) {
@@ -607,6 +617,36 @@
insertableFilesFromSettingsFile.putAll(
// Allow null values in the Map, as the caller can override them.
castSettingToMap(settingName, settingValue, String.class, String.class, true));
+ } else if (topSettingName.equals(SETTING_INSERTABLE_OUTPUT_COMMANDS)) {
+ Map<String, Map<String, Object>> m = castSetting(
+ settingName, settingValue,
+ Map.class,
+ new MapEntryType(String.class, Map.class),
+ new MapEntryType(
+ String.class, SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS, SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS,
+ Object.class, false));
+ for (Entry<String, Map<String, Object>> ent : m.entrySet()) {
+ String commandKey = ent.getKey();
+ Map<String, Object> outputCmdProps = ent.getValue();
+ InsertableOutputCommandProperties commandProps = new InsertableOutputCommandProperties(
+ castSetting(
+ settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY),
+ outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY),
+ String.class
+ ),
+ castSetting(
+ settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY),
+ outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY),
+ List.class
+ ),
+ Paths.get(castSetting(
+ settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY),
+ outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY),
+ String.class
+ ))
+ );
+ insertableOutputCommands.put(commandKey, commandProps);
+ }
} else if (topSettingName.equals(SETTING_TABS)) {
tabs.putAll(
castSettingToMap(settingName, settingValue, String.class, String.class));
@@ -1151,8 +1191,8 @@
private Map<String, Object> computeCustomVariables() throws DocgenException {
for (String varName : customVariableOverrides.keySet()) {
if (!customVariablesFromSettingsFile.containsKey(varName)) {
- throw new DocgenException("Attempt to set custom variable " + StringUtil.jQuote(varName)
- + ", when it was not set in the settings file (" + FILE_SETTINGS + ").");
+ throw new DocgenException("Attempt to override custom variable " + StringUtil.jQuote(varName)
+ + ", when it was not set in the settings file (" + cfgFile + ").");
}
}
@@ -2500,7 +2540,11 @@
return insertableFiles;
}
- // -------------------------------------------------------------------------
+ public Map<String, InsertableOutputCommandProperties> getInsertableOutputCommands() {
+ return insertableOutputCommands;
+ }
+
+// -------------------------------------------------------------------------
public File getDestinationDirectory() {
return destDir;
@@ -2734,4 +2778,85 @@
}
}
+ static class InsertableOutputCommandProperties {
+ private final String mainClassName;
+ private final List<String> prependedArguments;
+ private final Path workDirectory;
+
+ public InsertableOutputCommandProperties(String mainClassName, List<String> prependedArguments, Path workDirectory) {
+ this.mainClassName = mainClassName;
+ this.prependedArguments = prependedArguments;
+ this.workDirectory = workDirectory;
+ }
+
+ @Override
+ public String toString() {
+ return "InsertableOutputCommandProperties{"
+ + "mainClassName='" + mainClassName + '\''
+ + ", prependedArguments=" + prependedArguments
+ + ", workDirectory=" + workDirectory + '}';
+ }
+ }
+
+ @FunctionalInterface
+ interface CJSONFunction {
+ Object run(Transform context, CJSONInterpreter.FunctionCall fc);
+ }
+
+ private static final Map<String, CJSONFunction> CJSON_FUNCTIONS = ImmutableMap.of(
+ "getCustomVariable",
+ (ctx, fc) -> {
+ List<Object> params = fc.getParams();
+ if (params.size() != 1) {
+ throw new DocgenException(
+ "CJSON function " + fc.getName() + "(name) "
+ + "should have 1 arguments, but had " + params.size() + ".");
+ }
+
+ Object varName = params.get(0);
+ if (!(varName instanceof String)) {
+ throw new DocgenException(
+ "CJSON function " + fc.getName() + "(name) "
+ + "argument should be a string, but was a(n) "
+ + CJSONInterpreter.cjsonTypeNameOfValue(varName) + ".");
+ }
+
+ Object result = ctx.customVariableOverrides.get(varName);
+ if (result == null) {
+ result = ctx.customVariablesFromSettingsFile.get(varName);
+ }
+ if (result == null) {
+ throw new DocgenException(
+ "The custom variable " + StringUtil.jQuote(varName) + " is not set (or was set to null).");
+ }
+ return result;
+ },
+ "concat",
+ (ctx, fc) -> {
+ return fc.getParams().stream()
+ .filter(it -> it != null)
+ .map(Object::toString)
+ .collect(Collectors.joining());
+ }
+ );
+
+ class DocgenCJSONEvaluationEnvironment implements CJSONInterpreter.EvaluationEnvironment {
+ @Override
+ public Object evalFunctionCall(CJSONInterpreter.FunctionCall fc, CJSONInterpreter ip) {
+ String name = fc.getName();
+ CJSONFunction f = CJSON_FUNCTIONS.get(name);
+ if (f == null) {
+ throw new DocgenException("Unknown CJSON function: " + name
+ + "\nSupported functions are: " + String.join(", ", CJSON_FUNCTIONS.keySet()));
+ }
+ return f.run(Transform.this, fc);
+ }
+
+ @Override
+ public Object notify(CJSONInterpreter.EvaluationEvent event, CJSONInterpreter ip, String name, Object extra) throws
+ Exception {
+ return null;
+ }
+ }
+
}
diff --git a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java
new file mode 100644
index 0000000..8a9db4a
--- /dev/null
+++ b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+class BashCommandLineArgsParserTest {
+
+ @Test
+ void parse() {
+ assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse(""));
+ assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse( " "));
+ assertEquals(Arrays.asList("cmd", "1", "2", "3"), BashCommandLineArgsParser.parse("cmd 1\t2\r\n3"));
+ assertEquals(Arrays.asList("1 x", "2 x", "a'bcd"), BashCommandLineArgsParser.parse("'1 x' \"2 x\" a\"'\"b'c'd"));
+ assertEquals(Arrays.asList("abc"), BashCommandLineArgsParser.parse("a\\bc"));
+ assertEquals(Arrays.asList("a\\bc"), BashCommandLineArgsParser.parse("a\\\\bc"));
+ assertEquals(Arrays.asList("a'bc", "d e"), BashCommandLineArgsParser.parse("a\\'bc d\\ \\ e"));
+ assertEquals(Arrays.asList("a\\b\\c\"$"), BashCommandLineArgsParser.parse("\"a\\b\\\\c\\\"\\$\""));
+ assertEquals(Arrays.asList("a\\b\\\\c"), BashCommandLineArgsParser.parse("'a\\b\\\\c'"));
+ }
+
+}
\ No newline at end of file