diff --git a/README.md b/README.md
index 1f96f6c..35a8bc0 100644
--- a/README.md
+++ b/README.md
@@ -21,17 +21,17 @@ This project provides a collection of useful template renderers.
## Encoding Renderers
-| Renderer | Description |
-|:--------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------|
-| [rife.render.EncodeBase64](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeBase64) | Encodes a template value to Base64 |
-| [rife.render.EncodeHtml](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeHtml) | Encodes a template value to HTML |
-| [rife.render.EncodeHtmlEntities](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeHtmlEntities) | Encodes a template value to HTML decimal entities |
-| [rife.render.EncodeJs](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeJs) | Encodes a template value to JavaScript/ECMAScript |
-| [rife.render.EncodeJson](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeJson) | Encodes a template value to JSON |
-| [rife.render.EncodeUnicode](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeUnicode) | Encodes a template value to Unicode escape codes |
-| [rife.render.EncodeUrl](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeUrl) | URL-encodes a template value |
-| [rife.render.EncodeXml](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeXml) | Encodes a template value to XML |
-
+| Renderer | Description |
+|:--------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------|
+| [rife.render.EncodeBase64](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeBase64) | Encodes a template value to Base64 |
+| [rife.render.EncodeHtml](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeHtml) | Encodes a template value to HTML |
+| [rife.render.EncodeHtmlEntities](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeHtmlEntities) | Encodes a template value to HTML decimal entities |
+| [rife.render.EncodeJs](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeJs) | Encodes a template value to JavaScript/ECMAScript |
+| [rife.render.EncodeJson](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeJson) | Encodes a template value to JSON |
+| [rife.render.EncodeUnicode](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeUnicode) | Encodes a template value to Unicode escape codes |
+| [rife.render.EncodeUrl](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeUrl) | URL-encodes a template value |
+| [rife.render.EncodeXml](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.EncodeXml) | Encodes a template value to XML |
+| [rife.render.Normalize](https://github.com/rife2/rife2-template-renderers/wiki/rife.render.Normalize) | Normalizes a template value for inclusion in a URL path. |
## Format Renderers
| Renderer | Description |
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index f897576..976c72a 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -13,7 +13,7 @@ plugins {
id("com.github.ben-manes.versions") version "0.46.0"
}
-val rifeVersion by rootProject.extra { "1.5.0-SNAPSHOT" }
+val rifeVersion by rootProject.extra { "1.5.0" }
group = "com.uwyn.rife2"
version = "0.9.0-SNAPSHOT"
diff --git a/lib/src/main/java/rife/render/Normalize.java b/lib/src/main/java/rife/render/Normalize.java
new file mode 100644
index 0000000..2f1097d
--- /dev/null
+++ b/lib/src/main/java/rife/render/Normalize.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 rife.render;
+
+import rife.template.Template;
+import rife.template.ValueRenderer;
+
+/**
+ * Normalizes a template value for inclusion in URL path.
+ *
+ * Usage:
+ *
+ *
+ * <!--v render:rife.render.Normalize:valueId/-->
+ * {{v render:rife.render.Normalize:valueId/}}
+ *
+ *
+ * @author Erik C. Thauvin
+ * @see rife.render.Normalize
+ * @since 1.0
+ */
+public class Normalize implements ValueRenderer {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String render(Template template, String valueId, String differentiator) {
+ return RenderUtils.normalize(template.getValueOrAttribute(differentiator));
+ }
+}
diff --git a/lib/src/main/java/rife/render/RenderUtils.java b/lib/src/main/java/rife/render/RenderUtils.java
index 6692c3d..3883f05 100644
--- a/lib/src/main/java/rife/render/RenderUtils.java
+++ b/lib/src/main/java/rife/render/RenderUtils.java
@@ -24,6 +24,7 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.text.Normalizer;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
@@ -121,6 +122,34 @@ public final class RenderUtils {
return "";
}
+ /**
+ * Normalizes a String for inclusion in a URL path.
+ *
+ * @param src The source String
+ * @return The normalized String
+ */
+ public static String normalize(String src) {
+ var sb = new StringBuilder(src.length());
+ var normalized = Normalizer.normalize(src.trim(), Normalizer.Form.NFD);
+ boolean space = false;
+ for (var c : normalized.toCharArray()) {
+ if (c <= '\u007F') { // ascii only
+ if (!space && c == ' ') {
+ space = true;
+ sb.append('-');
+ } else {
+ space = false;
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {
+ sb.append(c);
+ } else if (c >= 'A' && c <= 'Z') {
+ sb.append((char) (c + 32)); // lowercase
+ }
+ }
+ }
+ }
+ return sb.toString();
+ }
+
/**
* Returns the plural form of a word, if count > 1.
*
diff --git a/lib/src/test/java/rife/render/TestEncode.java b/lib/src/test/java/rife/render/TestEncode.java
index 0ccf03e..fe54771 100644
--- a/lib/src/test/java/rife/render/TestEncode.java
+++ b/lib/src/test/java/rife/render/TestEncode.java
@@ -93,4 +93,13 @@ class TestEncode {
t.setAttribute(TestCase.FOO, "a test &");
assertThat(t.getContent()).isEqualTo("\n a test &\n");
}
+
+ @Test
+ void testNormalize() {
+ var t = TemplateFactory.HTML.get("normalize");
+ var foo = "News for January 6, 2023 (Paris)";
+ t.setValue(TestCase.FOO, foo);
+ assertThat(t.getContent()).isEqualTo(""
+ + foo + "");
+ }
}
\ No newline at end of file
diff --git a/lib/src/test/resources/templates/normalize.html b/lib/src/test/resources/templates/normalize.html
new file mode 100644
index 0000000..3f40a91
--- /dev/null
+++ b/lib/src/test/resources/templates/normalize.html
@@ -0,0 +1 @@
+
\ No newline at end of file