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}