Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.documentation; 16 : 17 : import static com.vladsch.flexmark.profiles.pegdown.Extensions.ALL; 18 : import static com.vladsch.flexmark.profiles.pegdown.Extensions.HARDWRAPS; 19 : import static com.vladsch.flexmark.profiles.pegdown.Extensions.SUPPRESS_ALL_HTML; 20 : import static java.nio.charset.StandardCharsets.UTF_8; 21 : 22 : import com.google.common.base.Strings; 23 : import com.google.common.flogger.FluentLogger; 24 : import com.google.gerrit.common.Nullable; 25 : import com.vladsch.flexmark.ast.Heading; 26 : import com.vladsch.flexmark.ast.util.TextCollectingVisitor; 27 : import com.vladsch.flexmark.html.HtmlRenderer; 28 : import com.vladsch.flexmark.parser.Parser; 29 : import com.vladsch.flexmark.profiles.pegdown.PegdownOptionsAdapter; 30 : import com.vladsch.flexmark.util.ast.Block; 31 : import com.vladsch.flexmark.util.ast.Node; 32 : import com.vladsch.flexmark.util.data.MutableDataHolder; 33 : import java.io.FileNotFoundException; 34 : import java.io.IOException; 35 : import java.io.InputStream; 36 : import java.io.UnsupportedEncodingException; 37 : import java.net.URL; 38 : import java.nio.charset.Charset; 39 : import java.util.concurrent.atomic.AtomicBoolean; 40 : import org.apache.commons.text.StringEscapeUtils; 41 : import org.eclipse.jgit.util.RawParseUtils; 42 : import org.eclipse.jgit.util.TemporaryBuffer; 43 : 44 0 : public class MarkdownFormatter { 45 0 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 46 : 47 : private static final String defaultCss; 48 : 49 : static { 50 0 : AtomicBoolean file = new AtomicBoolean(); 51 : String src; 52 : try { 53 0 : src = readFlexMarkJavaCss(file); 54 0 : } catch (IOException err) { 55 0 : logger.atWarning().withCause(err).log("Cannot load flexmark-java.css"); 56 0 : src = ""; 57 0 : } 58 0 : defaultCss = file.get() ? null : src; 59 0 : } 60 : 61 : private static String readCSS() { 62 0 : if (defaultCss != null) { 63 0 : return defaultCss; 64 : } 65 : try { 66 0 : return readFlexMarkJavaCss(new AtomicBoolean()); 67 0 : } catch (IOException err) { 68 0 : logger.atWarning().withCause(err).log("Cannot load flexmark-java.css"); 69 0 : return ""; 70 : } 71 : } 72 : 73 : private boolean suppressHtml; 74 : private String css; 75 : 76 : public MarkdownFormatter suppressHtml() { 77 0 : suppressHtml = true; 78 0 : return this; 79 : } 80 : 81 : public MarkdownFormatter setCss(String css) { 82 0 : this.css = StringEscapeUtils.escapeHtml4(css); 83 0 : return this; 84 : } 85 : 86 : private MutableDataHolder markDownOptions() { 87 0 : int options = ALL & ~HARDWRAPS; 88 0 : if (suppressHtml) { 89 0 : options |= SUPPRESS_ALL_HTML; 90 : } 91 : 92 0 : MutableDataHolder optionsExt = 93 0 : PegdownOptionsAdapter.flexmarkOptions( 94 0 : options, MarkdownFormatterHeader.HeadingExtension.create()) 95 0 : .toMutable(); 96 : 97 0 : return optionsExt; 98 : } 99 : 100 : public byte[] markdownToDocHtml(String md, String charEnc) throws UnsupportedEncodingException { 101 0 : Node root = parseMarkdown(md); 102 0 : HtmlRenderer renderer = HtmlRenderer.builder(markDownOptions()).build(); 103 0 : String title = findTitle(root); 104 : 105 0 : StringBuilder html = new StringBuilder(); 106 0 : html.append("<html>"); 107 0 : html.append("<head>"); 108 0 : if (!Strings.isNullOrEmpty(title)) { 109 0 : html.append("<title>").append(title).append("</title>"); 110 : } 111 0 : html.append("<style type=\"text/css\">\n"); 112 0 : if (css != null) { 113 0 : html.append(css); 114 : } else { 115 0 : html.append(readCSS()); 116 : } 117 0 : html.append("\n</style>"); 118 0 : html.append("</head>"); 119 0 : html.append("<body>\n"); 120 0 : html.append(renderer.render(root)); 121 0 : html.append("\n</body></html>"); 122 0 : return html.toString().getBytes(charEnc); 123 : } 124 : 125 : public String extractTitleFromMarkdown(byte[] data, String charEnc) { 126 0 : String md = RawParseUtils.decode(Charset.forName(charEnc), data); 127 0 : return findTitle(parseMarkdown(md)); 128 : } 129 : 130 : @Nullable 131 : private String findTitle(Node root) { 132 0 : if (root instanceof Heading) { 133 0 : Heading h = (Heading) root; 134 0 : if (h.getLevel() == 1 && h.hasChildren()) { 135 0 : TextCollectingVisitor collectingVisitor = new TextCollectingVisitor(); 136 0 : return collectingVisitor.collectAndGetText(h); 137 : } 138 : } 139 : 140 0 : if (root instanceof Block && root.hasChildren()) { 141 0 : Node child = root.getFirstChild(); 142 0 : while (child != null) { 143 0 : String title = findTitle(child); 144 0 : if (title != null) { 145 0 : return title; 146 : } 147 0 : child = child.getNext(); 148 0 : } 149 : } 150 : 151 0 : return null; 152 : } 153 : 154 : private Node parseMarkdown(String md) { 155 0 : Parser parser = Parser.builder(markDownOptions()).build(); 156 0 : Node document = parser.parse(md); 157 0 : return document; 158 : } 159 : 160 : private static String readFlexMarkJavaCss(AtomicBoolean file) throws IOException { 161 0 : String name = "flexmark-java.css"; 162 0 : URL url = MarkdownFormatter.class.getResource(name); 163 0 : if (url == null) { 164 0 : throw new FileNotFoundException("Resource " + name); 165 : } 166 0 : file.set("file".equals(url.getProtocol())); 167 0 : try (InputStream in = url.openStream(); 168 0 : TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) { 169 0 : tmp.copy(in); 170 0 : return new String(tmp.toByteArray(), UTF_8); 171 : } 172 : } 173 : }