1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.doxia.module.markdown;
20
21 import javax.inject.Inject;
22 import javax.inject.Named;
23 import javax.inject.Singleton;
24
25 import java.io.IOException;
26 import java.io.Reader;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.LinkedHashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36
37 import com.vladsch.flexmark.ast.Heading;
38 import com.vladsch.flexmark.ast.HtmlCommentBlock;
39 import com.vladsch.flexmark.ext.abbreviation.AbbreviationExtension;
40 import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
41 import com.vladsch.flexmark.ext.definition.DefinitionExtension;
42 import com.vladsch.flexmark.ext.escaped.character.EscapedCharacterExtension;
43 import com.vladsch.flexmark.ext.footnotes.FootnoteExtension;
44 import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
45 import com.vladsch.flexmark.ext.tables.TablesExtension;
46 import com.vladsch.flexmark.ext.typographic.TypographicExtension;
47 import com.vladsch.flexmark.ext.wikilink.WikiLinkExtension;
48 import com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension;
49 import com.vladsch.flexmark.html.HtmlRenderer;
50 import com.vladsch.flexmark.util.ast.Node;
51 import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
52 import com.vladsch.flexmark.util.data.MutableDataSet;
53 import org.apache.commons.io.IOUtils;
54 import org.apache.maven.doxia.markup.HtmlMarkup;
55 import org.apache.maven.doxia.markup.TextMarkup;
56 import org.apache.maven.doxia.module.xhtml5.Xhtml5Parser;
57 import org.apache.maven.doxia.parser.AbstractTextParser;
58 import org.apache.maven.doxia.parser.ParseException;
59 import org.apache.maven.doxia.sink.Sink;
60 import org.apache.maven.doxia.util.HtmlTools;
61 import org.codehaus.plexus.util.xml.pull.XmlPullParser;
62 import org.jsoup.Jsoup;
63 import org.jsoup.nodes.Document;
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 @Singleton
80 @Named("markdown")
81 public class MarkdownParser extends AbstractTextParser implements TextMarkup {
82
83
84
85
86
87
88
89
90
91 private static final Pattern METADATA_SECTION_PATTERN = Pattern.compile(
92 "\\A^"
93 + "(?:title|author|date|address|affiliation|copyright|email|keywords|language|phone|subtitle)"
94 + "[ \\t]*:[\\S\\s]+?^[ \\t]*$",
95 Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
96
97
98
99
100
101
102
103
104 private static final Pattern METADATA_ENTRY_PATTERN = Pattern.compile(
105 "^([^:\\r\\n]+?)[ \\t]*:([\\S\\s]+?)(?=(?:^(?:[^:\\r\\n]+?)[ \\t]*:)|^[ \\t]*$)", Pattern.MULTILINE);
106
107
108
109
110
111 @Inject
112 private MarkdownHtmlParser parser;
113
114
115
116
117 private static final com.vladsch.flexmark.parser.Parser FLEXMARK_PARSER;
118
119
120
121
122 private static final com.vladsch.flexmark.parser.Parser FLEXMARK_METADATA_PARSER;
123
124
125
126
127 private static final HtmlRenderer FLEXMARK_HTML_RENDERER;
128
129
130 static {
131 MutableDataSet flexmarkOptions = new MutableDataSet();
132
133
134 flexmarkOptions.set(
135 com.vladsch.flexmark.parser.Parser.EXTENSIONS,
136 Arrays.asList(
137 EscapedCharacterExtension.create(),
138 AbbreviationExtension.create(),
139 AutolinkExtension.create(),
140 DefinitionExtension.create(),
141 TypographicExtension.create(),
142 TablesExtension.create(),
143 WikiLinkExtension.create(),
144 FootnoteExtension.create(),
145 StrikethroughExtension.create()));
146
147
148 flexmarkOptions.set(TypographicExtension.SINGLE_QUOTE_UNMATCHED, "'");
149
150
151 flexmarkOptions.set(HtmlRenderer.HTML_BLOCK_OPEN_TAG_EOL, false);
152 flexmarkOptions.set(HtmlRenderer.HTML_BLOCK_CLOSE_TAG_EOL, false);
153 flexmarkOptions.set(HtmlRenderer.MAX_TRAILING_BLANK_LINES, -1);
154 flexmarkOptions.set(HtmlRenderer.FENCED_CODE_NO_LANGUAGE_CLASS, "nohighlight nocode");
155
156
157 FLEXMARK_PARSER =
158 com.vladsch.flexmark.parser.Parser.builder(flexmarkOptions).build();
159
160 MutableDataSet flexmarkMetadataOptions = new MutableDataSet();
161 flexmarkMetadataOptions.set(
162 com.vladsch.flexmark.parser.Parser.EXTENSIONS, Arrays.asList(YamlFrontMatterExtension.create()));
163 FLEXMARK_METADATA_PARSER = com.vladsch.flexmark.parser.Parser.builder(flexmarkMetadataOptions)
164 .build();
165
166
167 FLEXMARK_HTML_RENDERER = HtmlRenderer.builder(flexmarkOptions)
168 .linkResolverFactory(new FlexmarkDoxiaLinkResolver.Factory())
169 .build();
170 }
171
172 @Override
173 public void parse(Reader source, Sink sink, String reference) throws ParseException {
174 try {
175
176 String xhtml = toXhtml(source);
177
178
179
180
181
182 parser.setEmitComments(isEmitComments());
183 parser.parse(xhtml, getWrappedSink(sink), "Intermediate HTML from " + reference);
184 } catch (IOException e) {
185 throw new ParseException("Failed reading Markdown source document", e);
186 }
187 }
188
189 private boolean processMetadataForHtml(StringBuilder html, StringBuilder source) {
190 final Map<String, List<String>> metadata;
191 final int endOffset;
192
193 if (source.toString().startsWith("---")) {
194
195 Node documentRoot = FLEXMARK_METADATA_PARSER.parse(source.toString());
196 YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
197 visitor.visit(documentRoot);
198 metadata = visitor.getData();
199 endOffset = visitor.getEndOffset();
200 } else {
201
202
203 metadata = new LinkedHashMap<>();
204 Matcher metadataMatcher = METADATA_SECTION_PATTERN.matcher(source);
205 if (metadataMatcher.find()) {
206 String entry = metadataMatcher.group(0) + EOL;
207 Matcher entryMatcher = METADATA_ENTRY_PATTERN.matcher(entry);
208 while (entryMatcher.find()) {
209 String key = entryMatcher.group(1);
210 String value = normalizeMultilineValue(entryMatcher.group(2));
211 metadata.put(key, Collections.singletonList(value));
212 }
213 endOffset = metadataMatcher.end(0);
214 } else {
215 endOffset = 0;
216 }
217 }
218 if (endOffset > 0) {
219
220 source.delete(0, endOffset);
221 }
222 return writeHtmlMetadata(html, metadata);
223 }
224
225 static String normalizeMultilineValue(String value) {
226 return value.trim().replaceAll("[ \\t]*[\\r\\n]+[ \\t]*", " ");
227 }
228
229 private boolean writeHtmlMetadata(StringBuilder html, Map<String, List<String>> data) {
230 boolean containsTitle = false;
231 for (Entry<String, List<String>> entry : data.entrySet()) {
232 if (writeHtmlMetadata(html, entry.getKey(), entry.getValue())) {
233 containsTitle = true;
234 }
235 }
236 return containsTitle;
237 }
238
239 private boolean writeHtmlMetadata(StringBuilder html, String key, List<String> values) {
240 if ("title".equalsIgnoreCase(key)) {
241 html.append("<title>");
242 html.append(HtmlTools.escapeHTML(values.stream().collect(Collectors.joining(", ")), false));
243 html.append("</title>");
244 return true;
245 } else {
246 if (key.equalsIgnoreCase("author") && values.size() > 1) {
247
248 for (String value : values) {
249 writeHtmlMetadata(html, key, Collections.singletonList(value));
250 }
251 } else {
252
253 final String separator;
254 if (key.equalsIgnoreCase("keywords")) {
255 separator = ",";
256 } else {
257 separator = EOL;
258 }
259 html.append("<meta name='");
260 html.append(HtmlTools.escapeHTML(key));
261 html.append("' content='");
262 html.append(HtmlTools.escapeHTML(values.stream().collect(Collectors.joining(separator))));
263 html.append("' />");
264 }
265 return false;
266 }
267 }
268
269
270
271
272
273
274
275
276 String toXhtml(Reader source) throws IOException {
277
278 StringBuilder markdownText = new StringBuilder(IOUtils.toString(source));
279
280
281 StringBuilder html = new StringBuilder(1000);
282 html.append("<html>");
283 html.append("<head>");
284
285 boolean haveTitle = processMetadataForHtml(html, markdownText);
286
287
288
289 Node documentRoot = FLEXMARK_PARSER.parse(markdownText.toString());
290
291
292
293 if (!haveTitle && documentRoot.hasChildren()) {
294
295 Node firstNode = documentRoot.getFirstChild();
296 while (firstNode != null && firstNode instanceof HtmlCommentBlock) {
297 firstNode = firstNode.getNext();
298 }
299
300
301 if (firstNode != null && firstNode instanceof Heading) {
302 html.append("<title>");
303 TextCollectingVisitor collectingVisitor = new TextCollectingVisitor();
304 String headingText = collectingVisitor.collectAndGetText(firstNode);
305 html.append(HtmlTools.escapeHTML(headingText, false));
306 html.append("</title>");
307 }
308 }
309 html.append("</head>");
310 html.append("<body>");
311
312
313 FLEXMARK_HTML_RENDERER.render(documentRoot, html);
314
315 html.append("</body>");
316 html.append("</html>");
317
318 return toXhtml(html.toString());
319 }
320
321 private String toXhtml(String html) {
322 final Document document = Jsoup.parse(html);
323 document.outputSettings().syntax(Document.OutputSettings.Syntax.xml).prettyPrint(false);
324 return document.html();
325 }
326
327
328
329
330
331
332
333
334
335
336
337
338
339 @Named
340 public static class MarkdownHtmlParser extends Xhtml5Parser {
341 public MarkdownHtmlParser() {
342 super();
343 }
344
345 @Override
346 protected void init() {
347 super.init();
348 }
349
350 @Override
351 protected boolean baseEndTag(XmlPullParser parser, Sink sink) {
352 boolean visited = super.baseEndTag(parser, sink);
353 if (!visited) {
354 if (parser.getName().equals(HtmlMarkup.DIV.toString())) {
355 handleUnknown(parser, sink, TAG_TYPE_END);
356 visited = true;
357 }
358 }
359 return visited;
360 }
361
362 @Override
363 protected boolean baseStartTag(XmlPullParser parser, Sink sink) {
364 boolean visited = super.baseStartTag(parser, sink);
365 if (!visited) {
366 if (parser.getName().equals(HtmlMarkup.DIV.toString())) {
367 handleUnknown(parser, sink, TAG_TYPE_START);
368 visited = true;
369 }
370 }
371 return visited;
372 }
373 }
374 }