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.apache.maven.doxia.siterenderer.sink;
20  
21  import java.io.StringWriter;
22  import java.io.Writer;
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import org.apache.maven.doxia.markup.HtmlMarkup;
27  import org.apache.maven.doxia.module.xhtml5.Xhtml5Sink;
28  import org.apache.maven.doxia.sink.SinkEventAttributes;
29  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
30  import org.apache.maven.doxia.sink.impl.SinkUtils;
31  import org.apache.maven.doxia.site.MermaidConfiguration;
32  import org.apache.maven.doxia.siterenderer.DefaultSiteRenderer;
33  import org.apache.maven.doxia.siterenderer.DocumentContent;
34  import org.apache.maven.doxia.siterenderer.DocumentRenderingContext;
35  
36  /**
37   * Sink for site rendering of a document, to allow later merge document's output with a template.
38   * During raw Doxia rendering, content is stored in multiple fields for later use when incorporating
39   * into skin or template: title, date, authors, head, body
40   *
41   * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
42   */
43  @SuppressWarnings("checkstyle:methodname")
44  public class SiteRendererSink extends Xhtml5Sink implements DocumentContent {
45      private String date;
46  
47      private String title;
48  
49      private List<String> authors = new ArrayList<>();
50  
51      private final StringWriter headWriter;
52  
53      /** Buffer inside verbatim elements to potentially remove enclosed code elements for Mermaid diagrams */
54      private StringBuilder verbatimBuffer;
55  
56      private final Writer writer;
57  
58      private final MermaidConfiguration mermaidConfig;
59  
60      private DocumentRenderingContext docRenderingContext;
61  
62      private boolean containsMermaidDiagram = false;
63  
64      private boolean insideMermaidCodeElement = false;
65      /**
66       * Construct a new SiteRendererSink for a document.
67       *
68       * @param docRenderingContext the document's rendering context.
69       */
70      public SiteRendererSink(DocumentRenderingContext docRenderingContext) {
71          this(docRenderingContext, null);
72      }
73  
74      public SiteRendererSink(DocumentRenderingContext docRenderingContext, MermaidConfiguration mermaid) {
75          this(new StringWriter(), docRenderingContext, mermaid);
76      }
77  
78      private SiteRendererSink(
79              StringWriter writer, DocumentRenderingContext docRenderingContext, MermaidConfiguration mermaid) {
80          super(writer);
81  
82          this.writer = writer;
83          this.headWriter = new StringWriter();
84          this.docRenderingContext = docRenderingContext;
85          this.mermaidConfig = mermaid;
86  
87          /* the template is expected to have used the main tag, which can be used only once */
88          super.contentStack.push(HtmlMarkup.MAIN);
89      }
90  
91      /** {@inheritDoc} */
92      @Override
93      public void title_() {
94          if (getTextBuffer().length() > 0) {
95              title = getTextBuffer().toString();
96          }
97  
98          resetTextBuffer();
99      }
100 
101     /**
102      * {@inheritDoc}
103      *
104      * Reset text buffer, since text content before title must not be in title.
105      * @see org.apache.maven.doxia.module.xhtml5.Xhtml5Sink#title()
106      */
107     @Override
108     public void title(SinkEventAttributes attributes) {
109         resetTextBuffer();
110     }
111 
112     /** {@inheritDoc} */
113     @Override
114     public void author(SinkEventAttributes attributes) {
115         resetTextBuffer();
116     }
117 
118     /** {@inheritDoc} */
119     @Override
120     public void author_() {
121         if (getTextBuffer().length() > 0) {
122             String text = getTextBuffer().toString().trim();
123             authors.add(text);
124         }
125 
126         resetTextBuffer();
127     }
128 
129     /** {@inheritDoc} */
130     @Override
131     public void date(SinkEventAttributes attributes) {
132         resetTextBuffer();
133     }
134 
135     /** {@inheritDoc} */
136     @Override
137     public void date_() {
138         if (getTextBuffer().length() > 0) {
139             date = getTextBuffer().toString().trim();
140         }
141 
142         resetTextBuffer();
143     }
144 
145     @Override
146     public void verbatim(SinkEventAttributes attributes) {
147         if (mermaidConfig != null && normalizeClassAttributesForMermaid(attributes)) {
148             containsMermaidDiagram = true;
149             // remove the decoration code for Mermaid diagrams (otherwise Skins may add line numbers to the code
150             // element, which breaks Mermaid rendering)
151             SinkEventAttributes filteredAttributes = (SinkEventAttributes)
152                     SinkUtils.filterAttributes(attributes, new String[] {SinkEventAttributes.DECORATION});
153             super.verbatim(filteredAttributes);
154         } else {
155             // write subsequent verbatim content to a buffer, to be able to detect Mermaid diagrams in it and remove
156             // code element if needed
157             verbatimBuffer = new StringBuilder();
158             super.verbatim(attributes);
159         }
160     }
161 
162     @Override
163     public void verbatim_() {
164         flushVerbatimBuffer(false);
165         super.verbatim_();
166     }
167 
168     @Override
169     public void inline(SinkEventAttributes attributes) {
170         if (attributes.containsAttributes(SinkEventAttributeSet.Semantics.CODE)
171                 && mermaidConfig != null
172                 && normalizeClassAttributesForMermaid(attributes)) {
173             containsMermaidDiagram = true;
174             // writes to buffer
175             super.inline(attributes);
176             // remove code element from inline stack to prevent closing it
177             inlineStack.pop();
178             insideMermaidCodeElement = true;
179             // remove the code element from the verbatim buffer, to prevent Skins from adding line numbers to it, which
180             // breaks Mermaid rendering
181             flushVerbatimBuffer(true);
182         } else {
183             flushVerbatimBuffer(false);
184             super.inline(attributes);
185         }
186     }
187 
188     @Override
189     public void inline_() {
190         if (insideMermaidCodeElement) {
191             // this is the end of the code tag (which has been removed), so no need to close it
192             insideMermaidCodeElement = false;
193         } else {
194             super.inline_();
195         }
196     }
197 
198     private void flushVerbatimBuffer(boolean stripCodeElement) {
199         if (verbatimBuffer != null) {
200             String buffer = verbatimBuffer.toString();
201             if (stripCodeElement) {
202                 // remove code element and instead add attributes to the parent element <pre> to prevent the skin from
203                 // adding code highlighting/line numbers, which breaks Mermaid rendering
204                 buffer = buffer.replaceFirst("<pre><code([^>]*)>", "<pre$1>");
205             }
206             verbatimBuffer = null;
207             write(buffer);
208         }
209     }
210 
211     /**
212      * Normalize class attributes for Mermaid diagrams, to allow using either "mermaid" or "language-mermaid" as class.
213      *
214      * @param attributes the attributes to check and normalize.
215      * @return {@code true} if the attributes indicate a Mermaid diagram, {@code false} otherwise.
216      */
217     boolean normalizeClassAttributesForMermaid(SinkEventAttributes attributes) {
218         String lang = attributes != null ? (String) attributes.getAttribute(SinkEventAttributes.CLASS) : null;
219         if ("language-mermaid"
220                 .equals(lang)) { // "language-" prefix is used by some markdown parsers, e.g. flexmark-java
221             attributes.addAttribute(SinkEventAttributes.CLASS, "mermaid");
222             return true;
223         } else if ("mermaid".equals(lang)) {
224             return true;
225         }
226         return false;
227     }
228 
229     /**
230      * Include the Mermaid rendering script (either internal or external) and call it on diagrams afterwards.
231      * @see <a href="https://mermaid.ai/open-source/intro/getting-started.html#_4-calling-the-mermaid-javascript-api">Calling the Mermaid JavaScript API</a>
232      */
233     private void writeMermaidScript() {
234         if (mermaidConfig.getExternalJs() != null) {
235             write(mermaidConfig.getExternalJs().asScriptTag());
236         } else {
237             write("\n<script src=\"");
238             write(docRenderingContext.getRelativePath());
239 
240             if (mermaidConfig.isUseTiny()) {
241                 // use integrated tiny version of mermaid, which is smaller and faster to load, but has some limitations
242                 // (e.g. no sequence diagrams)
243                 write("/js/mermaid-" + DefaultSiteRenderer.MERMAID_VERSION + ".tiny.min.js");
244             } else {
245                 // use integrated full version of mermaid, which is larger and slower to load, but has all features
246                 write("/js/mermaid-" + DefaultSiteRenderer.MERMAID_VERSION + ".min.js");
247             }
248             write("\"></script>\n");
249         }
250         write("\n<script>\n");
251         if (mermaidConfig.getConfig() != null) {
252             write("mermaid.initialize(" + mermaidConfig.getConfig() + ");\n");
253         } else {
254             // By default, mermaid.run will be called when the document is ready, rendering all elements with
255             // class="mermaid".
256             write("mermaid.initialize({startOnLoad:true, securityLevel: 'loose'});\n");
257         }
258         write("</script>\n");
259     }
260 
261     /**
262      * {@inheritDoc}
263      *
264      * Do nothing.
265      * @see org.apache.maven.doxia.module.xhtml5.Xhtml5Sink#body_()
266      */
267     @Override
268     public void body_() {
269         if (containsMermaidDiagram && mermaidConfig != null) {
270             writeMermaidScript();
271         }
272     }
273 
274     /**
275      * {@inheritDoc}
276      *
277      * Do nothing.
278      * @see org.apache.maven.doxia.module.xhtml5.Xhtml5Sink#body()
279      */
280     @Override
281     public void body(SinkEventAttributes attributes) {
282         // nop
283     }
284 
285     /** {@inheritDoc} */
286     @Override
287     public void head_() {
288         setHeadFlag(false);
289     }
290 
291     /** {@inheritDoc} */
292     @Override
293     public void head(SinkEventAttributes attributes) {
294         setHeadFlag(true);
295     }
296 
297     /** {@inheritDoc} */
298     @Override
299     protected void write(String text) {
300         String txt = text;
301 
302         if (isHeadFlag()) {
303             headWriter.write(unifyEOLs(txt));
304 
305             return;
306         }
307 
308         if (docRenderingContext != null) {
309             String relativePathToBasedir = docRenderingContext.getRelativePath();
310 
311             if (relativePathToBasedir == null) {
312                 txt = txt == null || txt.isEmpty() ? txt : txt.replace("$relativePath", ".");
313             } else {
314                 txt = txt == null || txt.isEmpty() || relativePathToBasedir == null
315                         ? txt
316                         : txt.replace("$relativePath", relativePathToBasedir);
317             }
318         }
319 
320         if (verbatimBuffer != null) {
321             verbatimBuffer.append(unifyEOLs(txt));
322             return;
323         }
324         super.write(txt);
325     }
326 
327     // DocumentContent interface
328 
329     /** {@inheritDoc} */
330     public String getTitle() {
331         return title;
332     }
333 
334     /** {@inheritDoc} */
335     public List<String> getAuthors() {
336         return authors;
337     }
338 
339     /** {@inheritDoc} */
340     public String getDate() {
341         return date;
342     }
343 
344     /** {@inheritDoc} */
345     public String getBody() {
346         String body = writer.toString();
347 
348         return body.length() > 0 ? body : null;
349     }
350 
351     /** {@inheritDoc} */
352     public String getHead() {
353         String head = headWriter.toString();
354 
355         return head.length() > 0 ? head : null;
356     }
357 
358     /** {@inheritDoc} */
359     public DocumentRenderingContext getRenderingContext() {
360         return docRenderingContext;
361     }
362 }