Skip to content

Commit 44a59c8

Browse files
authored
support nested shortcodes (#339)
* support nested shortcodes * update tests for nested shortcodes ---------
1 parent ed6e7da commit 44a59c8

File tree

2 files changed

+196
-155
lines changed

2 files changed

+196
-155
lines changed

cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java

Lines changed: 162 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
* <http://www.gnu.org/licenses/gpl-3.0.html>.
2222
* #L%
2323
*/
24-
2524
import com.condation.cms.api.model.Parameter;
2625
import org.apache.commons.jexl3.JexlEngine;
2726
import org.apache.commons.jexl3.MapContext;
@@ -31,162 +30,170 @@
3130

3231
public class TagParser {
3332

34-
private final JexlEngine engine;
35-
36-
public TagParser(JexlEngine engine) {
37-
this.engine = engine;
38-
}
39-
40-
// Klasse zur Speicherung der Tag-Informationen
41-
public static record TagInfo (String name, Parameter rawAttributes, int startIndex, int endIndex) {}
42-
43-
// Erster Schritt: Alle Tags ermitteln und deren Positionen sowie Roh-Attribute speichern
44-
public List<TagInfo> findTags(String text, TagMap tagHandlers) {
45-
List<TagInfo> tags = new ArrayList<>();
46-
int i = 0;
47-
48-
while (i < text.length()) {
49-
if (text.charAt(i) == '[' && i + 1 < text.length() && text.charAt(i + 1) == '[') {
50-
int tagStart = i;
51-
int endTagIndex = findTagEnd(text, i);
52-
if (endTagIndex != -1) {
53-
String tagContent = text.substring(i + 2, endTagIndex).trim();
54-
boolean isSelfClosing = tagContent.endsWith("/");
55-
56-
if (isSelfClosing) {
57-
tagContent = tagContent.substring(0, tagContent.length() - 1).trim();
58-
}
59-
60-
int spaceIndex = tagContent.indexOf(' ');
61-
String tagName = spaceIndex == -1 ? tagContent : tagContent.substring(0, spaceIndex);
62-
Parameter rawAttributes = spaceIndex == -1
63-
? new Parameter()
64-
: parseRawAttributes(tagContent.substring(spaceIndex + 1));
65-
66-
int closingTagIndex = -1;
67-
if (!isSelfClosing) {
68-
closingTagIndex = text.indexOf("[[/" + tagName + "]]", endTagIndex + 2);
69-
if (closingTagIndex != -1) {
70-
String content = text.substring(endTagIndex + 2, closingTagIndex);
71-
rawAttributes.put("_content", content);
72-
endTagIndex = closingTagIndex + ("[[/" + tagName + "]]").length() - 2;
73-
}
74-
}
75-
76-
if (tagHandlers.has(tagName)) {
77-
tags.add(new TagInfo(tagName, rawAttributes, tagStart, endTagIndex + 2));
78-
i = endTagIndex + 2; // Zum nächsten Tag springen
79-
} else {
80-
i++;
81-
}
82-
} else {
83-
i++;
84-
}
85-
} else {
86-
i++;
87-
}
88-
}
89-
return tags;
90-
}
33+
private final JexlEngine engine;
34+
35+
public TagParser(JexlEngine engine) {
36+
this.engine = engine;
37+
}
38+
39+
// Klasse zur Speicherung der Tag-Informationen
40+
public static record TagInfo(String name, Parameter rawAttributes, int startIndex, int endIndex) {
41+
42+
}
43+
44+
// Erster Schritt: Alle Tags ermitteln und deren Positionen sowie Roh-Attribute speichern
45+
public List<TagInfo> findTags(String text, TagMap tagHandlers) {
46+
List<TagInfo> tags = new ArrayList<>();
47+
int i = 0;
48+
49+
while (i < text.length()) {
50+
if (text.charAt(i) == '[' && i + 1 < text.length() && text.charAt(i + 1) == '[') {
51+
int tagStart = i;
52+
int endTagIndex = findTagEnd(text, i);
53+
if (endTagIndex != -1) {
54+
String tagContent = text.substring(i + 2, endTagIndex).trim();
55+
boolean isSelfClosing = tagContent.endsWith("/");
56+
57+
if (isSelfClosing) {
58+
tagContent = tagContent.substring(0, tagContent.length() - 1).trim();
59+
}
60+
61+
int spaceIndex = tagContent.indexOf(' ');
62+
String tagName = spaceIndex == -1 ? tagContent : tagContent.substring(0, spaceIndex);
63+
Parameter rawAttributes = spaceIndex == -1
64+
? new Parameter()
65+
: parseRawAttributes(tagContent.substring(spaceIndex + 1));
66+
67+
int closingTagIndex = -1;
68+
if (!isSelfClosing) {
69+
closingTagIndex = text.indexOf("[[/" + tagName + "]]", endTagIndex + 2);
70+
if (closingTagIndex != -1) {
71+
String content = text.substring(endTagIndex + 2, closingTagIndex);
72+
rawAttributes.put("_content", content);
73+
endTagIndex = closingTagIndex + ("[[/" + tagName + "]]").length() - 2;
74+
}
75+
}
76+
77+
if (tagHandlers.has(tagName)) {
78+
tags.add(new TagInfo(tagName, rawAttributes, tagStart, endTagIndex + 2));
79+
i = endTagIndex + 2; // Zum nächsten Tag springen
80+
} else {
81+
i++;
82+
}
83+
} else {
84+
i++;
85+
}
86+
} else {
87+
i++;
88+
}
89+
}
90+
return tags;
91+
}
9192

9293
public String parse(String text, TagMap tagHandlers) {
9394
return parse(text, tagHandlers, Collections.emptyMap());
9495
}
95-
96-
// Zweiter Schritt: Tags basierend auf den gespeicherten Positionen ersetzen
97-
public String parse(String text, TagMap tagHandlers, Map<String, Object> contextModel) {
98-
// Erster Schritt: Finde alle Tags
99-
List<TagInfo> tags = findTags(text, tagHandlers);
100-
101-
// Zweiter Schritt: Ersetze alle Tags im Text
102-
StringBuilder result = new StringBuilder();
103-
int lastIndex = 0;
104-
for (TagInfo tag : tags) {
105-
result.append(text, lastIndex, tag.startIndex); // Unveränderten Teil des Textes hinzufügen
106-
Function<Parameter, String> handler = tagHandlers.get(tag.name);
107-
108-
// Im zweiten Schritt: Attribute auswerten
109-
Parameter evaluatedAttributes = evaluateAttributes(tag.rawAttributes, contextModel);
110-
111-
result.append(handler.apply(evaluatedAttributes)); // Tag-Ersetzung
112-
lastIndex = tag.endIndex; // Aktualisiere den Startpunkt für den nächsten Tag
113-
}
114-
result.append(text.substring(lastIndex)); // Füge den restlichen Text hinzu
115-
116-
return result.toString();
117-
}
118-
119-
// Methode zum Finden des Endes eines Tags
120-
private int findTagEnd(String text, int startIndex) {
121-
for (int i = startIndex; i < text.length() - 1; i++) {
122-
if (text.charAt(i) == ']' && text.charAt(i + 1) == ']') {
123-
return i;
124-
}
125-
}
126-
return -1; // Kein schließendes ']]' gefunden
127-
}
128-
129-
// Methode zur Attribut-Analyse im ersten Schritt (Rohwerte als Strings speichern)
130-
private Parameter parseRawAttributes(String attributesString) {
131-
Parameter attributes = new Parameter();
132-
StringBuilder key = new StringBuilder();
133-
StringBuilder value = new StringBuilder();
134-
boolean inQuotes = false;
135-
boolean readingKey = true;
136-
137-
for (int i = 0; i < attributesString.length(); i++) {
138-
char c = attributesString.charAt(i);
139-
if (c == '"' || c == '\'') {
140-
inQuotes = !inQuotes;
141-
} else if (!inQuotes && (c == '=' || c == ' ')) {
142-
if (readingKey) {
143-
readingKey = false;
144-
} else {
145-
attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern
146-
key.setLength(0);
147-
value.setLength(0);
148-
readingKey = true;
149-
}
150-
} else {
151-
if (readingKey) {
152-
key.append(c);
153-
} else {
154-
value.append(c);
155-
}
156-
}
157-
}
158-
159-
// Letztes Attribut verarbeiten
160-
if (key.length() > 0 && value.length() > 0) {
161-
attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern
162-
}
163-
164-
return attributes;
165-
}
166-
167-
// Zweiter Schritt: Attribute auswerten
168-
private Parameter evaluateAttributes(Parameter rawAttributes, Map<String, Object> contextModel) {
169-
Parameter evaluatedAttributes = new Parameter();
170-
for (Map.Entry<String, Object> entry : rawAttributes.entrySet()) {
171-
String key = entry.getKey();
172-
String rawValue = (String) entry.getValue(); // Rohwert als String
173-
evaluatedAttributes.put(key, parseValue(rawValue, contextModel)); // Wert erst jetzt parsen
174-
}
175-
return evaluatedAttributes;
176-
}
177-
178-
// Methode zur Auswertung von Attributwerten im zweiten Schritt
179-
private Object parseValue(String value, Map<String, Object> contextModel) {
180-
if (value.matches("\\d+")) {
181-
return Integer.valueOf(value);
182-
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
183-
return Boolean.valueOf(value);
184-
} else if (value.startsWith("${") && value.endsWith("}")) {
185-
String expressionString = value.substring(2, value.length() - 1);
186-
187-
var expression = engine.createExpression(expressionString);
188-
return expression.evaluate(new MapContext(contextModel));
189-
}
190-
return value;
191-
}
96+
97+
// Zweiter Schritt: Tags basierend auf den gespeicherten Positionen ersetzen
98+
public String parse(String text, TagMap tagHandlers, Map<String, Object> contextModel) {
99+
// Erster Schritt: Finde alle Tags
100+
List<TagInfo> tags = findTags(text, tagHandlers);
101+
102+
// Zweiter Schritt: Ersetze alle Tags im Text
103+
StringBuilder result = new StringBuilder();
104+
int lastIndex = 0;
105+
for (TagInfo tag : tags) {
106+
result.append(text, lastIndex, tag.startIndex); // Unveränderten Teil des Textes hinzufügen
107+
Function<Parameter, String> handler = tagHandlers.get(tag.name);
108+
109+
// Im zweiten Schritt: Attribute auswerten
110+
Parameter evaluatedAttributes = evaluateAttributes(tag.rawAttributes, contextModel);
111+
112+
if (evaluatedAttributes.containsKey("_content")) {
113+
String rawContent = (String) evaluatedAttributes.get("_content");
114+
String parsedContent = parse(rawContent, tagHandlers, contextModel); // Rekursives Parsen
115+
evaluatedAttributes.put("_content", parsedContent);
116+
}
117+
118+
result.append(handler.apply(evaluatedAttributes)); // Tag-Ersetzung
119+
lastIndex = tag.endIndex; // Aktualisiere den Startpunkt für den nächsten Tag
120+
}
121+
result.append(text.substring(lastIndex)); // Füge den restlichen Text hinzu
122+
123+
return result.toString();
124+
}
125+
126+
// Methode zum Finden des Endes eines Tags
127+
private int findTagEnd(String text, int startIndex) {
128+
for (int i = startIndex; i < text.length() - 1; i++) {
129+
if (text.charAt(i) == ']' && text.charAt(i + 1) == ']') {
130+
return i;
131+
}
132+
}
133+
return -1; // Kein schließendes ']]' gefunden
134+
}
135+
136+
// Methode zur Attribut-Analyse im ersten Schritt (Rohwerte als Strings speichern)
137+
private Parameter parseRawAttributes(String attributesString) {
138+
Parameter attributes = new Parameter();
139+
StringBuilder key = new StringBuilder();
140+
StringBuilder value = new StringBuilder();
141+
boolean inQuotes = false;
142+
boolean readingKey = true;
143+
144+
for (int i = 0; i < attributesString.length(); i++) {
145+
char c = attributesString.charAt(i);
146+
if (c == '"' || c == '\'') {
147+
inQuotes = !inQuotes;
148+
} else if (!inQuotes && (c == '=' || c == ' ')) {
149+
if (readingKey) {
150+
readingKey = false;
151+
} else {
152+
attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern
153+
key.setLength(0);
154+
value.setLength(0);
155+
readingKey = true;
156+
}
157+
} else {
158+
if (readingKey) {
159+
key.append(c);
160+
} else {
161+
value.append(c);
162+
}
163+
}
164+
}
165+
166+
// Letztes Attribut verarbeiten
167+
if (key.length() > 0 && value.length() > 0) {
168+
attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern
169+
}
170+
171+
return attributes;
172+
}
173+
174+
// Zweiter Schritt: Attribute auswerten
175+
private Parameter evaluateAttributes(Parameter rawAttributes, Map<String, Object> contextModel) {
176+
Parameter evaluatedAttributes = new Parameter();
177+
for (Map.Entry<String, Object> entry : rawAttributes.entrySet()) {
178+
String key = entry.getKey();
179+
String rawValue = (String) entry.getValue(); // Rohwert als String
180+
evaluatedAttributes.put(key, parseValue(rawValue, contextModel)); // Wert erst jetzt parsen
181+
}
182+
return evaluatedAttributes;
183+
}
184+
185+
// Methode zur Auswertung von Attributwerten im zweiten Schritt
186+
private Object parseValue(String value, Map<String, Object> contextModel) {
187+
if (value.matches("\\d+")) {
188+
return Integer.valueOf(value);
189+
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
190+
return Boolean.valueOf(value);
191+
} else if (value.startsWith("${") && value.endsWith("}")) {
192+
String expressionString = value.substring(2, value.length() - 1);
193+
194+
var expression = engine.createExpression(expressionString);
195+
return expression.evaluate(new MapContext(contextModel));
196+
}
197+
return value;
198+
}
192199
}

cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ void setup() {
6060
return "message: " + params.get("message");
6161
});
6262

63+
tagMap.put("parent", params -> {
64+
return "<div class='parent'>%s</div>".formatted((String)params.get("_content"));
65+
});
66+
tagMap.put("nested", params -> {
67+
return "nested";
68+
});
69+
6370
this.tagParser = new TagParser(new JexlBuilder().create());
6471
}
6572

@@ -143,4 +150,31 @@ public void namespace() {
143150
result = tagParser.parse("[[ns1:print message='Hello CondationCMS' /]]", tagMap);
144151
Assertions.assertThat(result).isEqualTo("message: Hello CondationCMS");
145152
}
153+
154+
@Test
155+
public void multiline () {
156+
String content = """
157+
[[content]]
158+
This is a multiline shortcode!
159+
[[/content]]
160+
""";
161+
162+
String result = tagParser.parse(content, tagMap);
163+
164+
Assertions.assertThat(result).isEqualToIgnoringWhitespace("This is a multiline shortcode!");
165+
}
166+
167+
@Test
168+
public void nested () {
169+
String content = """
170+
[[parent]]
171+
[[nested /]]
172+
[[/parent]]
173+
""";
174+
175+
var tags = tagParser.findTags(content, tagMap);
176+
Assertions.assertThat(tags.size()).isEqualTo(1);
177+
String result = tagParser.parse(content, tagMap);
178+
Assertions.assertThat(result).isEqualToIgnoringWhitespace("<div class='parent'>nested</div>");
179+
}
146180
}

0 commit comments

Comments
 (0)