1. Project Clover database mer. févr. 4 2026 12:48:44 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
76
15
2
282
174
40
0,53
5,07
7,5
2,67

Classes

Class Line # Actions
ComponentResolver 47 63 0% 28 11
0.8921568489,2%
ComponentResolver.Accumulator 238 13 0% 12 0
1.0100%
 

Contributing tests

This file is covered by 33 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.TagType;
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  49 toggle public ComponentResolver(final SnippetParser parser) {
69  49 super();
70  49 this.parser = requireNonNull(parser);
71    }
72   
 
73  104 toggle public boolean isSnippet(final Node node) {
74  104 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  36 toggle public Elements collect(final Element document) {
85  36 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  19 toggle public Element normalize(final Document document) {
96   
97  19 final Elements elements = collect(document);
98  19 if (LOGGER.isDebugEnabled()) {
99  19 LOGGER.debug("Snippet Collected");
100  19 LOGGER.debug(elements.toString());
101    }
102    // remove all section tags
103  19 if (!elements.isEmpty()) {
104  19 final Elements sections = document.getElementsByTag("section");
105  19 sections.forEach(Element::unwrap);
106    }
107   
108  19 elements.forEach(element -> {
109  73 String text = StringEscapeUtils.unescapeHtml4(element.html());
110  73 final Matcher matcher = RESOLVER_PATTERN.matcher(text);
111   
112  73 final List<MatchResult> results = Lists.newArrayList();
113   
114  166 while (matcher.find()) {
115  93 final MatchResult matchResult = matcher.toMatchResult();
116    // add only inner matches
117  93 if (matchResult.start() >= 0 || matchResult.end() <= text.length()) {
118  93 results.add(0, matcher.toMatchResult());
119    }
120    }
121  73 String convertedText = text;
122    // process from end to start
123  73 if (!results.isEmpty()) {
124  69 for (final MatchResult matchResult : results) {
125    // escape snippet tag
126  93 final String snippet = text.substring(matchResult.start(), matchResult.end());
127    // wrap snippet in span for display
128  93 text = text.substring(0, matchResult.start()) + "<span>" + StringEscapeUtils.escapeHtml4(snippet) + "</span>"
129    + text.substring(matchResult.end());
130    // convert snippet to html
131  93 final String convertedSnippet = convertSnippetTagsToHtml(
132    convertedText.substring(matchResult.start(), matchResult.end()));
133    // replace in text
134  93 convertedText = convertedText.substring(0, matchResult.start()) + convertedSnippet
135    + convertedText.substring(matchResult.end());
136    }
137    // convert to html
138    // remove all snippet tags to defirentiate inline snippet in paragraph to
139    // sequence of snippets.
140  69 Element body = JsoupUtils.createHtmlDocument(convertSnippetTagsToHtml(convertedText)).body();
141  69 body.children().forEach(e -> {
142  44 if (parser.isSnippet(e))
143  39 e.remove();
144    });
145  69 boolean containsOnlySnippets = !JsoupUtils.hasTextNode(body);
146  69 element.html(text);
147   
148    // remove <p> tag if not necessary, i.e. contains only snippet components
149  69 if (element.tagName().equals("p") && containsOnlySnippets) {
150  66 element.unwrap();
151    }
152    }
153    });
154  19 return document;
155    }
156   
157    /**
158    * Create a {@link ComponentToken} corresponding to the element.
159    *
160    * @param element
161    * the element to use.
162    * @return Return a new instance of {@link ComponentToken} representing the element.
163    */
 
164  104 toggle public ComponentToken create(final Element element) {
165  104 if (isSnippet(element)) {
166  2 Type type = Type.webComponent;
167  2 return new ComponentToken(element, element.tagName(), TagType.html, type);
168    } else {
169  102 final Matcher matcher = RESOLVER_PATTERN.matcher(element.ownText());
170   
171  102 if (matcher.matches()) {
172  101 return createToken(element, matcher);
173    }
174    }
175  1 return null;
176    }
177   
 
178  101 toggle private ComponentToken createToken(final Element element, final Matcher matcher) {
179  101 if (!Strings.isNullOrEmpty(matcher.group(2)) && !Strings.isNullOrEmpty(matcher.group(5))) {
180    // can not have same time empty and end identifier.
181  1 throw new RuntimeException("malformed component");
182    }
183  100 TagType tag = TagType.start;
184  100 if ("/".equals(matcher.group(2))) {
185  42 tag = TagType.end;
186  58 } else if ("/".equals(matcher.group(5))) {
187  11 tag = TagType.empty;
188    }
189  100 final Type type = "<".equals(matcher.group(1)) ? Type.shortcode : Type.webComponent;
190   
191  100 return new ComponentToken(element, matcher.group(3), tag, type);
192    }
193   
 
194  0 toggle protected static Map<String, String> extractAttributes(final String text) {
195  0 final Map<String, String> attrs = Maps.newHashMap();
196  0 final Matcher matcher = ATTRIBUTE_PATTERN.matcher(text);
197  0 while (matcher.find()) {
198  0 attrs.put(matcher.group(1).toLowerCase(), matcher.group(3));
199    }
200  0 return attrs;
201    }
202   
 
203  36 toggle public Elements collect(final Element root, final Pattern searchPattern) {
204  36 final Elements elements = new Elements();
205  36 NodeTraversor.traverse(new Accumulator(parser, root, elements, searchPattern), root);
206  36 return elements;
207    }
208   
209    /**
210    * Converts the text of element to html format.
211    *
212    * @param element
213    * the html element to use.
214    * @return Returns a {@link String} representing the snippet element in html format.
215    */
 
216  89 toggle public static String convertElementTextToHtml(final Element element) {
217  89 return convertSnippetTagsToHtml(element.text());
218    }
219   
220    /**
221    * Converts the snippet tags to html format.
222    *
223    * @param html
224    * the html to use.
225    * @return Returns a {@link String} representing the snippet html in html format.
226    */
 
227  251 toggle public static String convertSnippetTagsToHtml(final String html) {
228  251 String text = StringEscapeUtils.unescapeHtml4(html);
229  251 return text.replace("{{< ", "<")
230    .replace(" />}}", "/>")
231    .replace(" /%}}", "/>")
232    .replace(" >}}", ">")
233    .replace("{{% ", "<")
234    .replace(" %}}", ">")
235    .replaceAll("\\u201c|\\u201d", "\"");
236    }
237   
 
238    private static class Accumulator implements NodeVisitor {
239   
240    /** */
241    private final Pattern searchPattern;
242   
243    private final Element root;
244   
245    private final Elements elements;
246   
247    private final SnippetParser parser;
248   
 
249  36 toggle Accumulator(final SnippetParser parser, final Element root, final Elements elements, final Pattern searchPattern) {
250  36 this.root = root;
251  36 this.elements = elements;
252  36 this.searchPattern = searchPattern;
253  36 this.parser = parser;
254    }
255   
 
256  1261 toggle @Override
257    public void head(final Node node, final int depth) {
258  1261 if (node instanceof Element) {
259  447 final Element el = (Element) node;
260  447 if (matches(root, el)) {
261  158 elements.add(el);
262    }
263    }
264    }
265   
 
266  447 toggle public boolean matches(final Element root, final Element element) {
267    // exclude if in <pre> element, allowing highlight component in documentation
268  447 if ("pre".equals(element.tagName()) || "code".equals(element.tagName())
269    || element.hasParent() && "pre".equals(element.parent().tagName())) {
270  12 return false;
271    }
272  435 return searchPattern.matcher(element.ownText()).find();
273    }
274   
 
275  1261 toggle @Override
276    public void tail(final Node node, final int depth) {
277  1261 if (node instanceof Element && parser.isSnippet(node)) {
278  6 elements.add((Element) node);
279    }
280    }
281    }
282    }