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

papegaaij pushed a commit to branch WICKET-7196
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 39a7fd323e81b40ee91dc7db4693a54c0bdfaa83
Author: Emond Papegaaij <[email protected]>
AuthorDate: Sun Aug 23 21:48:04 2026 +0200

    WICKET-7196 Let a wicket:label tag turn off escaping
    
    The label text <wicket:label> takes from a model or from a resource bundle 
is
    escaped where it is written, and the flag that decides it is read from the
    TextLabel the resolver inserts. That component is never handed to the
    application, so its flag stays at the default and nothing can clear it. An
    application whose bundle holds markup, or an entity such as Save &amp; 
close,
    had no way back other than moving the content into the tag body.
    
    The tag now carries the decision. An escape attribute sets 
escapeModelStrings on
    the label the resolver creates, so the write site is unchanged and the 
attribute
    is the markup spelling of the flag the security model already describes: 
clearing
    it is the application saying the content is markup and taking 
responsibility for
    it.
    
    A missing attribute keeps the escaping, and so does an empty one. That 
differs
    from <wicket:message>, which reads the same attribute with 
IValueMap#getBoolean
    and resolves an empty value to false. There false is the default, here it 
is the
    opt-out, so an empty value has to keep the escaping rather than quietly 
drop it.
    An unrecognised value still fails, the way it does on <wicket:message>.
    
    The attribute says nothing about the tag body. That body is markup the 
label has
    just rendered itself, nested components and <wicket:message> included, and 
is
    written as is either way.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../AutoLabelEscapeAttributeTest$LabelPage.html    |  16 ++
 ...toLabelEscapeAttributeTest$LabelPage.properties |   2 +
 ...elEscapeAttributeTest$WrongEscapeValuePage.html |   8 +
 .../html/form/AutoLabelEscapeAttributeTest.java    | 182 +++++++++++++++++++++
 .../markup/html/form/AutoLabelTextResolver.java    |  27 ++-
 5 files changed, 234 insertions(+), 1 deletion(-)

diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html
new file mode 100644
index 0000000000..af9682f2f5
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html
@@ -0,0 +1,16 @@
+<html>
+<body>
+    <form wicket:id="form">
+        <label wicket:for="fromModel"><wicket:label escape="false"/></label>
+        <input type="text" wicket:id="fromModel"/>
+        <label wicket:for="fromDefaultLabel"><wicket:label 
escape="false"/></label>
+        <input type="text" wicket:id="fromDefaultLabel"/>
+        <label wicket:for="fromKey"><wicket:label key="markupKey" 
escape="false"/></label>
+        <input type="text" wicket:id="fromKey"/>
+        <label wicket:for="escapeTrue"><wicket:label escape="true"/></label>
+        <input type="text" wicket:id="escapeTrue"/>
+        <label wicket:for="escapeBlank"><wicket:label escape=""/></label>
+        <input type="text" wicket:id="escapeBlank"/>
+    </form>
+</body>
+</html>
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties
new file mode 100644
index 0000000000..168b79491f
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties
@@ -0,0 +1,2 @@
+fromDefaultLabel=<em>default</em>
+markupKey=<em>key</em>
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html
new file mode 100644
index 0000000000..42b20a27ca
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html
@@ -0,0 +1,8 @@
+<html>
+<body>
+    <form wicket:id="form">
+        <label wicket:for="field"><wicket:label escape="yesPlease"/></label>
+        <input type="text" wicket:id="field"/>
+    </form>
+</body>
+</html>
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java
new file mode 100644
index 0000000000..8400280aff
--- /dev/null
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.apache.wicket.markup.html.form;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.lang.Exceptions;
+import org.apache.wicket.util.string.StringValueConversionException;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the <code>escape</code> attribute of {@literal <wicket:label>}, which 
turns off the
+ * escaping of the label text the tag takes from a model or from a resource 
bundle.
+ * 
+ * @see AutoLabelEscapeMarkupTest for the escaping this attribute opts out of
+ */
+class AutoLabelEscapeAttributeTest extends WicketTestCase
+{
+       /** Label of the component whose tag says escape="false". */
+       private static final String FROM_MODEL = "<em>model</em>";
+
+       /** Bundle entry under the component's id, the default label, on a tag 
with escape="false". */
+       private static final String FROM_DEFAULT_LABEL = "<em>default</em>";
+
+       /** Bundle entry the key attribute names, on a tag with escape="false". 
*/
+       private static final String FROM_KEY = "<em>key</em>";
+
+       /** Label of the component whose tag says escape="true". */
+       private static final String ESCAPE_TRUE = "<em>escapeTrue</em>";
+
+       /** Label of the component whose tag says escape="", which is not a 
request to stop escaping. */
+       private static final String ESCAPE_BLANK = "<em>escapeBlank</em>";
+
+       public static class LabelPage extends WebPage
+       {
+               public LabelPage()
+               {
+                       Form<Void> form = new Form<>("form");
+                       add(form);
+                       form.add(new TextField<>("fromModel", 
Model.of("")).setLabel(Model.of(FROM_MODEL)));
+                       form.add(new TextField<>("fromDefaultLabel", 
Model.of("")));
+                       form.add(new TextField<>("fromKey", Model.of("")));
+                       form.add(new TextField<>("escapeTrue", 
Model.of("")).setLabel(Model.of(ESCAPE_TRUE)));
+                       form.add(new TextField<>("escapeBlank", 
Model.of("")).setLabel(Model.of(ESCAPE_BLANK)));
+               }
+       }
+
+       public static class WrongEscapeValuePage extends WebPage
+       {
+               public WrongEscapeValuePage()
+               {
+                       Form<Void> form = new Form<>("form");
+                       add(form);
+                       form.add(new TextField<>("field", 
Model.of("")).setLabel(Model.of("a label")));
+               }
+       }
+
+       /** The label model, the case {@link SimpleFormComponentLabel} renders 
escaped. */
+       @Test
+       void escapeFalseWritesTheModelLabelAsMarkup()
+       {
+               tester.startPage(new LabelPage());
+
+               String response = tester.getLastResponseAsString();
+               assertTrue(response.contains(FROM_MODEL),
+                       "label from the model should be written as markup");
+               assertFalse(response.contains("&lt;em&gt;model&lt;/em&gt;"),
+                       "label from the model should not be escaped");
+       }
+
+       /** The default label, looked up in the bundle by the component's id. */
+       @Test
+       void escapeFalseWritesTheDefaultLabelAsMarkup()
+       {
+               tester.startPage(new LabelPage());
+
+               String response = tester.getLastResponseAsString();
+               assertTrue(response.contains(FROM_DEFAULT_LABEL),
+                       "default label should be written as markup");
+               assertFalse(response.contains("&lt;em&gt;default&lt;/em&gt;"),
+                       "default label should not be escaped");
+       }
+
+       /** The message key on the tag, {@literal <wicket:label key="markupKey" 
escape="false"/>}. */
+       @Test
+       void escapeFalseWritesTheMessageKeyAsMarkup()
+       {
+               tester.startPage(new LabelPage());
+
+               String response = tester.getLastResponseAsString();
+               assertTrue(response.contains(FROM_KEY),
+                       "label from a message key should be written as markup");
+               assertFalse(response.contains("&lt;em&gt;key&lt;/em&gt;"),
+                       "label from a message key should not be escaped");
+       }
+
+       /** Spelling out the default changes nothing. */
+       @Test
+       void escapeTrueStillEscapes()
+       {
+               tester.startPage(new LabelPage());
+
+               String response = tester.getLastResponseAsString();
+               assertTrue(response.contains("&lt;em&gt;escapeTrue&lt;/em&gt;"),
+                       "escape=\"true\" should escape the label");
+               assertFalse(response.contains(ESCAPE_TRUE),
+                       "escape=\"true\" should not let the label reach the 
markup as markup");
+       }
+
+       /**
+        * An empty value is not a request to stop escaping. This is where the 
attribute deliberately
+        * differs from {@literal <wicket:message>}: that tag reads it with
+        * {@link org.apache.wicket.util.value.IValueMap#getBoolean(String)}, 
which resolves an empty
+        * value to false, and there false means the default. Here false is the 
opt-out, so an empty
+        * value has to keep the escaping rather than silently drop it.
+        */
+       @Test
+       void blankEscapeAttributeStillEscapes()
+       {
+               tester.startPage(new LabelPage());
+
+               String response = tester.getLastResponseAsString();
+               
assertTrue(response.contains("&lt;em&gt;escapeBlank&lt;/em&gt;"),
+                       "escape=\"\" should escape the label");
+               assertFalse(response.contains(ESCAPE_BLANK),
+                       "escape=\"\" should not let the label reach the markup 
as markup");
+       }
+
+       /**
+        * A value that is neither true nor false is a mistake in the markup, 
and it is reported the way
+        * the same mistake on {@literal <wicket:message>} is. The render wraps 
it, so the chain is what
+        * is asserted here.
+        * 
+        * @see org.apache.wicket.markup.resolver.WicketMessageResolverTest
+        */
+       @Test
+       void unrecognisedEscapeValueFails()
+       {
+               Exception exception = assertThrows(Exception.class,
+                       () -> tester.startPage(new WrongEscapeValuePage()));
+
+               StringValueConversionException cause = 
Exceptions.findCause(exception,
+                       StringValueConversionException.class);
+               assertNotNull(cause, "an unrecognised escape value should fail 
the render");
+               assertEquals("Boolean value \"yesPlease\" not recognized", 
cause.getMessage());
+       }
+
+       /**
+        * Escaping happens where the label is written, so the label the 
FormComponent keeps for its
+        * error messages is the bundle value either way.
+        */
+       @Test
+       void escapeFalseLeavesTheComponentLabelUnchanged()
+       {
+               tester.startPage(new LabelPage());
+
+               assertEquals(FROM_KEY,
+                       
((FormComponent<?>)tester.getComponentFromLastRenderedPage("form:fromKey")).getLabel()
+                               .getObject());
+       }
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java
index 9bfc3958fc..950622e745 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java
@@ -92,7 +92,27 @@ import org.apache.wicket.util.string.Strings;
  * </ul>
  * </li>
  * </ul>
+ * <p>
+ * The label taken from a model or from a resource bundle is text, so it is 
escaped before it is
+ * written. A tag can ask for it to be written as is instead:
+ * 
+ * <pre>
+ * {@literal
+ * <wicket:label escape="false"/>
+ * }
+ * </pre>
  * 
+ * The application then takes responsibility for the content. The attribute 
accepts
+ * <code>true</code>/<code>false</code>, <code>on</code>/<code>off</code>,
+ * <code>yes</code>/<code>no</code>, <code>y</code>/<code>n</code> and 
<code>1</code>/<code>0</code>;
+ * any other value raises a
+ * {@link org.apache.wicket.util.string.StringValueConversionException}. 
Leaving the attribute out,
+ * or leaving it empty, keeps the escaping.
+ * <p>
+ * The attribute says nothing about the tag body. That body is markup this 
label has just rendered
+ * itself and is always written as is. Note also that <code>{@literal 
<wicket:message>}</code>
+ * spells the same attribute the other way round: a message is written as 
markup by default and
+ * <code>escape="true"</code> asks for it to be escaped.
  * 
  * @author Carl-Eric Menzel
  * @author igor
@@ -101,6 +121,8 @@ public class AutoLabelTextResolver implements 
IComponentResolver
 {
        public static final String LABEL = "label";
 
+       public static final String ESCAPE_ATTRIBUTE = "escape";
+
        /**
         * This is inserted by the resolver to render the label.
         */
@@ -278,7 +300,10 @@ public class AutoLabelTextResolver implements 
IComponentResolver
                        else
                        {
                                // ...found the form component, so we can 
return our label.
-                               return new TextLabel(tag.getId(), related);
+                               TextLabel label = new TextLabel(tag.getId(), 
related);
+                               String escape = 
tag.getAttribute(ESCAPE_ATTRIBUTE);
+                               
label.setEscapeModelStrings(Strings.isEmpty(escape) || Strings.isTrue(escape));
+                               return label;
                        }
                }
                return null;

Reply via email to