View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.devacfr.maven.skins.reflow.snippet;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.io.IOException;
25  import java.util.List;
26  
27  import com.google.common.collect.Iterables;
28  import com.google.common.collect.Lists;
29  import org.devacfr.maven.skins.reflow.snippet.ComponentToken.Tag;
30  import org.jsoup.nodes.Comment;
31  import org.jsoup.nodes.Element;
32  import org.jsoup.nodes.Node;
33  import org.jsoup.nodes.TextNode;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  /**
38   * Specific process for each type of snippet component.
39   *
40   * @author Christophe Friederich
41   * @version 2.4
42   */
43  public abstract class Processor {
44  
45      private static final Logger LOGGER = LoggerFactory.getLogger(Processor.class);
46  
47      /** */
48      protected final SnippetParser parser;
49  
50      /**
51       * Default constructor
52       *
53       * @param parser
54       *            current parser.
55       */
56      public Processor(final SnippetParser parser) {
57          this.parser = parser;
58      }
59  
60      /**
61       * Specific parsing for each {@link ComponentToken}.
62       *
63       * @param token
64       *            the current token.
65       */
66      public void parse(final ComponentToken token) {
67          if (LOGGER.isDebugEnabled()) {
68              LOGGER.debug("Parse Token: {}", token);
69          }
70          switch (token.tag()) {
71              case empty:
72                  parser.getSnippetContext().render(createSnippetComponent(token, null));
73                  break;
74  
75              case start:
76                  parser.push(token);
77                  parser.parse();
78                  break;
79  
80              case end:
81                  final ComponentToken startToken = parser.pop();
82                  if (!token.isCloseTagOf(startToken)) {
83                      throw new RuntimeException(
84                              "start token " + startToken + " should be closed, but next token is " + token);
85                  }
86                  parser.getSnippetContext().render(createSnippetComponent(startToken, token));
87                  break;
88  
89              case html:
90                  parser.getSnippetContext().render(createSnippetComponent(token, null));
91                  break;
92  
93              default:
94                  throw new SnippetParseException("unknown token tag " + token.tag());
95          }
96      }
97  
98      /**
99       * Append child {@link Node} in html rendering.
100      *
101      * @param node
102      *            the node to use.
103      * @param writer
104      *            the html writer
105      * @throws IOException
106      *             If an I/O error occurs.
107      */
108     protected abstract void appendChildrenToHtml(Node node, Appendable writer) throws IOException;
109 
110     /**
111      * Convert the snippet to html.
112      *
113      * @param startToken
114      *            the start token
115      * @param endToken
116      *            the end token.
117      * @return Returns a new {@link Element} representing html represention of snippet.
118      */
119     protected Element convertToHtml(@Nonnull final ComponentToken startToken, @Nullable final ComponentToken endToken) {
120         final Element startElement = startToken.getElement();
121         final Node previousElement = startElement.previousSibling();
122         Element endElement = null;
123         if (endToken != null) {
124             endElement = endToken.getElement();
125         }
126         final Element tmp = new Element("component");
127         final Node parent = startElement.parentNode();
128 
129         final StringBuilder html = new StringBuilder(convertElementToHtml(startElement));
130         final List<Node> nodesToRemove = Lists.newArrayList();
131         nodesToRemove.add(startElement);
132         if (endElement != null) {
133             boolean startCopy = false;
134 
135             for (final Node n : parent.childNodes()) {
136                 if (n.equals(endElement)) {
137                     nodesToRemove.add(n);
138                     break;
139                 }
140                 if (startCopy) {
141                     try {
142                         appendChildrenToHtml(n, html);
143                     } catch (final IOException e) {
144                         throw new RuntimeException(e.getMessage(), e);
145                     }
146                     nodesToRemove.add(n);
147                 }
148                 if (n.equals(startElement)) {
149                     startCopy = true;
150                 }
151             }
152             html.append(convertElementToHtml(endElement));
153         }
154         tmp.append(html.toString());
155         final Element component = tmp.children().first();
156         final Element parentElement = startElement.parent();
157 
158         if (previousElement != null) {
159             previousElement.after(component);
160         } else {
161             if (parentElement.children().first() != null) {
162                 parentElement.children().first().before(component);
163             } else {
164                 parentElement.children().add(component);
165             }
166         }
167         nodesToRemove.forEach(Node::remove);
168         return component;
169     }
170 
171     /**
172      * Create a {@link SnippetComponent}.
173      *
174      * @param startToken
175      *            the start token.
176      * @param endToken
177      *            the end token.
178      * @return Returns a new instance of {@link SnippetComponent} representing the information contained between
179      *         {@code startToken} and {@code endToken}.
180      */
181     protected SnippetComponent<?> createSnippetComponent(final ComponentToken startToken,
182         final ComponentToken endToken) {
183         final SnippetContext snippetContext = parser.getSnippetContext();
184         Element componentElement = null;
185         if (Tag.html.equals(startToken.tag())) {
186             componentElement = startToken.getElement();
187         } else {
188             componentElement = convertToHtml(startToken, endToken);
189         }
190         return snippetContext.create(componentElement, startToken, endToken);
191     }
192 
193     /**
194      * Converts the snippet element to html format.
195      *
196      * @param element
197      *            the html element to use.
198      * @return Returns a {@link String} representing the snippet element in html format.
199      */
200     protected String convertElementToHtml(final Element element) {
201         return element.text()
202                 .replace("{{< ", "<")
203                 .replace(" />}}", "/>")
204                 .replace(" /%}}", "/>")
205                 .replace(" >}}", ">")
206                 .replace("{{% ", "<")
207                 .replace(" %}}", ">")
208                 .replaceAll("\\u201c|\\u201d", "\"");
209     }
210 
211     /**
212      * Specific process for snippet web component.
213      *
214      * @author Christophe Friederich
215      * @version 2.4
216      */
217     public static class WebComponentProcessor extends Processor {
218 
219         /**
220          * Default constructor
221          *
222          * @param parser
223          *            current parser.
224          */
225         public WebComponentProcessor(final SnippetParser parser) {
226             super(parser);
227         }
228 
229         /**
230          * {@inheritDoc}
231          */
232         @Override
233         protected void appendChildrenToHtml(final Node node, final Appendable writer) throws IOException {
234             if (node instanceof Element) {
235                 final Element el = (Element) node;
236                 // when use code highlighting
237                 if ("div".equals(el.tagName()) && el.hasClass("source")) {
238                     writer.append(el.text());
239                 } else {
240                     // comment can be enclose in <p> element.
241                     if (Iterables.tryFind(el.childNodes(), n -> n instanceof Comment).isPresent()) {
242                         writer.append(el.data());
243                     } else {
244                         writer.append(el.outerHtml());
245                     }
246                 }
247             } else if (node instanceof Comment) {
248                 writer.append(((Comment) node).getData());
249             } else if (node instanceof TextNode) {
250                 writer.append(((TextNode) node).text());
251             }
252         }
253     }
254 
255     /**
256      * Specific process for snippet shortcode component.
257      *
258      * @author Christophe Friederich
259      * @version 2.4
260      */
261     public static class ShortcodeProcessor extends Processor {
262 
263         /**
264          * Default constructor
265          *
266          * @param parser
267          *            current parser.
268          */
269         public ShortcodeProcessor(final SnippetParser parser) {
270             super(parser);
271         }
272 
273         @Override
274         protected void appendChildrenToHtml(final Node node, final Appendable writer) throws IOException {
275             writer.append(node.outerHtml());
276         }
277     }
278 }