You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by ul...@apache.org on 2013/02/27 16:22:05 UTC

git commit: TAP5-2071: Tapestry should output valid HTML5 when using the HTML5 doctype in templates

Updated Branches:
  refs/heads/master f10fc6bd2 -> 1fe4fa73c


TAP5-2071: Tapestry should output valid HTML5 when using the HTML5
doctype in templates 

Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/1fe4fa73
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/1fe4fa73
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/1fe4fa73

Branch: refs/heads/master
Commit: 1fe4fa73c4f59b99ce011a9f2119a97728dd77a0
Parents: f10fc6b
Author: Ulrich Staerk <ul...@apache.org>
Authored: Wed Feb 27 16:21:45 2013 +0100
Committer: Ulrich Staerk <ul...@apache.org>
Committed: Wed Feb 27 16:21:45 2013 +0100

----------------------------------------------------------------------
 .../java/org/apache/tapestry5/dom/EndTagStyle.java |    9 +-
 .../org/apache/tapestry5/dom/Html5MarkupModel.java |   51 ++++++++
 .../tapestry5/internal/InternalConstants.java      |    7 +
 .../services/AjaxComponentEventRequestHandler.java |   25 ++--
 .../services/AjaxPartialResponseRendererImpl.java  |    6 +-
 .../internal/services/MarkupWriterFactoryImpl.java |   97 ++++++++++++---
 .../services/PageResponseRendererImpl.java         |    9 +-
 .../tapestry5/services/MarkupWriterFactory.java    |   50 +++++++-
 .../java/org/apache/tapestry5/dom/DOMTest.java     |   36 +++++-
 9 files changed, 243 insertions(+), 47 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/dom/EndTagStyle.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/dom/EndTagStyle.java b/tapestry-core/src/main/java/org/apache/tapestry5/dom/EndTagStyle.java
index 7d94233..bb41483 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/dom/EndTagStyle.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/dom/EndTagStyle.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2008, 2011 The Apache Software Foundation
+// Copyright 2006, 2008, 2011, 2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -29,5 +29,10 @@ public enum EndTagStyle
      * in XML documents, but {@link org.apache.tapestry5.dom.DefaultMarkupModel} forces most tags to use {@link
      * #REQUIRE} for semi-obscure browser compatibility issues.
      */
-    ABBREVIATE
+    ABBREVIATE,
+    
+    /**
+     * No end tags for HTML5 compatibility.
+     */
+    VOID
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/dom/Html5MarkupModel.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/dom/Html5MarkupModel.java b/tapestry-core/src/main/java/org/apache/tapestry5/dom/Html5MarkupModel.java
new file mode 100644
index 0000000..e423c9a
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/dom/Html5MarkupModel.java
@@ -0,0 +1,51 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed 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.apache.tapestry5.dom;
+
+import java.util.Set;
+
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+
+/**
+ * Implementation of {@link org.apache.tapestry5.dom.MarkupModel} that correctly handles HTML5 void
+ * elements. It does not support XHTML5.
+ */
+public class Html5MarkupModel extends AbstractMarkupModel
+{
+    // http://www.w3.org/TR/html5/syntax.html#void-elements
+    private final Set<String> VOID_ELEMENTS = CollectionFactory.newSet("area", "base", "br", "col",
+            "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source",
+            "track", "wbr");
+    
+    public Html5MarkupModel()
+    {
+        super(false);
+    }
+
+    public Html5MarkupModel(boolean useApostropheForAttributes)
+    {
+        super(useApostropheForAttributes);
+    }
+
+    public EndTagStyle getEndTagStyle(String element)
+    {
+        return VOID_ELEMENTS.contains(element) ? EndTagStyle.VOID : EndTagStyle.REQUIRE;
+    }
+
+    public boolean isXML()
+    {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/internal/InternalConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/InternalConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/InternalConstants.java
index 6bf8f1b..2010a83 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/InternalConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/InternalConstants.java
@@ -14,6 +14,7 @@
 
 package org.apache.tapestry5.internal;
 
+import org.apache.tapestry5.dom.MarkupModel;
 import org.apache.tapestry5.ioc.util.TimeInterval;
 import org.apache.tapestry5.services.javascript.JavaScriptStack;
 
@@ -69,6 +70,12 @@ public final class InternalConstants
     public static final String CONTENT_TYPE_ATTRIBUTE_NAME = "content-type";
 
     public static final String CHARSET_CONTENT_TYPE_PARAMETER = "charset";
+    
+    /**
+     * As above but to store the name of the page. Necessary for determining the correct
+     * {@link MarkupModel} for the response.
+     */
+    public static final String PAGE_NAME_ATTRIBUTE_NAME = "page-name";
 
     /**
      * Required MIME type for JSON responses. If this MIME type is not used, the client-side

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxComponentEventRequestHandler.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxComponentEventRequestHandler.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxComponentEventRequestHandler.java
index 028f3b5..c4a852e 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxComponentEventRequestHandler.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxComponentEventRequestHandler.java
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008, 2010, 2011, 2012 The Apache Software Foundation
+// Copyright 2007-2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,7 +14,8 @@
 
 package org.apache.tapestry5.internal.services;
 
-import org.apache.tapestry5.ContentType;
+import java.io.IOException;
+
 import org.apache.tapestry5.TrackableComponentEventCallback;
 import org.apache.tapestry5.internal.InternalConstants;
 import org.apache.tapestry5.internal.structure.ComponentPageElement;
@@ -22,15 +23,18 @@ import org.apache.tapestry5.internal.structure.Page;
 import org.apache.tapestry5.internal.util.Holder;
 import org.apache.tapestry5.ioc.internal.util.TapestryException;
 import org.apache.tapestry5.json.JSONObject;
-import org.apache.tapestry5.services.*;
-
-import java.io.IOException;
+import org.apache.tapestry5.services.Ajax;
+import org.apache.tapestry5.services.ComponentEventRequestHandler;
+import org.apache.tapestry5.services.ComponentEventRequestParameters;
+import org.apache.tapestry5.services.ComponentEventResultProcessor;
+import org.apache.tapestry5.services.Environment;
+import org.apache.tapestry5.services.Request;
 
 /**
  * Similar to {@link ComponentEventRequestHandlerImpl}, but built around the Ajax request cycle, where the action
  * request sends back an immediate JSON response containing the new content.
  */
-@SuppressWarnings("unchecked")
+@SuppressWarnings({"unchecked", "rawtypes"})
 public class AjaxComponentEventRequestHandler implements ComponentEventRequestHandler
 {
     private final RequestPageCache cache;
@@ -41,8 +45,6 @@ public class AjaxComponentEventRequestHandler implements ComponentEventRequestHa
 
     private final ComponentEventResultProcessor resultProcessor;
 
-    private final PageContentTypeAnalyzer pageContentTypeAnalyzer;
-
     private final Environment environment;
 
     private final AjaxPartialResponseRenderer partialRenderer;
@@ -51,14 +53,13 @@ public class AjaxComponentEventRequestHandler implements ComponentEventRequestHa
 
     public AjaxComponentEventRequestHandler(RequestPageCache cache, Request request, PageRenderQueue queue, @Ajax
     ComponentEventResultProcessor resultProcessor, PageActivator pageActivator,
-                                            PageContentTypeAnalyzer pageContentTypeAnalyzer, Environment environment,
+                                            Environment environment,
                                             AjaxPartialResponseRenderer partialRenderer)
     {
         this.cache = cache;
         this.queue = queue;
         this.resultProcessor = resultProcessor;
         this.pageActivator = pageActivator;
-        this.pageContentTypeAnalyzer = pageContentTypeAnalyzer;
         this.request = request;
         this.environment = environment;
         this.partialRenderer = partialRenderer;
@@ -90,9 +91,7 @@ public class AjaxComponentEventRequestHandler implements ComponentEventRequestHa
                 .getPageActivationContext(), interceptor))
             return;
 
-        ContentType contentType = pageContentTypeAnalyzer.findContentType(activePage);
-
-        request.setAttribute(InternalConstants.CONTENT_TYPE_ATTRIBUTE_NAME, contentType);
+        request.setAttribute(InternalConstants.PAGE_NAME_ATTRIBUTE_NAME, parameters.getActivePageName());
 
         Page containerPage = cache.get(parameters.getContainingPageName());
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxPartialResponseRendererImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxPartialResponseRendererImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxPartialResponseRendererImpl.java
index d5f3d6a..aa69acf 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxPartialResponseRendererImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AjaxPartialResponseRendererImpl.java
@@ -78,11 +78,11 @@ public class AjaxPartialResponseRendererImpl implements AjaxPartialResponseRende
             // separated, and trying to keep stateless and stateful (i.e., perthread scope) services
             // separated. So we inform the stateful queue service what it needs to do here ...
 
-            ContentType pageContentType = (ContentType) request.getAttribute(InternalConstants.CONTENT_TYPE_ATTRIBUTE_NAME);
-
             ContentType contentType = new ContentType(InternalConstants.JSON_MIME_TYPE, outputEncoding);
+            
+            String pageName = (String) request.getAttribute(InternalConstants.PAGE_NAME_ATTRIBUTE_NAME);
 
-            MarkupWriter writer = factory.newPartialMarkupWriter(pageContentType);
+            MarkupWriter writer = factory.newPartialMarkupWriter(pageName);
 
             // ... and here, the pipeline eventually reaches the PRQ to let it render the root render command.
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MarkupWriterFactoryImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MarkupWriterFactoryImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MarkupWriterFactoryImpl.java
index 8dc6a51..48edfe0 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MarkupWriterFactoryImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MarkupWriterFactoryImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008, 2009 The Apache Software Foundation
+// Copyright 2007, 2008, 2009, 2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,19 +14,32 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.util.List;
+
 import org.apache.tapestry5.ContentType;
 import org.apache.tapestry5.MarkupWriter;
 import org.apache.tapestry5.dom.DefaultMarkupModel;
+import org.apache.tapestry5.dom.Html5MarkupModel;
 import org.apache.tapestry5.dom.MarkupModel;
 import org.apache.tapestry5.dom.XMLMarkupModel;
+import org.apache.tapestry5.internal.parser.DTDToken;
+import org.apache.tapestry5.internal.parser.TemplateToken;
+import org.apache.tapestry5.internal.parser.TokenType;
 import org.apache.tapestry5.internal.structure.Page;
+import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.services.MarkupWriterFactory;
+import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
+import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
 
 public class MarkupWriterFactoryImpl implements MarkupWriterFactory
 {
-    private final PageContentTypeAnalyzer analyzer;
+    private final PageContentTypeAnalyzer pageContentTypeAnalyzer;
 
     private final RequestPageCache cache;
+    
+    private final ComponentTemplateSource templateSource;
+    
+    private final ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer;
 
     private final MarkupModel htmlModel = new DefaultMarkupModel();
 
@@ -35,32 +48,41 @@ public class MarkupWriterFactoryImpl implements MarkupWriterFactory
     private final MarkupModel htmlPartialModel = new DefaultMarkupModel(true);
 
     private final MarkupModel xmlPartialModel = new XMLMarkupModel(true);
-
-    public MarkupWriterFactoryImpl(PageContentTypeAnalyzer analyzer, RequestPageCache cache)
+    
+    private final MarkupModel html5Model = new Html5MarkupModel();
+    
+    private final MarkupModel html5PartialModel = new Html5MarkupModel(true);
+
+    public MarkupWriterFactoryImpl(PageContentTypeAnalyzer pageContentTypeAnalyzer,
+            RequestPageCache cache, ComponentTemplateSource templateSource,
+            ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer)
     {
-        this.analyzer = analyzer;
+        this.pageContentTypeAnalyzer = pageContentTypeAnalyzer;
         this.cache = cache;
+        this.templateSource = templateSource;
+        this.componentRequestSelectorAnalyzer = componentRequestSelectorAnalyzer;
     }
 
     public MarkupWriter newMarkupWriter(ContentType contentType)
     {
-        return newMarkupWriter(contentType, false);
+        return constructMarkupWriter(contentType, false, false);
     }
 
     public MarkupWriter newPartialMarkupWriter(ContentType contentType)
     {
-        return newMarkupWriter(contentType, true);
+        return constructMarkupWriter(contentType, true, false);
     }
 
-    @SuppressWarnings({"UnusedDeclaration"})
-    private MarkupWriter newMarkupWriter(ContentType contentType, boolean partial)
+    private MarkupWriter constructMarkupWriter(ContentType contentType, boolean partial, boolean HTML5)
     {
         boolean isHTML = contentType.getMimeType().equalsIgnoreCase("text/html");
 
-        MarkupModel model = partial
-                            ? (isHTML ? htmlPartialModel : xmlPartialModel)
-                            : (isHTML ? htmlModel : xmlModel);
-
+        MarkupModel model;
+        
+        if(isHTML)
+            model = HTML5 ? (partial ? html5PartialModel : html5Model) : (partial ? htmlPartialModel : htmlModel);
+        else
+            model = partial ? xmlPartialModel : xmlModel;
         // The charset parameter sets the encoding attribute of the XML declaration, if
         // not null and if using the XML model.
 
@@ -71,8 +93,53 @@ public class MarkupWriterFactoryImpl implements MarkupWriterFactory
     {
         Page page = cache.get(pageName);
 
-        ContentType contentType = analyzer.findContentType(page);
+        return newMarkupWriter(page);
+    }
+    
+    private boolean hasHTML5Doctype(Page page)
+    {
+        ComponentModel componentModel = page.getRootComponent().getComponentResources().getComponentModel();
+        
+        ComponentResourceSelector selector = componentRequestSelectorAnalyzer.buildSelectorForRequest();
+        
+        List<TemplateToken> tokens = templateSource.getTemplate(componentModel, selector).getTokens();
+        
+        DTDToken dtd = null;
+        
+        for(TemplateToken token : tokens)
+        {
+            if(token.getTokenType() == TokenType.DTD)
+            {
+                dtd = (DTDToken) token;
+                break;
+            }
+        }
+        
+        return dtd != null && dtd.name.equalsIgnoreCase("html") && dtd.publicId == null && dtd.systemId == null;
+    }
+
+    public MarkupWriter newMarkupWriter(Page page)
+    {
+        boolean isHTML5 = hasHTML5Doctype(page);
+        
+        ContentType contentType = pageContentTypeAnalyzer.findContentType(page);
+        
+        return constructMarkupWriter(contentType, false, isHTML5);
+    }
 
-        return newMarkupWriter(contentType);
+    public MarkupWriter newPartialMarkupWriter(Page page)
+    {
+        boolean isHTML5 = hasHTML5Doctype(page);
+        
+        ContentType contentType = pageContentTypeAnalyzer.findContentType(page);
+        
+        return constructMarkupWriter(contentType, true, isHTML5);
+    }
+
+    public MarkupWriter newPartialMarkupWriter(String pageName)
+    {
+        Page page = cache.get(pageName);
+        
+        return newPartialMarkupWriter(page);
     }
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageResponseRendererImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageResponseRendererImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageResponseRendererImpl.java
index 093c467..4231e20 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageResponseRendererImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageResponseRendererImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2007, 2008, 2009, 2010 The Apache Software Foundation
+// Copyright 2006-2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -58,11 +58,8 @@ public class PageResponseRendererImpl implements PageResponseRenderer
         requestGlobals.storeActivePageName(page.getName());
 
         ContentType contentType = pageContentTypeAnalyzer.findContentType(page);
-
-        // For the moment, the content type is all that's used determine the model for the markup writer.
-        // It's something of a can of worms.
-
-        MarkupWriter writer = markupWriterFactory.newMarkupWriter(contentType);
+        
+        MarkupWriter writer = markupWriterFactory.newMarkupWriter(page);
 
         markupRenderer.renderPageMarkup(page, writer);
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/main/java/org/apache/tapestry5/services/MarkupWriterFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/MarkupWriterFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/MarkupWriterFactory.java
index 2445456..b00c338 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/MarkupWriterFactory.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/MarkupWriterFactory.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2007, 2008 The Apache Software Foundation
+// Copyright 2006-2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@ package org.apache.tapestry5.services;
 
 import org.apache.tapestry5.ContentType;
 import org.apache.tapestry5.MarkupWriter;
+import org.apache.tapestry5.internal.structure.Page;
 
 /**
  * Source for {@link org.apache.tapestry5.MarkupWriter} instances.
@@ -28,6 +29,8 @@ public interface MarkupWriterFactory
      * @param contentType type of content generated by the markup write; used to control the type of {@link
      *                    org.apache.tapestry5.dom.MarkupModel} used with the {@link org.apache.tapestry5.dom.Document}
      *                    the backs the markup writer.
+     *                    
+     * @deprecated use {@link #newMarkupWriter(Page)} instead which doesn't rely on the content type alone.
      */
     MarkupWriter newMarkupWriter(ContentType contentType);
 
@@ -38,14 +41,55 @@ public interface MarkupWriterFactory
      * @param contentType type of content generated by the markup write; used to control the type of {@link
      *                    org.apache.tapestry5.dom.MarkupModel} used with the {@link org.apache.tapestry5.dom.Document}
      *                    the backs the markup writer.
+     *
+     * @deprecated use {@link #newPartialMarkupWriter(Page)} instead which doesn't rely on the content type alone.
      */
     MarkupWriter newPartialMarkupWriter(ContentType contentType);
 
     /**
      * Obtains a markup writer that will render the content for the provided logical page name.
-     *
-     * @param pageName logical page name
+     * Convenience method for {@link #newMarkupWriter(Page)}
+     * 
+     * @param pageName
+     *            logical page name
      * @return writer configured for the page
      */
     MarkupWriter newMarkupWriter(String pageName);
+    
+    /**
+     * Obtains a markup writer that will render the content for the provided logical page name,
+     * configured for <em>partial page rendering</em> (i.e., for a response to an Ajax request).
+     * Convenience method for {@link #newPartialMarkupWriter(Page)}
+     * 
+     * @param pageName
+     *            logical page name
+     * @return writer configured for the page
+     * 
+     * @since 5.4
+     */
+    MarkupWriter newPartialMarkupWriter(String pageName);
+    
+    /**
+     * Obtains a markup writer that will render the content for the provided page. Takes into
+     * account all necessary information such as the page's content type and doctype.
+     * 
+     * @param page the page to obtain a writer for
+     * @return writer configured for the page
+     * 
+     * @since 5.4
+     */
+    MarkupWriter newMarkupWriter(Page page);
+    
+    /**
+     * Obtains a markup writer that will render the content for the provided page,
+     * configured for <em>partial page rendering</em> (i.e., for a response to an Ajax request).
+     * Takes into account all necessary information such as the page's content type and doctype.
+     * 
+     * @param page
+     *            the page to obtain a writer for
+     * @return writer configured for the page
+     * 
+     * @since 5.4
+     */
+    MarkupWriter newPartialMarkupWriter(Page page);
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/1fe4fa73/tapestry-core/src/test/java/org/apache/tapestry5/dom/DOMTest.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/dom/DOMTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/dom/DOMTest.java
index 5a19530..4f152e6 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/dom/DOMTest.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/dom/DOMTest.java
@@ -14,17 +14,17 @@
 
 package org.apache.tapestry5.dom;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
 import org.apache.tapestry5.MarkupWriter;
 import org.apache.tapestry5.internal.services.MarkupWriterImpl;
 import org.apache.tapestry5.internal.test.InternalBaseTestCase;
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.testng.annotations.Test;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
 /**
  * Tests for a number of DOM node classes, including {@link org.apache.tapestry5.dom.Element} and {@link
  * org.apache.tapestry5.dom.Document}.
@@ -951,4 +951,30 @@ public class DOMTest extends InternalBaseTestCase
         assertEquals(writer.toString(), "<?xml version=\"1.0\"?>\n" +
                 "<ul><li>0</li><li>1</li><li>3</li></ul>");
     }
+    
+    /**
+     * TAP5-2071
+     */
+    @Test
+    public void html5_void_elements()
+    {
+        final List<String> voidElements = CollectionFactory.newList("area", "base", "br", "col",
+                "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
+                "source", "track", "wbr");
+        
+        MarkupWriter writer = new MarkupWriterImpl(new Html5MarkupModel());
+        
+        writer.element("html");
+        
+        for(String element : voidElements)
+        {
+            writer.element(element);
+            writer.end();
+        }
+        
+        writer.end();
+        
+        assertEquals(writer.toString(),
+                "<html><area><base><br><col><command><embed><hr><img><input><keygen><link><meta><param><source><track><wbr></html>");
+    }
 }