From 8e35cd5eca0b23993b44f1711396b376dde2e13b Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sat, 7 Dec 2024 16:47:00 +0100 Subject: [PATCH 1/2] support nested shortcodes --- .../cms/content/shortcodes/TagParser.java | 317 +++++++++--------- .../cms/content/shortcodes/TagParserTest.java | 27 ++ 2 files changed, 189 insertions(+), 155 deletions(-) diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java b/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java index af357e1e6..054e64f23 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java +++ b/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java @@ -21,7 +21,6 @@ * . * #L% */ - import com.condation.cms.api.model.Parameter; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.MapContext; @@ -31,162 +30,170 @@ public class TagParser { - private final JexlEngine engine; - - public TagParser(JexlEngine engine) { - this.engine = engine; - } - - // Klasse zur Speicherung der Tag-Informationen - public static record TagInfo (String name, Parameter rawAttributes, int startIndex, int endIndex) {} - - // Erster Schritt: Alle Tags ermitteln und deren Positionen sowie Roh-Attribute speichern - public List findTags(String text, TagMap tagHandlers) { - List tags = new ArrayList<>(); - int i = 0; - - while (i < text.length()) { - if (text.charAt(i) == '[' && i + 1 < text.length() && text.charAt(i + 1) == '[') { - int tagStart = i; - int endTagIndex = findTagEnd(text, i); - if (endTagIndex != -1) { - String tagContent = text.substring(i + 2, endTagIndex).trim(); - boolean isSelfClosing = tagContent.endsWith("/"); - - if (isSelfClosing) { - tagContent = tagContent.substring(0, tagContent.length() - 1).trim(); - } - - int spaceIndex = tagContent.indexOf(' '); - String tagName = spaceIndex == -1 ? tagContent : tagContent.substring(0, spaceIndex); - Parameter rawAttributes = spaceIndex == -1 - ? new Parameter() - : parseRawAttributes(tagContent.substring(spaceIndex + 1)); - - int closingTagIndex = -1; - if (!isSelfClosing) { - closingTagIndex = text.indexOf("[[/" + tagName + "]]", endTagIndex + 2); - if (closingTagIndex != -1) { - String content = text.substring(endTagIndex + 2, closingTagIndex); - rawAttributes.put("_content", content); - endTagIndex = closingTagIndex + ("[[/" + tagName + "]]").length() - 2; - } - } - - if (tagHandlers.has(tagName)) { - tags.add(new TagInfo(tagName, rawAttributes, tagStart, endTagIndex + 2)); - i = endTagIndex + 2; // Zum nächsten Tag springen - } else { - i++; - } - } else { - i++; - } - } else { - i++; - } - } - return tags; - } + private final JexlEngine engine; + + public TagParser(JexlEngine engine) { + this.engine = engine; + } + + // Klasse zur Speicherung der Tag-Informationen + public static record TagInfo(String name, Parameter rawAttributes, int startIndex, int endIndex) { + + } + + // Erster Schritt: Alle Tags ermitteln und deren Positionen sowie Roh-Attribute speichern + public List findTags(String text, TagMap tagHandlers) { + List tags = new ArrayList<>(); + int i = 0; + + while (i < text.length()) { + if (text.charAt(i) == '[' && i + 1 < text.length() && text.charAt(i + 1) == '[') { + int tagStart = i; + int endTagIndex = findTagEnd(text, i); + if (endTagIndex != -1) { + String tagContent = text.substring(i + 2, endTagIndex).trim(); + boolean isSelfClosing = tagContent.endsWith("/"); + + if (isSelfClosing) { + tagContent = tagContent.substring(0, tagContent.length() - 1).trim(); + } + + int spaceIndex = tagContent.indexOf(' '); + String tagName = spaceIndex == -1 ? tagContent : tagContent.substring(0, spaceIndex); + Parameter rawAttributes = spaceIndex == -1 + ? new Parameter() + : parseRawAttributes(tagContent.substring(spaceIndex + 1)); + + int closingTagIndex = -1; + if (!isSelfClosing) { + closingTagIndex = text.indexOf("[[/" + tagName + "]]", endTagIndex + 2); + if (closingTagIndex != -1) { + String content = text.substring(endTagIndex + 2, closingTagIndex); + rawAttributes.put("_content", content); + endTagIndex = closingTagIndex + ("[[/" + tagName + "]]").length() - 2; + } + } + + if (tagHandlers.has(tagName)) { + tags.add(new TagInfo(tagName, rawAttributes, tagStart, endTagIndex + 2)); + i = endTagIndex + 2; // Zum nächsten Tag springen + } else { + i++; + } + } else { + i++; + } + } else { + i++; + } + } + return tags; + } public String parse(String text, TagMap tagHandlers) { return parse(text, tagHandlers, Collections.emptyMap()); } - - // Zweiter Schritt: Tags basierend auf den gespeicherten Positionen ersetzen - public String parse(String text, TagMap tagHandlers, Map contextModel) { - // Erster Schritt: Finde alle Tags - List tags = findTags(text, tagHandlers); - - // Zweiter Schritt: Ersetze alle Tags im Text - StringBuilder result = new StringBuilder(); - int lastIndex = 0; - for (TagInfo tag : tags) { - result.append(text, lastIndex, tag.startIndex); // Unveränderten Teil des Textes hinzufügen - Function handler = tagHandlers.get(tag.name); - - // Im zweiten Schritt: Attribute auswerten - Parameter evaluatedAttributes = evaluateAttributes(tag.rawAttributes, contextModel); - - result.append(handler.apply(evaluatedAttributes)); // Tag-Ersetzung - lastIndex = tag.endIndex; // Aktualisiere den Startpunkt für den nächsten Tag - } - result.append(text.substring(lastIndex)); // Füge den restlichen Text hinzu - - return result.toString(); - } - - // Methode zum Finden des Endes eines Tags - private int findTagEnd(String text, int startIndex) { - for (int i = startIndex; i < text.length() - 1; i++) { - if (text.charAt(i) == ']' && text.charAt(i + 1) == ']') { - return i; - } - } - return -1; // Kein schließendes ']]' gefunden - } - - // Methode zur Attribut-Analyse im ersten Schritt (Rohwerte als Strings speichern) - private Parameter parseRawAttributes(String attributesString) { - Parameter attributes = new Parameter(); - StringBuilder key = new StringBuilder(); - StringBuilder value = new StringBuilder(); - boolean inQuotes = false; - boolean readingKey = true; - - for (int i = 0; i < attributesString.length(); i++) { - char c = attributesString.charAt(i); - if (c == '"' || c == '\'') { - inQuotes = !inQuotes; - } else if (!inQuotes && (c == '=' || c == ' ')) { - if (readingKey) { - readingKey = false; - } else { - attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern - key.setLength(0); - value.setLength(0); - readingKey = true; - } - } else { - if (readingKey) { - key.append(c); - } else { - value.append(c); - } - } - } - - // Letztes Attribut verarbeiten - if (key.length() > 0 && value.length() > 0) { - attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern - } - - return attributes; - } - - // Zweiter Schritt: Attribute auswerten - private Parameter evaluateAttributes(Parameter rawAttributes, Map contextModel) { - Parameter evaluatedAttributes = new Parameter(); - for (Map.Entry entry : rawAttributes.entrySet()) { - String key = entry.getKey(); - String rawValue = (String) entry.getValue(); // Rohwert als String - evaluatedAttributes.put(key, parseValue(rawValue, contextModel)); // Wert erst jetzt parsen - } - return evaluatedAttributes; - } - - // Methode zur Auswertung von Attributwerten im zweiten Schritt - private Object parseValue(String value, Map contextModel) { - if (value.matches("\\d+")) { - return Integer.valueOf(value); - } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { - return Boolean.valueOf(value); - } else if (value.startsWith("${") && value.endsWith("}")) { - String expressionString = value.substring(2, value.length() - 1); - - var expression = engine.createExpression(expressionString); - return expression.evaluate(new MapContext(contextModel)); - } - return value; - } + + // Zweiter Schritt: Tags basierend auf den gespeicherten Positionen ersetzen + public String parse(String text, TagMap tagHandlers, Map contextModel) { + // Erster Schritt: Finde alle Tags + List tags = findTags(text, tagHandlers); + + // Zweiter Schritt: Ersetze alle Tags im Text + StringBuilder result = new StringBuilder(); + int lastIndex = 0; + for (TagInfo tag : tags) { + result.append(text, lastIndex, tag.startIndex); // Unveränderten Teil des Textes hinzufügen + Function handler = tagHandlers.get(tag.name); + + // Im zweiten Schritt: Attribute auswerten + Parameter evaluatedAttributes = evaluateAttributes(tag.rawAttributes, contextModel); + + if (evaluatedAttributes.containsKey("_content")) { + String rawContent = (String) evaluatedAttributes.get("_content"); + String parsedContent = parse(rawContent, tagHandlers, contextModel); // Rekursives Parsen + evaluatedAttributes.put("_content", parsedContent); + } + + result.append(handler.apply(evaluatedAttributes)); // Tag-Ersetzung + lastIndex = tag.endIndex; // Aktualisiere den Startpunkt für den nächsten Tag + } + result.append(text.substring(lastIndex)); // Füge den restlichen Text hinzu + + return result.toString(); + } + + // Methode zum Finden des Endes eines Tags + private int findTagEnd(String text, int startIndex) { + for (int i = startIndex; i < text.length() - 1; i++) { + if (text.charAt(i) == ']' && text.charAt(i + 1) == ']') { + return i; + } + } + return -1; // Kein schließendes ']]' gefunden + } + + // Methode zur Attribut-Analyse im ersten Schritt (Rohwerte als Strings speichern) + private Parameter parseRawAttributes(String attributesString) { + Parameter attributes = new Parameter(); + StringBuilder key = new StringBuilder(); + StringBuilder value = new StringBuilder(); + boolean inQuotes = false; + boolean readingKey = true; + + for (int i = 0; i < attributesString.length(); i++) { + char c = attributesString.charAt(i); + if (c == '"' || c == '\'') { + inQuotes = !inQuotes; + } else if (!inQuotes && (c == '=' || c == ' ')) { + if (readingKey) { + readingKey = false; + } else { + attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern + key.setLength(0); + value.setLength(0); + readingKey = true; + } + } else { + if (readingKey) { + key.append(c); + } else { + value.append(c); + } + } + } + + // Letztes Attribut verarbeiten + if (key.length() > 0 && value.length() > 0) { + attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern + } + + return attributes; + } + + // Zweiter Schritt: Attribute auswerten + private Parameter evaluateAttributes(Parameter rawAttributes, Map contextModel) { + Parameter evaluatedAttributes = new Parameter(); + for (Map.Entry entry : rawAttributes.entrySet()) { + String key = entry.getKey(); + String rawValue = (String) entry.getValue(); // Rohwert als String + evaluatedAttributes.put(key, parseValue(rawValue, contextModel)); // Wert erst jetzt parsen + } + return evaluatedAttributes; + } + + // Methode zur Auswertung von Attributwerten im zweiten Schritt + private Object parseValue(String value, Map contextModel) { + if (value.matches("\\d+")) { + return Integer.valueOf(value); + } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return Boolean.valueOf(value); + } else if (value.startsWith("${") && value.endsWith("}")) { + String expressionString = value.substring(2, value.length() - 1); + + var expression = engine.createExpression(expressionString); + return expression.evaluate(new MapContext(contextModel)); + } + return value; + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java b/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java index 4ebb118c1..3911dcf3c 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java @@ -143,4 +143,31 @@ public void namespace() { result = tagParser.parse("[[ns1:print message='Hello CondationCMS' /]]", tagMap); Assertions.assertThat(result).isEqualTo("message: Hello CondationCMS"); } + + @Test + public void multiline () { + String content = """ + [[content]] + This is a multiline shortcode! + [[/content]] + """; + + String result = tagParser.parse(content, tagMap); + + System.out.println("-" + result + "-"); + } + + @Test + public void nested () { + String content = """ + [[content]] + [[nested /]] + [[/content]] + """; + + var tags = tagParser.findTags(content, tagMap); + Assertions.assertThat(tags.size()).isEqualTo(1); + String result = tagParser.parse(content, tagMap); + System.out.println("-" + result + "-"); + } } From 53c494bdbf6b1d74f0e431fb0652304c234783dc Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 9 Dec 2024 09:31:19 +0100 Subject: [PATCH 2/2] update tests for nested shortcodes --- .../cms/content/shortcodes/TagParserTest.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java b/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java index 3911dcf3c..80ba9abff 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java @@ -60,6 +60,13 @@ void setup() { return "message: " + params.get("message"); }); + tagMap.put("parent", params -> { + return "
%s
".formatted((String)params.get("_content")); + }); + tagMap.put("nested", params -> { + return "nested"; + }); + this.tagParser = new TagParser(new JexlBuilder().create()); } @@ -154,20 +161,20 @@ public void multiline () { String result = tagParser.parse(content, tagMap); - System.out.println("-" + result + "-"); + Assertions.assertThat(result).isEqualToIgnoringWhitespace("This is a multiline shortcode!"); } @Test public void nested () { String content = """ - [[content]] + [[parent]] [[nested /]] - [[/content]] + [[/parent]] """; var tags = tagParser.findTags(content, tagMap); Assertions.assertThat(tags.size()).isEqualTo(1); String result = tagParser.parse(content, tagMap); - System.out.println("-" + result + "-"); + Assertions.assertThat(result).isEqualToIgnoringWhitespace("
nested
"); } }