001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2016 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.text.filter;
021
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Map;
025
026import edu.umd.cs.findbugs.annotations.Nullable;
027import edu.umd.cs.findbugs.annotations.OverrideMustInvoke;
028import org.commonmark.Extension;
029import org.commonmark.node.FencedCodeBlock;
030import org.commonmark.node.Image;
031import org.commonmark.node.Link;
032import org.commonmark.node.Node;
033import org.commonmark.parser.Parser;
034import org.commonmark.renderer.html.AttributeProvider;
035import org.commonmark.renderer.html.HtmlRenderer;
036import org.shredzone.commons.text.LinkAnalyzer;
037import org.shredzone.commons.text.TextFilter;
038
039/**
040 * A filter that converts Markdown to HTML.
041 * <p>
042 * Uses <a href="https://github.com/atlassian/commonmark-java">commonmark-java</a> for
043 * converting Markdown to HTML.
044 *
045 * @see <a href="https://github.com/atlassian/commonmark-java">commonmark-java</a>
046 * @author Richard "Shred" Körber
047 */
048public class MarkdownFilter implements TextFilter {
049
050    private @Nullable LinkAnalyzer analyzer;
051    private @Nullable String preClass;
052
053    /**
054     * Sets a {@link LinkAnalyzer} to be used for converting links and image source URLs.
055     *
056     * @param analyzer
057     *            {@link LinkAnalyzer} to be used
058     */
059    public void setAnalyzer(@Nullable LinkAnalyzer analyzer) {
060        this.analyzer = analyzer;
061    }
062
063    /**
064     * Class name to be added to each fenced code block. This can be used for syntax
065     * highlighters like prettify.
066     *
067     * @param preClass
068     *            Name of the css class to be added to each fenced block.
069     * @since 2.4
070     */
071    public void setPreClass(@Nullable String preClass) {
072        this.preClass = preClass;
073    }
074
075    @Override
076    public CharSequence apply(CharSequence text) {
077        Node document = createParserBuilder().build().parse(text.toString());
078        return createHtmlRendererBuilder().build().render(document);
079    }
080
081    /**
082     * Creates and configures a commonmark {@link Parser.Builder} to be used for parsing.
083     * <p>
084     * Note that this method is commonmark specific and might be removed in future
085     * versions.
086     *
087     * @return {@link Parser.Builder} to be used for the markup parser
088     */
089    protected Parser.Builder createParserBuilder() {
090        Parser.Builder builder = Parser.builder();
091        List<Extension> extensions = createExtensionList();
092        if (!extensions.isEmpty()) {
093            builder.extensions(extensions);
094        }
095        return builder;
096    }
097
098    /**
099     * Creates and configures a commonmark {@link HtmlRenderer.Builder} to be used for
100     * rendering HTML. The default implementation adds an {@link AttributeProvider} that
101     * uses the {@link LinkAnalyzer} for analyzing links and generating HTML attributes.
102     * <p>
103     * Note that this method is commonmark specific and might be removed in future
104     * versions.
105     *
106     * @return {@link HtmlRenderer.Builder} to be used for HTML rendering
107     */
108    protected HtmlRenderer.Builder createHtmlRendererBuilder() {
109        HtmlRenderer.Builder builder = HtmlRenderer.builder();
110        if (analyzer != null) {
111            builder.attributeProviderFactory(context -> new LinkAnalyzingAttributeProvider(analyzer));
112        }
113        if (preClass != null) {
114            builder.attributeProviderFactory(context -> new FencedCodeBlockAttributeProvider(preClass));
115        }
116        List<Extension> extensions = createExtensionList();
117        if (!extensions.isEmpty()) {
118            builder.extensions(extensions);
119        }
120        return builder;
121    }
122
123    /**
124     * Creates a list of markdown extensions to be used. By default this list is empty.
125     * Subclasses may add extensions to the list.
126     * <p>
127     * Note that this method is commonmark specific and might be removed in future
128     * versions.
129     *
130     * @return Modifiable {@link List} of extensions
131     * @since 2.8
132     */
133    @OverrideMustInvoke
134    protected List<Extension> createExtensionList() {
135        return new ArrayList<>();
136    }
137
138    /**
139     * An {@link AttributeProvider} that uses {@link LinkAnalyzer}.
140     */
141    private static class LinkAnalyzingAttributeProvider implements AttributeProvider {
142        private static final String HTML_SRC = "src";
143        private static final String HTML_HREF = "href";
144        private static final String HTML_CLASS = "class";
145
146        private final LinkAnalyzer analyzer;
147
148        public LinkAnalyzingAttributeProvider(LinkAnalyzer analyzer) {
149            this.analyzer = analyzer;
150        }
151
152        @Override
153        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
154            if (node instanceof Image) {
155                String src = attributes.get(HTML_SRC);
156                if (src != null) {
157                    attributes.put(HTML_SRC, analyzer.imageUrl(src));
158                }
159            } else if (node instanceof Link) {
160                String href = attributes.get(HTML_HREF);
161                if (href != null) {
162                    attributes.put(HTML_HREF, analyzer.linkUrl(href));
163                    String type = analyzer.linkType(href);
164                    if (type != null) {
165                        String cssClass = attributes.get(HTML_CLASS);
166                        if (cssClass != null) {
167                            attributes.put(HTML_CLASS, cssClass + ' ' + type);
168                        } else {
169                            attributes.put(HTML_CLASS, type);
170                        }
171                    }
172                }
173            }
174        }
175    }
176
177    /**
178     * An {@link AttributeProvider} that adds a css class to all fenced code blocks.
179     */
180    private static class FencedCodeBlockAttributeProvider implements AttributeProvider {
181        private static final String HTML_CLASS = "class";
182
183        private final String preClass;
184
185        public FencedCodeBlockAttributeProvider(String preClass) {
186            this.preClass = preClass;
187        }
188
189        @Override
190        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
191            if (node instanceof FencedCodeBlock && "pre".equals(tagName)) {
192                attributes.put(HTML_CLASS, preClass);
193            }
194        }
195    }
196
197}