Merged master into issue/SLING-7681

* brought in fixes for SLING-7710, SLING-7549
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParser.java b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParser.java
index 877e8c4..6fce2b3 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParser.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParser.java
@@ -21,6 +21,10 @@
 import java.io.CharArrayWriter;
 import java.io.IOException;
 import java.io.Reader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * HTML parser. Invokes a <code>DocumentHandler</code> whenever an event occurs.
@@ -68,6 +72,9 @@
     /** Expression state constant */
     private final static int EXPR_MAYBE = 1;
 
+    final static Set<String> VOID_ELEMENTS = Collections.unmodifiableSet(new HashSet<>(
+            Arrays.asList("area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr")));
+
     /** Parse state */
     private PARSE_STATE parseState = PARSE_STATE.OUTSIDE;
 
@@ -479,9 +486,8 @@
 
         tokenizer.tokenize(snippet, 0, snippet.length);
         if (!tokenizer.endTag()) {
-            documentHandler.onStartElement(tokenizer.tagName(), tokenizer
-                    .attributes(), tokenizer
-                    .endSlash());
+            documentHandler.onStartElement(tokenizer.tagName(), tokenizer.attributes(),
+                    tokenizer.endSlash() || VOID_ELEMENTS.contains(tokenizer.tagName().toLowerCase()));
         } else {
             documentHandler.onEndElement(tokenizer.tagName());
         }
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/MarkupHandler.java b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/MarkupHandler.java
index 58c6ccb..0e41724 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/MarkupHandler.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/MarkupHandler.java
@@ -32,6 +32,7 @@
 import org.apache.sling.scripting.sightly.compiler.commands.VariableBinding;
 import org.apache.sling.scripting.sightly.compiler.expression.Expression;
 import org.apache.sling.scripting.sightly.compiler.expression.ExpressionNode;
+import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext;
 import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperation;
 import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperator;
 import org.apache.sling.scripting.sightly.compiler.expression.nodes.BooleanConstant;
@@ -51,7 +52,6 @@
 import org.apache.sling.scripting.sightly.impl.filter.ExpressionContext;
 import org.apache.sling.scripting.sightly.impl.filter.Filter;
 import org.apache.sling.scripting.sightly.impl.html.MarkupUtils;
-import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext;
 import org.apache.sling.scripting.sightly.impl.plugin.Plugin;
 import org.apache.sling.scripting.sightly.impl.plugin.PluginCallInfo;
 import org.apache.sling.scripting.sightly.impl.plugin.PluginInvoke;
@@ -85,7 +85,15 @@
     public void onAttribute(String name, String value, char quoteChar) {
         ElementContext context = elementStack.peek();
         if (Syntax.isPluginAttribute(name)) {
-            handlePlugin(name, StringUtils.defaultString(value, ""), context);
+            try {
+                handlePlugin(name, StringUtils.defaultString(value, ""), context);
+            } catch (SightlyCompilerException e) {
+                if (StringUtils.isEmpty(e.getOffendingInput())) {
+                    throw new SightlyCompilerException(e.getMessage(),
+                            name + (StringUtils.isNotEmpty(value) ? "=" + quoteChar + value + quoteChar : ""));
+                }
+                throw e;
+            }
         } else {
             context.addAttribute(name, value, quoteChar);
         }
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/TreeTraverser.java b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/TreeTraverser.java
index 30e2701..43611d7 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/TreeTraverser.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/TreeTraverser.java
@@ -57,12 +57,12 @@
         }
         String tagName = elem.getName();
 
-        if (elem.isHasStartElement()) {
+        if (elem.isStartElement()) {
             handler.onOpenTagStart("<" + tagName, tagName);
             for (TemplateAttribute attribute : elem.getAttributes()) {
                 handler.onAttribute(attribute.getName(), attribute.getValue(), attribute.getQuoteChar());
             }
-            if (elem.isHasEndSlash()) {
+            if (elem.hasEndSlash()) {
                 handler.onOpenTagEnd("/>");
             } else {
                 handler.onOpenTagEnd(">");
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/template/TemplateElementNode.java b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/template/TemplateElementNode.java
index 266a748..d6b04be 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/template/TemplateElementNode.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/html/dom/template/TemplateElementNode.java
@@ -55,11 +55,11 @@
         return name;
     }
 
-    public boolean isHasEndSlash() {
+    public boolean hasEndSlash() {
         return hasEndSlash;
     }
 
-    public boolean isHasStartElement() {
+    public boolean isStartElement() {
         return hasStartElement;
     }
 
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/CallPlugin.java b/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/CallPlugin.java
index 455612e..b666575 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/CallPlugin.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/CallPlugin.java
@@ -39,7 +39,7 @@
                                final PluginCallInfo callInfo,
                                final CompilerContext compilerContext) {
         if (callInfo.getArguments().length > 0) {
-            throw new SightlyCompilerException("Call plugin should have no arguments.", "data-sly-call." + callInfo.getArguments()[0]);
+            throw new SightlyCompilerException("Call plugin should have no arguments.");
         }
         return new DefaultPluginInvoke() {
 
diff --git a/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/TemplatePlugin.java b/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/TemplatePlugin.java
index 2890c19..a22116f 100644
--- a/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/TemplatePlugin.java
+++ b/src/main/java/org/apache/sling/scripting/sightly/impl/plugin/TemplatePlugin.java
@@ -41,6 +41,8 @@
 
     @Override
     public PluginInvoke invoke(final Expression expressionNode, final PluginCallInfo callInfo, CompilerContext compilerContext) {
+        final String templateName = decodeName(callInfo);
+
         return new DefaultPluginInvoke() {
 
             @Override
@@ -51,9 +53,8 @@
 
             @Override
             public void beforeElement(PushStream stream, String tagName) {
-                String name = decodeName();
                 Set<String> parameters = extractParameters();
-                stream.write(new Procedure.Start(name, parameters));
+                stream.write(new Procedure.Start(templateName, parameters));
             }
 
             @Override
@@ -81,13 +82,14 @@
                 return options.keySet();
             }
 
-            private String decodeName() {
-                String[] arguments = callInfo.getArguments();
-                if (arguments.length == 0) {
-                    throw new SightlyCompilerException("Template name was not provided.", "data-sly-template=");
-                }
-                return arguments[0];
-            }
         };
     }
+
+    private String decodeName(PluginCallInfo callInfo) {
+        String[] arguments = callInfo.getArguments();
+        if (arguments.length == 0) {
+            throw new SightlyCompilerException("Template name was not provided.");
+        }
+        return arguments[0];
+    }
 }
diff --git a/src/test/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParserTest.java b/src/test/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParserTest.java
index c849cf7..c1ea70b 100644
--- a/src/test/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParserTest.java
+++ b/src/test/java/org/apache/sling/scripting/sightly/impl/html/dom/HtmlParserTest.java
@@ -18,20 +18,33 @@
  ******************************************************************************/
 package org.apache.sling.scripting.sightly.impl.html.dom;
 
+import java.io.IOException;
 import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.sling.scripting.sightly.impl.html.dom.template.*;
-
+import org.apache.sling.scripting.sightly.impl.html.dom.template.Template;
+import org.apache.sling.scripting.sightly.impl.html.dom.template.TemplateCommentNode;
+import org.apache.sling.scripting.sightly.impl.html.dom.template.TemplateElementNode;
+import org.apache.sling.scripting.sightly.impl.html.dom.template.TemplateNode;
+import org.apache.sling.scripting.sightly.impl.html.dom.template.TemplateParser;
+import org.apache.sling.scripting.sightly.impl.html.dom.template.TemplateTextNode;
 import org.junit.Test;
 import org.powermock.reflect.Whitebox;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 public class HtmlParserTest {
 
+    private static final String DATA_TEST_ATTRIBUTE = "data-test-attribute";
+
     /**
      * Pretty print a template nodes structure for debugging of failed tests
      *
@@ -79,7 +92,7 @@
      */
     private static void assertSameStructure(TemplateNode reference, TemplateNode parsed) {
         if (parsed == null || reference == null) {
-            assertTrue("Expecting both null", parsed == reference);
+            assertSame("Expecting both null", parsed, reference);
             return;
         }
         assertEquals("Expecting same class", reference.getClass(), parsed.getClass());
@@ -113,7 +126,7 @@
         }
 
         if (parsedChildren == null || referenceChildren == null) {
-            assertTrue("Expecting both children null", parsedChildren == referenceChildren);
+            assertSame("Expecting both children null", parsedChildren, referenceChildren);
             return;
         }
 
@@ -130,7 +143,7 @@
      * Create a basic template nodes structure containing one text nodes and one comment nodes
      *
      * @param textAndComment - String containing text (optional) and comment
-     * @return
+     * @return a reference template
      */
     private Template createReference(String textAndComment) {
         int commentIx = textAndComment.indexOf("<!");
@@ -185,4 +198,123 @@
             throw e;
         }
     }
+
+    @Test
+    public void testVoidElements() throws IOException {
+        VoidAndSelfClosingElementsDocumentHandler voidAndSelfClosingElementsDocumentHandler = new VoidAndSelfClosingElementsDocumentHandler();
+        for (String tag : HtmlParser.VOID_ELEMENTS) {
+            StringReader stringReader = new StringReader("<" + tag + " " + DATA_TEST_ATTRIBUTE + ">");
+            HtmlParser.parse(stringReader, voidAndSelfClosingElementsDocumentHandler);
+            assertEquals("Parsed tag is not what was expected.", tag, voidAndSelfClosingElementsDocumentHandler.getLastProcessedTag());
+        }
+        assertEquals("Expected 0 invocations for 'onEndElement'", 0, voidAndSelfClosingElementsDocumentHandler.onEndElementInvocations);
+        assertEquals("Expected as many invocations for 'onStartElement' as many void elements.", HtmlParser.VOID_ELEMENTS.size(),
+                voidAndSelfClosingElementsDocumentHandler.onStartElementInvocations);
+
+        HtmlParser.parse(new StringReader("<img " + DATA_TEST_ATTRIBUTE + "/>"), voidAndSelfClosingElementsDocumentHandler);
+        assertEquals("Expected 0 invocations for 'onEndElement'", 0, voidAndSelfClosingElementsDocumentHandler.onEndElementInvocations);
+        assertEquals("Expected as many invocations for 'onStartElement' as many void elements + 1.", HtmlParser.VOID_ELEMENTS.size() + 1,
+                voidAndSelfClosingElementsDocumentHandler.onStartElementInvocations);
+    }
+
+    @Test
+    public void testExplicitlyClosedElements() throws IOException {
+        TestDocumentHandler handler = new TestDocumentHandler();
+        Set<String> tags = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "p", "div")));
+        for (String tag : tags) {
+            StringReader stringReader = new StringReader("<" + tag + " " + DATA_TEST_ATTRIBUTE + "></" + tag + ">");
+            HtmlParser.parse(stringReader, handler);
+            assertEquals("Parsed tag is not what was expected.", tag, handler.getLastProcessedTag());
+        }
+        assertEquals("Expected as many invocations for 'onEndElement' as many test elements.", tags.size(),
+                handler.onEndElementInvocations);
+        assertEquals("Expected as many invocations for 'onStartElement' as many test elements.", tags.size(),
+                handler.onStartElementInvocations);
+    }
+
+    abstract class AbstractDocumentHandler implements DocumentHandler {
+
+        private String lastProcessedTag;
+
+        @Override
+        public void onCharacters(char[] ch, int off, int len) {
+
+        }
+
+        @Override
+        public void onComment(String characters) {
+
+        }
+
+        @Override
+        public void onStartElement(String name, AttributeList attList, boolean endSlash) {
+            lastProcessedTag = name;
+        }
+
+        @Override
+        public void onEndElement(String name) {
+
+        }
+
+        @Override
+        public void onStart() {
+
+        }
+
+        @Override
+        public void onEnd() {
+
+        }
+
+        String getLastProcessedTag() {
+            return lastProcessedTag;
+        }
+    }
+
+    class VoidAndSelfClosingElementsDocumentHandler extends AbstractDocumentHandler {
+
+        int onEndElementInvocations = 0;
+        int onStartElementInvocations = 0;
+
+        @Override
+        public void onStartElement(String name, AttributeList attList, boolean endSlash) {
+            super.onStartElement(name, attList, endSlash);
+            assertTrue("Expected a " + DATA_TEST_ATTRIBUTE + ".",
+                    attList.attributeCount() == 1 && attList.containsAttribute(DATA_TEST_ATTRIBUTE) &&
+                            attList.getValue(DATA_TEST_ATTRIBUTE) == null);
+            assertTrue("Expected a self-closing attribute.", endSlash);
+            onStartElementInvocations++;
+        }
+
+        @Override
+        public void onEndElement(String name) {
+            super.onEndElement(name);
+            onEndElementInvocations++;
+        }
+    }
+
+    class TestDocumentHandler extends AbstractDocumentHandler {
+        int onEndElementInvocations = 0;
+        int onStartElementInvocations = 0;
+
+        @Override
+        public void onStartElement(String name, AttributeList attList, boolean endSlash) {
+            super.onStartElement(name, attList, endSlash);
+            assertTrue("Expected a " + DATA_TEST_ATTRIBUTE + ".",
+                    attList.attributeCount() == 1 && attList.containsAttribute(DATA_TEST_ATTRIBUTE) &&
+                            attList.getValue(DATA_TEST_ATTRIBUTE) == null);
+            assertFalse("Did not expect a self-closing attribute.", endSlash);
+            onStartElementInvocations++;
+        }
+
+        @Override
+        public void onEndElement(String name) {
+            super.onEndElement(name);
+            onEndElementInvocations++;
+        }
+    }
+
+
+
+
 }