diff --git a/README.md b/README.md
index 7cd7327..f8b68da 100644
--- a/README.md
+++ b/README.md
@@ -21,18 +21,24 @@ This project provides a collection of template renderers.
## Encoding Renderers
-| Renderer | Description |
-|:---------------------------------|:--------------------------------------------------------|
-| `rife.render.EncodeBase64` | Encodes a template value to Base64 |
-| `rife.render.EncodeHtml` | Encodes a template value to HTML |
-| `rife.render.EncodeHtmlEntities` | Encodes a template value to HTML decimal entities |
-| `rife.render.EncodeJs` | Encodes a template value to JavaScript/ECMAScript |
-| `rife.render.EncodeJson` | Encodes a template value to JSON |
-| `rife.render.EncodeQp` | Converts a template value to a quoted-printable string |
-| `rife.render.EncodeUnicode` | Encodes a template value to Unicode escape codes |
-| `rife.render.EncodeUrl` | URL-encodes a template value |
-| `rife.render.EncodeXml` | Encodes a template value to XML |
-| `rife.render.ShorteUrl` | Shortens a template value using [is.gd](https://is.gd/) |
+| Renderer | Description |
+|:---------------------------------|:-------------------------------------------------------|
+| `rife.render.EncodeBase64` | Encodes a template value to Base64 |
+| `rife.render.EncodeHtml` | Encodes a template value to HTML |
+| `rife.render.EncodeHtmlEntities` | Encodes a template value to HTML decimal entities |
+| `rife.render.EncodeJs` | Encodes a template value to JavaScript/ECMAScript |
+| `rife.render.EncodeJson` | Encodes a template value to JSON |
+| `rife.render.EncodeQp` | Converts a template value to a quoted-printable string |
+| `rife.render.EncodeUnicode` | Encodes a template value to Unicode escape codes |
+| `rife.render.EncodeUrl` | URL-encodes a template value |
+| `rife.render.EncodeXml` | Encodes a template value to XML |
+
+## Format Renderers
+
+| Renderer | Description |
+|:---------------------------------|:-----------------------------------------------------------------|
+| `rife.render.formatCreditcard` | Formats a template credit card number value to the last 4 digits |
+| `rife.render.ShorteUrl` | Shortens a template value using [is./gd](https://is.gd/) |
## Text Renderers
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 77c19a3..ca883c0 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -67,13 +67,13 @@ tasks {
}
}
- javadoc {
+ javadoc {
title = "RIFE2 Template Renderers"
options {
this as StandardJavadocDocletOptions
keyWords(true)
splitIndex(true)
- links()
+ links("https://rife2.github.io/rife2/")
}
}
}
diff --git a/lib/src/main/java/rife/render/FormatCreditCard.java b/lib/src/main/java/rife/render/FormatCreditCard.java
new file mode 100644
index 0000000..c7f6f89
--- /dev/null
+++ b/lib/src/main/java/rife/render/FormatCreditCard.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * Formats a template credit card number value to the last 4 digits.
+ *
+ * Usage:
+ *
+ *
+ * <!--v render:rife.render.FormatCreditCard:valueId/-->
+ * {{v render:rife.render.FormatCreditCard:valueId/}}
+ *
+ *
+ * @author Erik C. Thauvin
+ * @since 1.0
+ */
+public class FormatCreditCard implements ValueRenderer {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String render(Template template, String valueId, String differentiator) {
+ return RenderUtils.formatCreditCard(RenderUtils.fetchValue(template, differentiator));
+ }
+}
diff --git a/lib/src/main/java/rife/render/RenderUtils.java b/lib/src/main/java/rife/render/RenderUtils.java
index d722d93..ffd0dd2 100644
--- a/lib/src/main/java/rife/render/RenderUtils.java
+++ b/lib/src/main/java/rife/render/RenderUtils.java
@@ -53,7 +53,7 @@ public final class RenderUtils {
/**
* Encodes a string to JavaScript/ECMAScript.
*
- * @param src the source string.
+ * @param src the source string
* @return the enocded string
*/
public static String encodeJS(String src) {
@@ -96,11 +96,53 @@ public final class RenderUtils {
return Convert.toString(value);
}
+ /**
+ * Returns the last 4 digits a credit card number. The number must satisfy the Luhn algorithm.
+ * Non-digits are stripped from the number.
+ *
+ * @param src the credit card number
+ * @return the last 4 digits of the credit card number or empty
+ */
+ public static String formatCreditCard(String src) {
+ if (src == null || src.isBlank()) {
+ return src;
+ }
+
+ try {
+ var cc = src.replaceAll("[^0-9]", "");
+
+ var len = cc.length();
+ if (len >= 4) {
+ // Luhn algorithm
+ var sum = 0;
+ boolean isSecond = false;
+ int digit;
+ for (int i = len - 1; i >= 0; i--) {
+ digit = cc.charAt(i) - '0';
+ if (isSecond) {
+ digit = digit * 2;
+ }
+ sum += digit / 10;
+ sum += digit % 10;
+
+ isSecond = !isSecond;
+ }
+ if (sum % 10 == 0) {
+ return cc.substring(len - 4);
+ }
+ }
+ } catch (NumberFormatException ignore) {
+ // do nothing
+ }
+
+ return "";
+ }
+
/**
* Translates a String to/from ROT13.
*
- * @param src the source String.
- * @return the translated String.
+ * @param src the source String
+ * @return the translated String
*/
public static String rot13(String src) {
if (src == null || src.isBlank()) {
@@ -144,7 +186,7 @@ public final class RenderUtils {
* @return the short URL
*/
public static String shortenUrl(String url) {
- if (url == null || url.isBlank() || !url.matches("^[Hh][Tt][Tt][Pp][Ss]?:\\/\\/\\w.*")) {
+ if (url == null || url.isBlank() || !url.matches("^[Hh][Tt][Tt][Pp][Ss]?://\\w.*")) {
return url;
}
@@ -203,8 +245,8 @@ public final class RenderUtils {
/**
* Converts a text string to HTML decimal entities.
*
- * @param src the String to convert.
- * @return the converted string.
+ * @param src the String to convert
+ * @return the converted String
*/
@SuppressWarnings("PMD.AvoidReassigningLoopVariables")
public static String toHtmlEntities(String src) {
diff --git a/lib/src/main/java/rife/render/ShortenUrl.java b/lib/src/main/java/rife/render/ShortenUrl.java
index 4a2c3f8..8340cc8 100644
--- a/lib/src/main/java/rife/render/ShortenUrl.java
+++ b/lib/src/main/java/rife/render/ShortenUrl.java
@@ -19,7 +19,6 @@ package rife.render;
import rife.template.Template;
import rife.template.ValueRenderer;
-import rife.tools.StringUtils;
/**
* Shortens a template value using is.gid. The value must a valid http or https URL.
@@ -34,7 +33,7 @@ import rife.tools.StringUtils;
*
*
* @author Erik C. Thauvin
-= * @since 1.0
+ * @since 1.0
*/
public class ShortenUrl implements ValueRenderer {
/**
diff --git a/lib/src/test/java/rife/render/TestEncode.java b/lib/src/test/java/rife/render/TestEncode.java
index f6fea39..f6f4144 100644
--- a/lib/src/test/java/rife/render/TestEncode.java
+++ b/lib/src/test/java/rife/render/TestEncode.java
@@ -100,16 +100,4 @@ class TestEncode {
t.setAttribute(TestCase.FOO, "a test &");
assertThat(t.getContent()).isEqualTo("\n a test &\n");
}
-
- @Test
- void testShortenUrl() {
- var t = TemplateFactory.HTML.get("shortenUrl");
- var url = "https://example.com/";
- var shortUrl = "https://is.gd/AG3Hwv";
- t.setValue(TestCase.FOO, url);
- assertThat(t.getContent()).isEqualTo(String.format("%s", shortUrl, url));
- t.setValue(TestCase.FOO, TestCase.FOO);
- assertThat(t.getContent()).isEqualTo("foo");
-
- }
}
\ No newline at end of file
diff --git a/lib/src/test/java/rife/render/TestFormat.java b/lib/src/test/java/rife/render/TestFormat.java
new file mode 100644
index 0000000..7153c68
--- /dev/null
+++ b/lib/src/test/java/rife/render/TestFormat.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+import rife.template.TemplateFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TestFormat {
+ @Test
+ void testFormatCreditCard() {
+ var t = TemplateFactory.TXT.get("formatCreditCard");
+ t.setAttribute(TestCase.FOO, "4342 2565 6244 0179");
+ assertThat(t.getContent()).as("US VISA").isEqualTo("0179");
+ t.setAttribute(TestCase.FOO, "5130-3899-9169-8324");
+ assertThat(t.getContent()).as("FR MASTERCARD").isEqualTo("8324");
+ t.setAttribute(TestCase.FOO, "374380141731053");
+ assertThat(t.getContent()).as("UK AMEX").isEqualTo("1053");
+ t.setAttribute(TestCase.FOO, "000000000000001");
+ assertThat(t.getContent()).isEmpty();
+ }
+
+ @Test
+ void testShortenUrl() {
+ var t = TemplateFactory.HTML.get("shortenUrl");
+ var url = "https://example.com/";
+ var shortUrl = "https://is.gd/AG3Hwv";
+ t.setValue(TestCase.FOO, url);
+ assertThat(t.getContent()).isEqualTo(String.format("%s", shortUrl, url));
+ t.setValue(TestCase.FOO, TestCase.FOO);
+ assertThat(t.getContent()).isEqualTo("foo");
+
+ }
+}
diff --git a/lib/src/test/resources/templates/formatCreditCard.txt b/lib/src/test/resources/templates/formatCreditCard.txt
new file mode 100644
index 0000000..973da5e
--- /dev/null
+++ b/lib/src/test/resources/templates/formatCreditCard.txt
@@ -0,0 +1 @@
+{{v render:rife.render.FormatCreditCard:foo/}}
\ No newline at end of file