View Javadoc
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 com.google.common.collect.Iterables;
19  import com.google.common.collect.Lists;
20  import java.io.IOException;
21  import java.util.List;
22  import javax.annotation.Nonnull;
23  import javax.annotation.Nullable;
24  import org.devacfr.maven.skins.reflow.JsoupUtils;
25  import org.jsoup.nodes.Comment;
26  import org.jsoup.nodes.Document;
27  import org.jsoup.nodes.Element;
28  import org.jsoup.nodes.Node;
29  import org.jsoup.nodes.TextNode;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  /**
34   * Specific process for each type of snippet component.
35   *
36   * @author Christophe Friederich
37   * @version 2.4
38   */
39  public abstract class Processor {
40  
41    private static final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
42  
43    /** */
44    protected final SnippetParser parser;
45  
46    /**
47     * Default constructor
48     *
49     * @param parser
50     *          current parser.
51     */
52    public Processor(final SnippetParser parser) {
53      this.parser = parser;
54    }
55  
56    /**
57     * Specific parsing for each {@link ComponentToken}.
58     *
59     * @param token
60     *          the current token.
61     */
62    public void parse(final ComponentToken token) {
63      if (LOGGER.isDebugEnabled()) {
64        LOGGER.debug("Parse Token: {}", token);
65      }
66      switch (token.tag()) {
67        case empty:
68          this.handleEmptyTag(token);
69          break;
70  
71        case start:
72          this.handleStartTag(token);
73          parser.push(token);
74          parser.parse();
75          break;
76  
77        case end:
78          final ComponentToken startToken = parser.pop();
79          final ComponentToken endToken = token;
80          this.handleCloseTag(startToken, endToken);
81          break;
82        case html:
83          this.handleHtmlTag(token);
84          break;
85        default:
86          throw new SnippetParseException("unknown token tag " + token.tag());
87      }
88    }
89  
90    /**
91     * Append child {@link Node} in html rendering.
92     *
93     * @param node
94     *          the node to use.
95     * @param writer
96     *          the html writer
97     * @throws IOException
98     *           If an I/O error occurs.
99     */
100   protected abstract void appendChildrenToHtml(Node node, Appendable writer) throws IOException;
101 
102   /**
103    * Handle start tag mismatch.
104    *
105    * @param token
106    *          the token
107    */
108   protected void handleStartTag(final ComponentToken token) {
109     // do nothing
110   }
111 
112   /**
113    * Handle close tag mismatch.
114    *
115    * @param startToken
116    *          the start token
117    * @param endToken
118    *          the end token.
119    */
120   protected void handleCloseTag(final ComponentToken startToken, final ComponentToken endToken) {
121     // do nothing
122   }
123 
124   /**
125    * Handle empty tag.
126    *
127    * @param token
128    *          the token
129    */
130   protected void handleEmptyTag(final ComponentToken token) {
131     // do nothing
132   }
133 
134   /**
135    * Handle html tag.
136    *
137    * @param token
138    *          the token
139    */
140   protected void handleHtmlTag(final ComponentToken token) {
141     // do nothing
142   }
143 
144   /**
145    * Convert the snippet to html.
146    *
147    * @param startToken
148    *          the start token
149    * @param endToken
150    *          the end token.
151    * @return Returns a new {@link Element} representing html represention of snippet.
152    */
153   protected Element convertToHtml(@Nonnull final ComponentToken startToken, @Nullable final ComponentToken endToken) {
154     final Element startElement = startToken.getElement();
155     final Node previousElement = startElement.previousSibling();
156     Element endElement = null;
157     if (endToken != null) {
158       endElement = endToken.getElement();
159     }
160     final Document doc = JsoupUtils.createHtmlDocument("");
161     final Element tmp = doc.body();
162     final Node parent = startElement.parentNode();
163 
164     final StringBuilder html = new StringBuilder(ComponentResolver.convertElementTextToHtml(startElement));
165     final List<Node> nodesToRemove = Lists.newArrayList();
166     nodesToRemove.add(startElement);
167     if (endElement != null) {
168       boolean startCopy = false;
169 
170       for (final Node n : parent.childNodes()) {
171         if (n.equals(endElement)) {
172           nodesToRemove.add(n);
173           break;
174         }
175         if (startCopy) {
176           try {
177             appendChildrenToHtml(n, html);
178           } catch (final IOException e) {
179             throw new RuntimeException(e.getMessage(), e);
180           }
181           nodesToRemove.add(n);
182         }
183         if (n.equals(startElement)) {
184           startCopy = true;
185         }
186       }
187       html.append(ComponentResolver.convertElementTextToHtml(endElement));
188     }
189     tmp.html(html.toString());
190     final Element component = tmp.children().first();
191     final Element parentElement = startElement.parent();
192 
193     if (previousElement != null) {
194       previousElement.after(component);
195     } else {
196       if (parentElement.children().first() != null) {
197         parentElement.children().first().before(component);
198       } else {
199         parentElement.children().add(component);
200       }
201     }
202     nodesToRemove.forEach(Node::remove);
203     return component;
204   }
205 
206   /**
207    * Specific process for snippet web component.
208    *
209    * @author Christophe Friederich
210    * @version 2.4
211    */
212   public static class WebComponentProcessor extends Processor {
213 
214     /**
215      * Default constructor
216      *
217      * @param parser
218      *          current parser.
219      */
220     public WebComponentProcessor(final SnippetParser parser) {
221       super(parser);
222     }
223 
224     /**
225      * {@inheritDoc}
226      */
227     @Override
228     protected void appendChildrenToHtml(final Node node, final Appendable writer) throws IOException {
229       if (node instanceof Element) {
230         final Element el = (Element) node;
231         // when use code highlighting
232         if ("div".equals(el.tagName()) && el.hasClass("source")) {
233           writer.append(el.text());
234         } else {
235           // comment can be enclose in <p> element.
236           if (Iterables.tryFind(el.childNodes(), n -> n instanceof Comment).isPresent()) {
237             writer.append(el.data());
238           } else {
239             writer.append(el.outerHtml());
240           }
241         }
242       } else if (node instanceof Comment) {
243         writer.append(((Comment) node).getData());
244       } else if (node instanceof TextNode) {
245         writer.append(((TextNode) node).text());
246       }
247     }
248 
249     @Override
250     protected void handleStartTag(ComponentToken token) {
251 
252     }
253 
254     /**
255      * {@inheritDoc}
256      */
257     @Override
258     protected void handleEmptyTag(final ComponentToken token) {
259       SnippetContext snippetContext = parser.getSnippetContext();
260       Element componentElement = convertToHtml(token, null);
261       Component<?> component = snippetContext.create(componentElement, token, null);
262       snippetContext.render(component);
263     }
264 
265     /**
266      * {@inheritDoc}
267      */
268     @Override
269     protected void handleCloseTag(final ComponentToken startToken, final ComponentToken endToken) {
270       SnippetContext snippetContext = parser.getSnippetContext();
271       // if (snippetContext.getCurrentWebComponentToken() != null) {
272       // return;
273       // }
274       if (!endToken.isCloseTagOf(startToken)) {
275         throw new RuntimeException("start token " + startToken + " should be closed by " + startToken.getCloseTag()
276             + ", but found " + endToken);
277       }
278 
279       Element componentElement = convertToHtml(startToken, endToken);
280       Component<?> component = snippetContext.create(componentElement, startToken, endToken);
281       snippetContext.render(component);
282     }
283 
284     /**
285      * {@inheritDoc}
286      */
287     @Override
288     protected void handleHtmlTag(final ComponentToken token) {
289       SnippetContext snippetContext = parser.getSnippetContext();
290       Element componentElement = token.getElement();
291       Component<?> component = snippetContext.create(componentElement, token, null);
292       snippetContext.render(component);
293     }
294 
295   }
296 
297 }