Switch from pegdown to flexmark-java

The pegdown project is no longer maintained and has been deprecated
in favour of flexmark [1].  Furthermore, flexmark has more features
than pegdown and has better performance [2].

[1] https://github.com/sirthias/pegdown
[2] https://github.com/vsch/flexmark-java#feature-comparison

Change-Id: Ie3c20a7fffb0f6917bd3de353a9d7b46eeaa818f
This commit is contained in:
Paladox none
2017-11-04 19:00:02 +00:00
parent 37748ec2ec
commit 54bd23bb4c
14 changed files with 715 additions and 148 deletions

View File

@@ -14,30 +14,33 @@
package com.google.gerrit.server.documentation;
import static com.vladsch.flexmark.profiles.pegdown.Extensions.ALL;
import static com.vladsch.flexmark.profiles.pegdown.Extensions.HARDWRAPS;
import static com.vladsch.flexmark.profiles.pegdown.Extensions.SUPPRESS_ALL_HTML;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.pegdown.Extensions.ALL;
import static org.pegdown.Extensions.HARDWRAPS;
import static org.pegdown.Extensions.SUPPRESS_ALL_HTML;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.vladsch.flexmark.Extension;
import com.vladsch.flexmark.ast.Block;
import com.vladsch.flexmark.ast.Heading;
import com.vladsch.flexmark.ast.Node;
import com.vladsch.flexmark.ast.util.TextCollectingVisitor;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.profiles.pegdown.PegdownOptionsAdapter;
import com.vladsch.flexmark.util.options.MutableDataHolder;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.StringEscapeUtils;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.pegdown.LinkRenderer;
import org.pegdown.PegDownProcessor;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.ast.HeaderNode;
import org.pegdown.ast.Node;
import org.pegdown.ast.RootNode;
import org.pegdown.ast.TextNode;
public class MarkdownFormatter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -48,9 +51,9 @@ public class MarkdownFormatter {
AtomicBoolean file = new AtomicBoolean();
String src;
try {
src = readPegdownCss(file);
src = readFlexMarkJavaCss(file);
} catch (IOException err) {
logger.atWarning().withCause(err).log("Cannot load pegdown.css");
logger.atWarning().withCause(err).log("Cannot load flexmark-java.css");
src = "";
}
defaultCss = file.get() ? null : src;
@@ -61,9 +64,9 @@ public class MarkdownFormatter {
return defaultCss;
}
try {
return readPegdownCss(new AtomicBoolean());
return readFlexMarkJavaCss(new AtomicBoolean());
} catch (IOException err) {
logger.atWarning().withCause(err).log("Cannot load pegdown.css");
logger.atWarning().withCause(err).log("Cannot load flexmark-java.css");
return "";
}
}
@@ -81,8 +84,28 @@ public class MarkdownFormatter {
return this;
}
private MutableDataHolder markDownOptions() {
int options = ALL & ~(HARDWRAPS);
if (suppressHtml) {
options |= SUPPRESS_ALL_HTML;
}
MutableDataHolder optionsExt =
PegdownOptionsAdapter.flexmarkOptions(
options, MarkdownFormatterHeader.HeadingExtension.create())
.toMutable();
ArrayList<Extension> extensions = new ArrayList<>();
for (Extension extension : optionsExt.get(com.vladsch.flexmark.parser.Parser.EXTENSIONS)) {
extensions.add(extension);
}
return optionsExt;
}
public byte[] markdownToDocHtml(String md, String charEnc) throws UnsupportedEncodingException {
RootNode root = parseMarkdown(md);
Node root = parseMarkdown(md);
HtmlRenderer renderer = HtmlRenderer.builder(markDownOptions()).build();
String title = findTitle(root);
StringBuilder html = new StringBuilder();
@@ -100,7 +123,7 @@ public class MarkdownFormatter {
html.append("\n</style>");
html.append("</head>");
html.append("<body>\n");
html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
html.append(renderer.render(root));
html.append("\n</body></html>");
return html.toString().getBytes(charEnc);
}
@@ -111,38 +134,36 @@ public class MarkdownFormatter {
}
private String findTitle(Node root) {
if (root instanceof HeaderNode) {
HeaderNode h = (HeaderNode) root;
if (h.getLevel() == 1 && h.getChildren() != null && !h.getChildren().isEmpty()) {
StringBuilder b = new StringBuilder();
for (Node n : root.getChildren()) {
if (n instanceof TextNode) {
b.append(((TextNode) n).getText());
}
}
return b.toString();
if (root instanceof Heading) {
Heading h = (Heading) root;
if (h.getLevel() == 1 && h.hasChildren()) {
TextCollectingVisitor collectingVisitor = new TextCollectingVisitor();
return collectingVisitor.collectAndGetText(h);
}
}
for (Node n : root.getChildren()) {
String title = findTitle(n);
if (title != null) {
return title;
if (root instanceof Block && root.hasChildren()) {
Node child = root.getFirstChild();
while (child != null) {
String title = findTitle(child);
if (title != null) {
return title;
}
child = child.getNext();
}
}
return null;
}
private RootNode parseMarkdown(String md) {
int options = ALL & ~(HARDWRAPS);
if (suppressHtml) {
options |= SUPPRESS_ALL_HTML;
}
return new PegDownProcessor(options).parseMarkdown(md.toCharArray());
private Node parseMarkdown(String md) {
Parser parser = Parser.builder(markDownOptions()).build();
Node document = parser.parse(md);
return document;
}
private static String readPegdownCss(AtomicBoolean file) throws IOException {
String name = "pegdown.css";
private static String readFlexMarkJavaCss(AtomicBoolean file) throws IOException {
String name = "flexmark-java.css";
URL url = MarkdownFormatter.class.getResource(name);
if (url == null) {
throw new FileNotFoundException("Resource " + name);

View File

@@ -0,0 +1,162 @@
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.documentation;
import com.google.common.flogger.FluentLogger;
import com.vladsch.flexmark.ast.Heading;
import com.vladsch.flexmark.ast.Node;
import com.vladsch.flexmark.ext.anchorlink.AnchorLink;
import com.vladsch.flexmark.ext.anchorlink.internal.AnchorLinkNodeRenderer;
import com.vladsch.flexmark.html.CustomNodeRenderer;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.html.renderer.DelegatingNodeRendererFactory;
import com.vladsch.flexmark.html.renderer.NodeRenderer;
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
import com.vladsch.flexmark.profiles.pegdown.Extensions;
import com.vladsch.flexmark.profiles.pegdown.PegdownOptionsAdapter;
import com.vladsch.flexmark.util.options.DataHolder;
import com.vladsch.flexmark.util.options.MutableDataHolder;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class MarkdownFormatterHeader {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static class HeadingExtension implements HtmlRendererExtension {
@Override
public void rendererOptions(final MutableDataHolder options) {
// add any configuration settings to options you want to apply to everything, here
}
@Override
public void extend(final HtmlRenderer.Builder rendererBuilder, final String rendererType) {
rendererBuilder.nodeRendererFactory(new HeadingNodeRenderer.Factory());
}
static HeadingExtension create() {
return new HeadingExtension();
}
}
static class HeadingNodeRenderer implements NodeRenderer {
public HeadingNodeRenderer(DataHolder options) {}
@Override
public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
return new HashSet<NodeRenderingHandler<? extends Node>>(
Arrays.asList(
new NodeRenderingHandler<AnchorLink>(
AnchorLink.class,
new CustomNodeRenderer<AnchorLink>() {
@Override
public void render(
AnchorLink node, NodeRendererContext context, HtmlWriter html) {
HeadingNodeRenderer.this.render(node, context, html);
}
}),
new NodeRenderingHandler<Heading>(
Heading.class,
new CustomNodeRenderer<Heading>() {
@Override
public void render(Heading node, NodeRendererContext context, HtmlWriter html) {
HeadingNodeRenderer.this.render(node, context, html);
}
})));
}
void render(final AnchorLink node, final NodeRendererContext context, final HtmlWriter html) {
Node parent = node.getParent();
if (parent instanceof Heading && ((Heading) parent).getLevel() == 1) {
// render without anchor link
context.renderChildren(node);
} else {
context.delegateRender();
}
}
static boolean haveExtension(int extensions, int flags) {
return (extensions & flags) != 0;
}
static boolean haveAllExtensions(int extensions, int flags) {
return (extensions & flags) == flags;
}
void render(final Heading node, final NodeRendererContext context, final HtmlWriter html) {
if (node.getLevel() == 1) {
// render without anchor link
final int extensions = context.getOptions().get(PegdownOptionsAdapter.PEGDOWN_EXTENSIONS);
if (context.getHtmlOptions().renderHeaderId
|| haveExtension(extensions, Extensions.ANCHORLINKS)
|| haveAllExtensions(
extensions, Extensions.EXTANCHORLINKS | Extensions.EXTANCHORLINKS_WRAP)) {
String id = context.getNodeId(node);
if (id != null) {
html.attr("id", id);
}
}
if (context.getHtmlOptions().sourcePositionParagraphLines) {
html.srcPos(node.getChars())
.withAttr()
.tagLine(
"h" + node.getLevel(),
new Runnable() {
@Override
public void run() {
html.srcPos(node.getText()).withAttr().tag("span");
context.renderChildren(node);
html.tag("/span");
}
});
} else {
html.srcPos(node.getText())
.withAttr()
.tagLine(
"h" + node.getLevel(),
new Runnable() {
@Override
public void run() {
context.renderChildren(node);
}
});
}
} else {
context.delegateRender();
}
}
public static class Factory implements DelegatingNodeRendererFactory {
@Override
public NodeRenderer create(final DataHolder options) {
return new HeadingNodeRenderer(options);
}
@Override
public Set<Class<? extends NodeRendererFactory>> getDelegates() {
Set<Class<? extends NodeRendererFactory>> delegates =
new HashSet<Class<? extends NodeRendererFactory>>();
delegates.add(AnchorLinkNodeRenderer.Factory.class);
return delegates;
}
}
}
}