This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/freemarker-docgen.git


The following commit(s) were added to refs/heads/master by this push:
     new 8f290de  Fixed several glitches in new features. Renamed insertOutput 
to insertWithOutput, and made insertCommand=true the default. Added 
docgen.verifyCommand, that doesn't insert output, just ensures that the command 
doesn't fail.
8f290de is described below

commit 8f290de844ce74c3bdcc91af5038fb14a431f43b
Author: ddekany <[email protected]>
AuthorDate: Sun Feb 14 21:09:18 2021 +0100

    Fixed several glitches in new features. Renamed insertOutput to 
insertWithOutput, and made insertCommand=true the default. Added 
docgen.verifyCommand, that doesn't insert output, just ensures that the command 
doesn't fail.
---
 .../docgen/core/BashCommandLineArgsParser.java     |  44 ++++-
 .../docgen/core/ChopLinebreakDirective.java        |  48 ++++++
 .../docgen/core/ChopLinebreakWriter.java           |  99 ++++++++++++
 .../freemarker/docgen/core/FilterDirective.java    |  52 ++++++
 .../PrintTextWithDocgenSubstitutionsDirective.java | 180 ++++++++++++++++-----
 .../org/freemarker/docgen/core/SettingName.java    |  44 ++++-
 .../org/freemarker/docgen/core/SettingUtils.java   |   1 -
 .../java/org/freemarker/docgen/core/Transform.java |  11 +-
 .../docgen/core/templates/node-handlers.ftlh       |   3 +-
 .../docgen/core/BashCommandLineArgsParserTest.java |   9 +-
 .../docgen/core/ChopLinebreakWriterTest.java       | 146 +++++++++++++++++
 .../docgen/core/EscapeHtmlAndXmlWriterTest.java    |  75 +++++++++
 .../freemarker/docgen/core/SettingNameTest.java    |   2 +
 13 files changed, 656 insertions(+), 58 deletions(-)

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
index c464b34..662cfb2 100644
--- 
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
@@ -47,7 +47,7 @@ public class BashCommandLineArgsParser {
     }
 
     private String skipWSAndFetchArg() {
-        skipWS();
+        skipBashWS();
         return fetchArg();
     }
 
@@ -58,11 +58,24 @@ public class BashCommandLineArgsParser {
         boolean escaped = false;
         while (pos < src.length()) {
             char c = src.charAt(pos);
+
+            // Get rid of Windows and Mac line-breaks:
+            if (c == '\r') {
+                if (pos + 1 < src.length()) {
+                    if (src.charAt(pos + 1) == '\n') {
+                        pos++;
+                    }
+                }
+                c = '\n';
+            }
+
             if (escaped) {
-                if (openedQuote == '"' && !(c == '"' || c == '\\' || c == 
'$')) {
+                if (openedQuote == '"' && !(c == '"' || c == '\\' || c == '$' 
|| c == '\n')) {
                     arg.append('\\');
+                    arg.append(c);
+                } else if (c != '\n') { // Otherwise it's an escaped 
line-break, so we just drop it to join lines.
+                    arg.append(c);
                 }
-                arg.append(c);
                 escaped = false;
             } else {
                 if (c == '"' || c == '\'') {
@@ -86,13 +99,32 @@ public class BashCommandLineArgsParser {
         return startPos != pos ? arg.toString() : null;
     }
 
-    private void skipWS() {
-        while (pos < src.length() && isWS(src.charAt(pos))) {
+    private void skipBashWS() {
+        while (pos < src.length()) {
+            char c = src.charAt(pos);
+
+            if (c == '\\') {
+                if (pos + 1 < src.length() && isLinebreak(src.charAt(pos + 
1))) {
+                    // Skip escaped linebreak as whitespace
+                    if (src.charAt(pos + 1) == '\r' && pos + 2 < src.length() 
&& src.charAt(pos + 2) == '\n') {
+                        pos++;
+                    }
+                } else {
+                    break;
+                }
+            } else if (!isWS(c)) {
+                break;
+            }
             pos++;
         }
     }
 
     private boolean isWS(char c) {
-        return c == ' ' || c == '\n' || c == '\r' || c == '\t';
+        return c == ' ' || c == '\t' || isLinebreak(c);
+    }
+
+    private boolean isLinebreak(char c) {
+        return c == '\n' || c == '\r';
     }
+
 }
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakDirective.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakDirective.java
new file mode 100644
index 0000000..599b1a2
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakDirective.java
@@ -0,0 +1,48 @@
+/*
+ * 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.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import freemarker.core.Environment;
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTransformModel;
+
+/**
+ * Similar to <code>${capturedContent?chopLinebreak}</code>, but it's 
"streaming", which is important if we want the
+ * partial output when there's an exception in the mid of generating the 
content.
+ */
+class ChopLinebreakDirective extends FilterDirective {
+    static final ChopLinebreakDirective INSTANCE = new 
ChopLinebreakDirective();
+
+    private ChopLinebreakDirective() {
+    }
+
+    @Override
+    protected Writer wrapWriter(Writer out) {
+        return new ChopLinebreakWriter(out);
+    }
+}
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakWriter.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakWriter.java
new file mode 100644
index 0000000..2020e96
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ChopLinebreakWriter.java
@@ -0,0 +1,99 @@
+/*
+ * 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.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Writer that passes content through to a wrapped {@link Writer}, but will 
remove the line-break from the end of what
+ * was written so far.
+ */
+class ChopLinebreakWriter extends FilterWriter {
+    private String pendingLinebreak;
+
+    protected ChopLinebreakWriter(Writer out) {
+        super(out);
+    }
+
+    @Override
+    public void write(int c) throws IOException {
+        if (isLinebreakChar(c)) {
+            if (pendingLinebreak == null) {
+                pendingLinebreak = Character.toString((char) c);
+            } else {
+                if (c == '\n' && pendingLinebreak.equals("\r")) {
+                    pendingLinebreak = "\r\n";
+                } else {
+                    commitPendingLinebreak();
+                    pendingLinebreak = Character.toString((char) c);
+                }
+            }
+        } else {
+            commitPendingLinebreak();
+            out.write(c);
+        }
+    }
+
+    private static boolean isLinebreakChar(int c) {
+        return c == '\n' || c == '\r';
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        int endOff = off + len;
+        int lastNonBrIndex = endOff - 1;
+        while (lastNonBrIndex >= off && isLinebreakChar(cbuf[lastNonBrIndex])) 
{
+            lastNonBrIndex--;
+        }
+        if (lastNonBrIndex >= off) {
+            commitPendingLinebreak();
+            out.write(cbuf, off, lastNonBrIndex + 1 - off);
+        }
+        for (int i = lastNonBrIndex + 1; i < endOff; i++) {
+            write(cbuf[i]);
+        }
+    }
+
+    @Override
+    public void write(String str, int off, int len) throws IOException {
+        int endOff = off + len;
+        int lastNonBrIndex = endOff - 1;
+        while (lastNonBrIndex >= off && 
isLinebreakChar(str.charAt(lastNonBrIndex))) {
+            lastNonBrIndex--;
+        }
+        if (lastNonBrIndex >= off) {
+            commitPendingLinebreak();
+            out.write(str, off, lastNonBrIndex + 1 - off);
+        }
+        for (int i = lastNonBrIndex + 1; i < endOff; i++) {
+            write(str.charAt(i));
+        }
+    }
+
+    private void commitPendingLinebreak() throws IOException {
+        if (pendingLinebreak != null) {
+            out.write(pendingLinebreak);
+            pendingLinebreak = null;
+        }
+    }
+
+}
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/FilterDirective.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/FilterDirective.java
new file mode 100644
index 0000000..a319216
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/FilterDirective.java
@@ -0,0 +1,52 @@
+/*
+ * 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.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import freemarker.core.Environment;
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+
+abstract class FilterDirective implements TemplateDirectiveModel {
+    protected FilterDirective() {
+    }
+
+    @Override
+    public void execute(Environment env, Map params, TemplateModel[] loopVars, 
TemplateDirectiveBody body) throws
+            TemplateException, IOException {
+        if (!params.isEmpty()) {
+            throw new TemplateException("This directive doesn't support any 
parameters", env);
+        }
+        if (loopVars.length != 0) {
+            throw new TemplateException("This directive doesn't support any 
loop variables", env);
+        }
+        if (body == null) {
+            return;
+        }
+        body.render(wrapWriter(env.getOut()));
+    }
+
+    protected abstract Writer wrapWriter(Writer out);
+}
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 b245e94..d2eb333 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
@@ -24,10 +24,8 @@ import static 
org.freemarker.docgen.core.PrintTextWithDocgenSubstitutionsDirecti
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
-import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.nio.charset.Charset;
@@ -48,6 +46,7 @@ import java.util.stream.Collectors;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.ClosedInputStream;
 import org.apache.commons.io.output.WriterOutputStream;
 import org.apache.commons.text.StringEscapeUtils;
 
@@ -68,6 +67,7 @@ import freemarker.template.TemplateModel;
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullWriter;
 import freemarker.template.utility.StringUtil;
 
 public class PrintTextWithDocgenSubstitutionsDirective implements 
TemplateDirectiveModel {
@@ -79,7 +79,8 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
 
     enum InsertDirectiveType {
         INSERT_FILE("insertFile"),
-        INSERT_OUTPUT("insertOutput");
+        INSERT_WITH_OUTPUT("insertWithOutput"),
+        CHECK_COMMAND("checkCommand");
 
         private final String directiveName;
 
@@ -127,7 +128,8 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
         new DocgenSubstitutionInterpreter(text, env).execute();
     }
 
-    private static final String DOCGEN_WD_TAG = "[docgen.wd]";
+    private static final String WD = "wd";
+    private static final String DOCGEN_WD_TAG = "[docgen." + WD + "]";
     private static final Pattern DOCGEN_WD_TAG_AND_SLASH_PATTERN = 
Pattern.compile(Pattern.quote(DOCGEN_WD_TAG) +  "/?");
 
     private class DocgenSubstitutionInterpreter {
@@ -171,10 +173,18 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                     InsertDirectiveArgs args = 
fetchInsertDirectiveArgs(subvarName, INSERT_FILE);
                     lastUnprintedIdx = cursor;
                     insertFile(args);
-                } else if (INSERT_OUTPUT.directiveName.equals(subvarName)) {
-                    InsertDirectiveArgs args = 
fetchInsertDirectiveArgs(subvarName, INSERT_OUTPUT);
+                } else if 
(INSERT_WITH_OUTPUT.directiveName.equals(subvarName)) {
+                    InsertDirectiveArgs args = 
fetchInsertDirectiveArgs(subvarName, INSERT_WITH_OUTPUT);
                     lastUnprintedIdx = cursor;
-                    insertOutput(args);
+                    insertCommandAndOutput(INSERT_WITH_OUTPUT, args);
+                } else if (CHECK_COMMAND.directiveName.equals(subvarName)) {
+                    InsertDirectiveArgs args = 
fetchInsertDirectiveArgs(subvarName, CHECK_COMMAND);
+                    lastUnprintedIdx = cursor;
+                    insertCommandAndOutput(CHECK_COMMAND, args);
+                } else if (subvarName.equals(WD)) {
+                    throw new TemplateException(
+                            "The " + WD + " docgen subvariable can only be 
used in the nested content of Docgen "
+                                    + "directives that specify a command to 
run.", env);
                 } else {
                     throw new TemplateException(
                             "Unsupported docgen subvariable " + 
StringUtil.jQuote(subvarName) + ".", env);
@@ -286,8 +296,9 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
             }
         }
 
-        private void insertOutput(InsertDirectiveArgs args) throws 
TemplateException, IOException {
-            if (args.printCommand) {
+        private void insertCommandAndOutput(InsertDirectiveType 
insertDirectiveType, InsertDirectiveArgs args)
+                throws TemplateException, IOException {
+            if (args.printCommand || insertDirectiveType == CHECK_COMMAND) {
                 out.write("> ");
                 
out.write(DOCGEN_WD_TAG_AND_SLASH_PATTERN.matcher(StringUtil.chomp(args.body)).replaceAll(""));
                 out.write("\n");
@@ -313,14 +324,20 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
 
             Method mainMethod = getMainMethod(cmdKey, cmdProps);
 
-            StringWriter stdOutCapturer;
+            String cmdRunExceptionShortMessage;
+            TemplateException cmdRunException;
+            Writer outCapturer;
             PrintStream prevOut = System.out;
+            PrintStream prevErr = System.err;
+            InputStream prevIn = System.in;
             Map<String, String> prevSystemProperties = new HashMap<>();
             try {
-                stdOutCapturer = new StringWriter();
-                PrintStream stdOutCapturerPrintStream = new PrintStream(
-                        new WriterOutputStream(stdOutCapturer, 
Charset.defaultCharset()));
-                System.setOut(stdOutCapturerPrintStream);
+                outCapturer = insertDirectiveType != CHECK_COMMAND ? new 
StringWriter() : NullWriter.INSTANCE;
+                PrintStream outCapturerPrintStream = new PrintStream(
+                        new WriterOutputStream(outCapturer, 
Charset.defaultCharset()));
+                System.setOut(outCapturerPrintStream);
+                System.setErr(outCapturerPrintStream);
+                System.setIn(ClosedInputStream.CLOSED_INPUT_STREAM);
 
                 cmdProps.getSystemProperties().forEach((k, v) -> {
                     String prevValue = setOrClearSystemProperty(k, v);
@@ -342,28 +359,70 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                         })
                         .collect(Collectors.toList());
 
-                Object returnValue;
+                Object cmdExitCode;
                 try {
-                    returnValue = mainMethod.invoke(null, (Object) 
cmdArgs.toArray(new String[0]));
+                    cmdExitCode = mainMethod.invoke(null, (Object) 
cmdArgs.toArray(new String[0]));
+                    if (cmdExitCode instanceof Integer && ((Integer) 
cmdExitCode) != 0) {
+                        cmdRunExceptionShortMessage = "Command execution has 
returned with non-0 exit code " + cmdExitCode + ".";
+                        cmdRunException = 
newErrorInInsertOutputCommandException(
+                                cmdRunExceptionShortMessage,
+                                cmdProps, cmdArgs,
+                                null);
+                    } else {
+                        cmdRunExceptionShortMessage = null;
+                        cmdRunException = null;
+                    }
                 } catch (Exception e) {
-                    throw newErrorInDocgenTag("Error when executing command 
with "
-                                    + cmdProps.getMainClassName() + "." + 
cmdProps.getMainMethodName()
-                                    + ", and arguments " + cmdArgs + ".",
+                    cmdRunExceptionShortMessage = "The main method has thrown 
this exception:\n" + e;
+                    cmdRunException = newErrorInInsertOutputCommandException(
+                            cmdRunExceptionShortMessage,
+                            cmdProps, cmdArgs,
                             e);
                 }
-                if (returnValue instanceof Integer && ((Integer) returnValue) 
!= 0) {
-                    throw newErrorInDocgenTag(
-                            "Command execution has returned with non-0 exit 
code " + returnValue
-                                    + ", from " + cmdProps.getMainClassName() 
+ "." + cmdProps.getMainMethodName()
-                                    + ", called with arguments " + cmdArgs + 
".");
-                }
 
-                stdOutCapturerPrintStream.flush();
+                outCapturerPrintStream.flush();
             } finally {
                 
prevSystemProperties.forEach(PrintTextWithDocgenSubstitutionsDirective::setOrClearSystemProperty);
+                System.setIn(prevIn);
+                System.setErr(prevErr);
                 System.setOut(prevOut);
             }
-            cutAndInsertContent(args, stdOutCapturer.toString());
+            if (cmdRunException == null) {
+                if (insertDirectiveType != CHECK_COMMAND) {
+                    cutAndInsertContent(args, outCapturer.toString());
+                }
+            } else {
+                out.write(
+                        "--------------------\n" +
+                        "Docgen " + INSERT_WITH_OUTPUT.directiveName + " 
directive failed: "
+                                + cmdRunExceptionShortMessage + "\n"
+                                + "The command was:\n"
+                                + StringUtil.chomp(args.body) + "\n\n"
+                                + "The output of the command (if any) until it 
failed:\n\n");
+                HTMLOutputFormat.INSTANCE.output(outCapturer.toString(), out);
+                throw cmdRunException;
+            }
+        }
+
+        private TemplateException newErrorInInsertOutputCommandException(
+                String specificMessage,
+                Transform.InsertableOutputCommandProperties cmdProps, 
List<String> cmdArgs,
+                Throwable e) {
+            String outputFileName = 
transform.getCurrentFileTOCNode().getOutputFileName();
+            return newErrorInDocgenTag(
+                    specificMessage
+                            + "\nCommand main method: "
+                            + cmdProps.getMainClassName() + "." + 
cmdProps.getMainMethodName()
+                            + "\nCommand arguments:"
+                            + (cmdArgs.size() != 0
+                                    ? "\n  "
+                                            + 
cmdArgs.stream().map(StringUtil::jQuote)
+                                                    
.collect(Collectors.joining("\n  "))
+                                            + "\n"
+                                    : " None")
+                            + "\nThe error message printed by the command 
itself, if any, can be found at the end of "
+                            + (outputFileName != null ? "\"" + outputFileName 
+ "\"" : "the output file") + ".",
+                    e);
         }
 
         private void cutAndInsertContent(InsertDirectiveArgs args, String 
content)
@@ -485,6 +544,37 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
             return found;
         }
 
+        private boolean skipLineBreak() {
+            int savedCursor = cursor;
+
+            // Skip horizontal whitespace
+            while (cursor < text.length()) {
+                char c = text.charAt(cursor);
+                if (c != ' ' && c != '\t' && c != '\u00A0') {
+                    break;
+                }
+                cursor++;
+            }
+
+            // Skip line-break:
+            if (cursor < text.length()) {
+                char c = text.charAt(cursor);
+                if (c == '\n') {
+                    cursor++;
+                    return true;
+                }
+                if (c == '\r') {
+                    cursor++;
+                    if (cursor < text.length() && text.charAt(cursor) == '\n') 
{
+                        cursor++;
+                    }
+                    return true;
+                }
+            }
+            cursor = savedCursor;
+            return false;
+        }
+
         private void skipRequiredToken(String token) throws TemplateException {
             if (!skipOptionalToken(token)) {
                 throw newUnexpectedTokenException(StringUtil.jQuote(token), 
env);
@@ -492,16 +582,17 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
         }
 
         private boolean skipOptionalToken(String token) throws 
TemplateException {
+            int savedCursor = cursor;
             skipWS();
             for (int i = 0; i < token.length(); i++) {
                 char expectedChar = token.charAt(i);
                 int lookAheadCursor = cursor + i;
                 if (charAt(lookAheadCursor) != expectedChar) {
+                    cursor = savedCursor;
                     return false;
                 }
             }
             cursor += token.length();
-            skipWS();
             return true;
         }
 
@@ -514,7 +605,10 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
         }
 
         private String fetchOptionalVariableName() {
+            int savedCursor = cursor;
+            skipWS();
             if (!Character.isJavaIdentifierStart(charAt(cursor))) {
+                cursor = savedCursor;
                 return null;
             }
             int varNameStart = cursor;
@@ -534,6 +628,8 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
         }
 
         private String fetchOptionalString() throws TemplateException {
+            int savedCursor = cursor;
+            skipWS();
             char quoteChar = charAt(cursor);
             boolean rawString = quoteChar == 'r';
             if (rawString) {
@@ -542,6 +638,7 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                 }
             }
             if (quoteChar != '"' && quoteChar != '\'') {
+                cursor = savedCursor;
                 return null;
             }
             cursor += rawString ? 2 : 1;
@@ -617,6 +714,7 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                 TemplateException {
             InsertDirectiveArgs args = new InsertDirectiveArgs();
             args.toOptional = true;
+            args.printCommand = true;
 
             if (insertDirectiveType == INSERT_FILE) {
                 skipWS();
@@ -632,18 +730,19 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                             "Duplicate docgen." + subvarName +  " parameter " 
+ StringUtil.jQuote(paramName) + ".",
                             env);
                 }
-                if (insertDirectiveType == INSERT_FILE && 
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")) {
+                boolean insertFileOrOutput = insertDirectiveType == 
INSERT_FILE || insertDirectiveType ==
+                        INSERT_WITH_OUTPUT;
+                if (insertFileOrOutput && paramName.equals("charset")) {
+                    args.charset = fetchRequiredString();
+                } else if (insertFileOrOutput && paramName.equals("from")) {
+                    args.from = parseRegularExpressionParam(paramName, 
fetchRequiredString());
+                } else if (insertFileOrOutput && paramName.equals("to")) {
+                    args.to = parseRegularExpressionParam(paramName, 
fetchRequiredString());
+                } else if (insertFileOrOutput && 
paramName.equals("fromOptional")) {
                     args.fromOptional = fetchRequiredBoolean();
-                } else if (paramName.equals("toOptional")) {
+                } else if (insertFileOrOutput && 
paramName.equals("toOptional")) {
                     args.toOptional = fetchRequiredBoolean();
-                } else if (insertDirectiveType == INSERT_OUTPUT
-                        && paramName.equals("printCommand")) {
+                } else if (insertDirectiveType == INSERT_WITH_OUTPUT && 
paramName.equals("printCommand")) {
                     args.printCommand = fetchRequiredBoolean();
                 } else {
                     throw new DocgenTagException(
@@ -653,9 +752,10 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
             }
 
             skipRequiredToken(DOCGEN_TAG_END);
+            skipLineBreak();
             int indexAfterStartTag = cursor;
 
-            if (insertDirectiveType == INSERT_OUTPUT) {
+            if (insertDirectiveType == INSERT_WITH_OUTPUT || 
insertDirectiveType == CHECK_COMMAND) {
                 int endTagIndex = findNextDocgenEndTag(cursor);
                 if (endTagIndex == -1) {
                     throw new DocgenTagException(
@@ -663,7 +763,7 @@ public class PrintTextWithDocgenSubstitutionsDirective 
implements TemplateDirect
                 }
                 lastDocgenTagStart = endTagIndex;
 
-                args.body = 
StringEscapeUtils.unescapeXml(text.substring(indexAfterStartTag, endTagIndex));
+                args.body = 
StringUtil.chomp(text.substring(indexAfterStartTag, endTagIndex));
 
                 cursor = endTagIndex + DOCGEN_END_TAG_START.length();
                 skipRequiredToken(".");
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
index 78bb341..952df37 100644
--- 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
@@ -22,8 +22,12 @@ package org.freemarker.docgen.core;
 import java.io.File;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 
+import freemarker.ext.beans.NumberModel;
+import freemarker.template.utility.StringUtil;
+
 final class SettingName {
     private final File parentFile;
     private final SettingName parent;
@@ -32,7 +36,11 @@ final class SettingName {
     public SettingName(File parentFile, SettingName parent, Object key) {
         this.parentFile = parentFile;
         this.parent = parent;
-        this.key = key;
+        this.key = Objects.requireNonNull(key);
+        if (!(key instanceof String) && !(key instanceof Number)) {
+            throw new IllegalArgumentException(
+                    "Key must be String or Number, but it was: " + 
key.getClass().getName());
+        }
     }
 
     static SettingName topLevel(File parentFile, String simpleName) {
@@ -44,7 +52,7 @@ final class SettingName {
     }
 
     SettingName subKey(Object... keys) {
-        return new SettingName(null,this, subKey(Arrays.asList(keys)));
+        return subKey(Arrays.asList(keys));
     }
 
     SettingName subKey(List<Object> keys) {
@@ -71,12 +79,36 @@ final class SettingName {
             parent.appendName(sb);
         }
         if (key instanceof String) {
-            if (sb.length() != 0) {
-                sb.append('.');
+            String strKey = (String) key;
+            if (isIdentifierLike(strKey)) {
+                if (sb.length() != 0) {
+                    sb.append('.');
+                }
+                sb.append(key);
+            } else {
+                if (sb.length() == 0) {
+                    sb.append("#ROOT");
+                }
+                sb.append('[').append(StringUtil.jQuote(key)).append(']');
             }
-            sb.append(key);
-        } else {
+        } else if (key instanceof Number) {
             sb.append('[').append(key).append(']');
         }
     }
+
+    private boolean isIdentifierLike(String s) {
+        if (s.length() == 0) {
+            return false;
+        }
+        if (!Character.isJavaIdentifierStart(s.charAt(0))) {
+            return false;
+        }
+        for (int i = 1; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (!Character.isJavaIdentifierPart(c) && c != '-') {
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
index 39c1f59..30e6a84 100644
--- 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
@@ -114,7 +114,6 @@ final class SettingUtils {
             throw newNullSettingValueException(settingName);
         }
         if (!valueType.isInstance(settingValue)) {
-            System.out.println("BAD VALUE: " + settingValue); //!!T
             throw newBadSettingValueTypeException(settingName, valueType, 
settingValue);
         }
 
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 ed3c6fc..f860012 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
@@ -1056,6 +1056,9 @@ public final class Transform {
             fmConfig.setSharedVariable(
                     "printTextWithDocgenSubstitutions",
                     new PrintTextWithDocgenSubstitutionsDirective(this));
+            fmConfig.setSharedVariable(
+                    "chopLinebreak",
+                    ChopLinebreakDirective.INSTANCE);
 
             // Calculated data:
             {
@@ -2573,12 +2576,16 @@ public final class Transform {
         return insertableFiles;
     }
 
+    TOCNode getCurrentFileTOCNode() {
+        return currentFileTOCNode;
+    }
+
+    // 
-------------------------------------------------------------------------
+
     public Map<String, InsertableOutputCommandProperties> 
getInsertableOutputCommands() {
         return insertableOutputCommands;
     }
 
-// -------------------------------------------------------------------------
-
     public File getDestinationDirectory() {
         return destDir;
     }
diff --git 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
index 3be163b..4075ae6 100644
--- 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
+++ 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
@@ -391,8 +391,7 @@
       <#if roleLabel != ''><div 
class="code-block-label">${roleLabel}</div></#if><#t>
       <pre class="code-block-body"><@Anchor/><#t>
         <#-- XXE and usual FO-stylesheet-compatible interpretation of inital 
line-breaks -->
-        <#local content><#recurse></#local>
-        ${content?markupString?chopLinebreak?noEsc}<#t>
+        <@chopLinebreak><#recurse></@><#t>
       </pre><#t>
     </div>
   </@CantBeNestedIntoP>
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
index 8a9db4a..6692bfe 100644
--- 
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
@@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test;
 class BashCommandLineArgsParserTest {
 
     @Test
-    void parse() {
+    void test1() {
         assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse(""));
         assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse( " "));
         assertEquals(Arrays.asList("cmd", "1", "2", "3"), 
BashCommandLineArgsParser.parse("cmd 1\t2\r\n3"));
@@ -40,4 +40,11 @@ class BashCommandLineArgsParserTest {
         assertEquals(Arrays.asList("a\\b\\\\c"), 
BashCommandLineArgsParser.parse("'a\\b\\\\c'"));
     }
 
+    @Test
+    void testBackslashLineBreak() {
+        assertEquals(Arrays.asList("ab", "c"), 
BashCommandLineArgsParser.parse("a\\\nb\\\n c"));
+        assertEquals(Arrays.asList("ab", "c"), 
BashCommandLineArgsParser.parse("a\\\r\nb\\\r\n c"));
+        assertEquals(Arrays.asList("a ab", "c"), 
BashCommandLineArgsParser.parse("a\\ a\\\r\nb \\\r\n c"));
+    }
+
 }
\ No newline at end of file
diff --git 
a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/ChopLinebreakWriterTest.java
 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/ChopLinebreakWriterTest.java
new file mode 100644
index 0000000..9f3216d
--- /dev/null
+++ 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/ChopLinebreakWriterTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.junit.jupiter.api.Test;
+
+class ChopLinebreakWriterTest {
+
+    @Test
+    public void testCharArg1() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        w.write('a');
+        w.write('\n');
+        w.write('b');
+        w.write('\n');
+        assertEquals("a\nb", sw.toString());
+    }
+
+    @Test
+    public void testStringArg1() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        w.write("a");
+        assertEquals("a", sw.toString());
+        w.write("b");
+        assertEquals("ab", sw.toString());
+        w.write("\r");
+        assertEquals("ab", sw.toString());
+        w.write("\n");
+        assertEquals("ab", sw.toString());
+        w.write("c");
+        assertEquals("ab\r\nc", sw.toString());
+        w.write("\ndef\r");
+        assertEquals("ab\r\nc\ndef", sw.toString());
+        w.write("g\n");
+        assertEquals("ab\r\nc\ndef\rg", sw.toString());
+        w.write("\nh");
+        assertEquals("ab\r\nc\ndef\rg\n\nh", sw.toString());
+    }
+
+    @Test
+    public void testStrignArg3() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        w.write("\n\n\n\r");
+        assertEquals("\n\n\n", sw.toString());
+        w.write("\n");
+        assertEquals("\n\n\n", sw.toString());
+        w.write("\r");
+        assertEquals("\n\n\n\r\n", sw.toString());
+        w.write("\n");
+        assertEquals("\n\n\n\r\n", sw.toString());
+        w.write("c");
+        assertEquals("\n\n\n\r\n\r\nc", sw.toString());
+    }
+
+    @Test
+    public void testStringArg2() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        w.write("a\n\n");
+        assertEquals("a\n", sw.toString());
+        w.write("b");
+        assertEquals("a\n\nb", sw.toString());
+    }
+
+    @Test
+    public void testCharArray() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        w.write("a".toCharArray());
+        assertEquals("a", sw.toString());
+        w.write("\nb\n".toCharArray());
+        assertEquals("a\nb", sw.toString());
+        w.write("\nc".toCharArray());
+        assertEquals("a\nb\n\nc", sw.toString());
+        w.write("def\n\nghi\n\n".toCharArray());
+        assertEquals("a\nb\n\ncdef\n\nghi\n", sw.toString());
+    }
+
+    @Test
+    public void testSubstringWrites() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        String s = "0123\n567\n\nA";
+        w.write(s, 1, 3);
+        assertEquals("123", sw.toString());
+        w.write(s, 4, 1);
+        assertEquals("123", sw.toString());
+        w.write(s, 5, 2);
+        assertEquals("123\n56", sw.toString());
+        w.write(s, 7, 3);
+        assertEquals("123\n567\n", sw.toString());
+        w.write(s, 10, 1);
+        assertEquals("123\n567\n\nA", sw.toString());
+    }
+
+    @Test
+    public void testSubArrayWrites() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new ChopLinebreakWriter(sw);
+
+        String s = "0123\n567\n\nA";
+        w.write(s.toCharArray(), 1, 3);
+        assertEquals("123", sw.toString());
+        w.write(s.toCharArray(), 4, 1);
+        assertEquals("123", sw.toString());
+        w.write(s.toCharArray(), 5, 2);
+        assertEquals("123\n56", sw.toString());
+        w.write(s.toCharArray(), 7, 3);
+        assertEquals("123\n567\n", sw.toString());
+        w.write(s.toCharArray(), 10, 1);
+        assertEquals("123\n567\n\nA", sw.toString());
+    }
+
+}
\ No newline at end of file
diff --git 
a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/EscapeHtmlAndXmlWriterTest.java
 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/EscapeHtmlAndXmlWriterTest.java
new file mode 100644
index 0000000..555888f
--- /dev/null
+++ 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/EscapeHtmlAndXmlWriterTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.junit.jupiter.api.Test;
+
+class EscapeHtmlAndXmlWriterTest {
+
+    @Test
+    public void testCharArg1() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new EscapeHtmlAndXmlWriter(sw);
+
+        w.write('a');
+        w.write('<');
+        w.write('>');
+        w.write('b');
+        w.write('&');
+        assertEquals("a&lt;&gt;b&amp;", sw.toString());
+    }
+
+    @Test
+    public void testStringArg1() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new EscapeHtmlAndXmlWriter(sw);
+
+        w.write("");
+        w.write("a");
+        w.write("b<cd>ef&g");
+        assertEquals("ab&lt;cd&gt;ef&amp;g", sw.toString());
+        w.write("<>");
+        assertEquals("ab&lt;cd&gt;ef&amp;g&lt;&gt;", sw.toString());
+        w.write("<");
+        assertEquals("ab&lt;cd&gt;ef&amp;g&lt;&gt;&lt;", sw.toString());
+    }
+
+    @Test
+    public void testArrayArg1() throws IOException {
+        StringWriter sw = new StringWriter();
+        Writer w = new EscapeHtmlAndXmlWriter(sw);
+
+        w.write("".toCharArray());
+        w.write("a".toCharArray());
+        w.write("b<cd>ef&g".toCharArray());
+        assertEquals("ab&lt;cd&gt;ef&amp;g", sw.toString());
+        w.write("<>".toCharArray());
+        assertEquals("ab&lt;cd&gt;ef&amp;g&lt;&gt;", sw.toString());
+        w.write("<".toCharArray());
+        assertEquals("ab&lt;cd&gt;ef&amp;g&lt;&gt;&lt;", sw.toString());
+    }
+
+}
\ No newline at end of file
diff --git 
a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
index a0eab82..c399ea5 100644
--- 
a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
+++ 
b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
@@ -28,5 +28,7 @@ public class SettingNameTest {
     public void toStringTest() {
         assertEquals("a", SettingName.topLevel(null, "a").toString());
         assertEquals("a.b[1]", SettingName.topLevel(null, 
"a").subKey("b").subKey(1).toString());
+        assertEquals("a.b[1].c.d", SettingName.topLevel(null, "a").subKey("b", 
1, "c", "d").toString());
+        assertEquals("a[\"a b\"].b1", SettingName.topLevel(null, 
"a").subKey("a b").subKey("b1").toString());
     }
 }

Reply via email to