1. Project Clover database mar. janv. 20 2026 12:32:22 CET
  2. Package org.devacfr.maven.skins.reflow.snippet

File ComponentResolver.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

36
74
15
2
275
172
40
0,54
4,93
7,5
2,67

Classes

Class Line # Actions
ComponentResolver 47 61 0% 28 11
0.8989%
ComponentResolver.Accumulator 231 13 0% 12 0
1.0100%
 

Contributing tests

This file is covered by 32 tests. .

Source view

1    /*
2    * Copyright 2012-2025 Christophe Friederich
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10    * Unless required by applicable law or agreed to in writing, software
11    * distributed under the License is distributed on an "AS IS" BASIS,
12    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    * See the License for the specific language governing permissions and
14    * limitations under the License.
15    */
16    package org.devacfr.maven.skins.reflow.snippet;
17   
18    import static java.util.Objects.requireNonNull;
19   
20    import com.google.common.base.Strings;
21    import com.google.common.collect.Lists;
22    import com.google.common.collect.Maps;
23    import java.util.List;
24    import java.util.Map;
25    import java.util.regex.MatchResult;
26    import java.util.regex.Matcher;
27    import java.util.regex.Pattern;
28    import org.apache.commons.text.StringEscapeUtils;
29    import org.devacfr.maven.skins.reflow.JsoupUtils;
30    import org.devacfr.maven.skins.reflow.snippet.ComponentToken.Tag;
31    import org.devacfr.maven.skins.reflow.snippet.SnippetComponent.Type;
32    import org.jsoup.nodes.Document;
33    import org.jsoup.nodes.Element;
34    import org.jsoup.nodes.Node;
35    import org.jsoup.select.Elements;
36    import org.jsoup.select.NodeTraversor;
37    import org.jsoup.select.NodeVisitor;
38    import org.slf4j.Logger;
39    import org.slf4j.LoggerFactory;
40   
41    /**
42    * Resolve the type and tag type of component.
43    *
44    * @author Christophe Friederich
45    * @version 2.4
46    */
 
47    public class ComponentResolver {
48   
49    private static final Logger LOGGER = LoggerFactory.getLogger(ComponentResolver.class);
50   
51    /** **/
52    private static final Pattern RESOLVER_PATTERN = Pattern.compile(
53    "\\{\\{(<|%) (\\/?)([\\w\\-_]*)(\\s?(?:[\\w\\-_]*)(?:=[\\u201c|\"](?:[\\s\\w\\p{Punct}]*)[\\u201d|\"])?)* (\\/?)(>|%)\\}\\}",
54    Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);
55   
56    private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\s?(\\w*)=(\\\")?(\\w*)\2\\s?",
57    Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);
58   
59    private final SnippetParser parser;
60   
61    /**
62    * Default constructor
63    */
 
64  15 toggle public ComponentResolver() {
65  15 this(new SnippetParser());
66    }
67   
 
68  48 toggle public ComponentResolver(final SnippetParser parser) {
69  48 super();
70  48 this.parser = requireNonNull(parser);
71    }
72   
 
73  100 toggle public boolean isSnippet(final Node node) {
74  100 return parser.isSnippet(node);
75    }
76   
77    /**
78    * Collects all (start,end,empty) Element corresponding to a snippet component.
79    *
80    * @param document
81    * the Jsoup element to use
82    * @return Return a {@link Elements} representing all web components contained in Jsoup document.
83    */
 
84  34 toggle public Elements collect(final Element document) {
85  34 return collect(document, RESOLVER_PATTERN);
86    }
87   
88    /**
89    * Normalise the {@link Document} to enclose inline snippet in html element.
90    *
91    * @param document
92    * the document to use
93    * @return Returns the same normalised {@link Document}.
94    */
 
95  18 toggle public Element normalize(final Document document) {
96   
97  18 final Elements elements = collect(document);
98  18 if (LOGGER.isDebugEnabled()) {
99  18 LOGGER.debug("Snippet Collected");
100  18 LOGGER.debug(elements.toString());
101    }
102    // remove all section tags
103  18 if (!elements.isEmpty()) {
104  18 final Elements sections = document.getElementsByTag("section");
105  18 sections.forEach(Element::unwrap);
106    }
107   
108  18 elements.forEach(element -> {
109  69 String text = StringEscapeUtils.unescapeHtml4(element.html());
110  69 final Matcher matcher = RESOLVER_PATTERN.matcher(text);
111   
112  69 final List<MatchResult> results = Lists.newArrayList();
113   
114  158 while (matcher.find()) {
115  89 final MatchResult matchResult = matcher.toMatchResult();
116    // add only inner matches
117  89 if (matchResult.start() >= 0 || matchResult.end() <= text.length()) {
118  89 results.add(0, matcher.toMatchResult());
119    }
120    }
121  69 String convertedText = text;
122    // process from end to start
123  69 if (!results.isEmpty()) {
124  65 for (final MatchResult matchResult : results) {
125  89 final String snippet = text.substring(matchResult.start(), matchResult.end());
126  89 text = text.substring(0, matchResult.start()) + "<span>" + StringEscapeUtils.escapeHtml4(snippet) + "</span>"
127    + text.substring(matchResult.end());
128    // convert snippet to html
129  89 final String convertedSnippet = convertElementToHtml(
130    convertedText.substring(matchResult.start(), matchResult.end()));
131  89 convertedText = convertedText.substring(0, matchResult.start()) + convertedSnippet
132    + convertedText.substring(matchResult.end());
133    }
134  65 element.html(text);
135    // convert to html
136    // remove all snippet tags to defirentiate inline snippet in paragraph to sequence of snippets.
137  65 Element converted = JsoupUtils.createHtmlDocument(convertElementToHtml(convertedText)).body();
138  65 converted.children().forEach(e -> {
139  42 if (parser.isSnippet(e))
140  33 e.remove();
141    });
142    // remove empty <p> tags if no text content.
143  65 if (element.tagName().equals("p") && !JsoupUtils.hasTextNode(converted)) {
144  62 element.unwrap();
145    }
146    }
147    });
148  18 return document;
149    }
150   
151    /**
152    * Create a {@link ComponentToken} corresponding to the element.
153    *
154    * @param element
155    * the element to use.
156    * @return Return a new instance of {@link ComponentToken} representing the element.
157    */
 
158  100 toggle public ComponentToken create(final Element element) {
159  100 if (isSnippet(element)) {
160  2 Type type = Type.webComponent;
161  2 return new ComponentToken(element, element.tagName(), Tag.html, type);
162    } else {
163  98 final Matcher matcher = RESOLVER_PATTERN.matcher(element.ownText());
164   
165  98 if (matcher.matches()) {
166  97 return createToken(element, matcher);
167    }
168    }
169  1 return null;
170    }
171   
 
172  97 toggle private ComponentToken createToken(final Element element, final Matcher matcher) {
173  97 if (!Strings.isNullOrEmpty(matcher.group(2)) && !Strings.isNullOrEmpty(matcher.group(5))) {
174    // can not have same time empty and end identifier.
175  1 throw new RuntimeException("malformed component");
176    }
177  96 Tag tag = Tag.start;
178  96 if ("/".equals(matcher.group(2))) {
179  40 tag = Tag.end;
180  56 } else if ("/".equals(matcher.group(5))) {
181  11 tag = Tag.empty;
182    }
183  96 final Type type = "<".equals(matcher.group(1)) ? Type.shortcode : Type.webComponent;
184   
185  96 return new ComponentToken(element, matcher.group(3), tag, type);
186    }
187   
 
188  0 toggle protected static Map<String, String> extractAttributes(final String text) {
189  0 final Map<String, String> attrs = Maps.newHashMap();
190  0 final Matcher matcher = ATTRIBUTE_PATTERN.matcher(text);
191  0 while (matcher.find()) {
192  0 attrs.put(matcher.group(1).toLowerCase(), matcher.group(3));
193    }
194  0 return attrs;
195    }
196   
 
197  34 toggle public Elements collect(final Element root, final Pattern searchPattern) {
198  34 final Elements elements = new Elements();
199  34 NodeTraversor.traverse(new Accumulator(parser, root, elements, searchPattern), root);
200  34 return elements;
201    }
202   
203    /**
204    * Converts the snippet element to html format.
205    *
206    * @param element
207    * the html element to use.
208    * @return Returns a {@link String} representing the snippet element in html format.
209    */
 
210  85 toggle public static String convertElementToHtml(final Element element) {
211  85 return convertElementToHtml(element.text());
212    }
213   
214    /**
215    * Converts the snippet html to html format.
216    *
217    * @param html
218    * the html to use.
219    * @return Returns a {@link String} representing the snippet html in html format.
220    */
 
221  239 toggle public static String convertElementToHtml(final String html) {
222  239 return html.replace("{{< ", "<")
223    .replace(" />}}", "/>")
224    .replace(" /%}}", "/>")
225    .replace(" >}}", ">")
226    .replace("{{% ", "<")
227    .replace(" %}}", ">")
228    .replaceAll("\\u201c|\\u201d", "\"");
229    }
230   
 
231    private static class Accumulator implements NodeVisitor {
232   
233    /** */
234    private final Pattern searchPattern;
235   
236    private final Element root;
237   
238    private final Elements elements;
239   
240    private final SnippetParser parser;
241   
 
242  34 toggle Accumulator(final SnippetParser parser, final Element root, final Elements elements, final Pattern searchPattern) {
243  34 this.root = root;
244  34 this.elements = elements;
245  34 this.searchPattern = searchPattern;
246  34 this.parser = parser;
247    }
248   
 
249  1191 toggle @Override
250    public void head(final Node node, final int depth) {
251  1191 if (node instanceof Element) {
252  421 final Element el = (Element) node;
253  421 if (matches(root, el)) {
254  150 elements.add(el);
255    }
256    }
257    }
258   
 
259  421 toggle public boolean matches(final Element root, final Element element) {
260    // exclude if in <pre> element, allowing highlight component in documentation
261  421 if ("pre".equals(element.tagName()) || "code".equals(element.tagName())
262    || element.hasParent() && "pre".equals(element.parent().tagName())) {
263  12 return false;
264    }
265  409 return searchPattern.matcher(element.ownText()).find();
266    }
267   
 
268  1191 toggle @Override
269    public void tail(final Node node, final int depth) {
270  1191 if (node instanceof Element && parser.isSnippet(node)) {
271  6 elements.add((Element) node);
272    }
273    }
274    }
275    }