001/*
002 * JDrupes MDoclet
003 * Copyright (C) 2017, 2021  Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.mdoclet.processors;
020
021import com.vladsch.flexmark.ext.abbreviation.AbbreviationExtension;
022import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension;
023import com.vladsch.flexmark.ext.definition.DefinitionExtension;
024import com.vladsch.flexmark.ext.footnotes.FootnoteExtension;
025import com.vladsch.flexmark.ext.tables.TablesExtension;
026import com.vladsch.flexmark.ext.toc.TocExtension;
027import com.vladsch.flexmark.ext.typographic.TypographicExtension;
028import com.vladsch.flexmark.ext.wikilink.WikiLinkExtension;
029import com.vladsch.flexmark.html.HtmlRenderer;
030import com.vladsch.flexmark.parser.Parser;
031import com.vladsch.flexmark.parser.ParserEmulationProfile;
032import com.vladsch.flexmark.util.ast.Node;
033import com.vladsch.flexmark.util.data.MutableDataSet;
034import com.vladsch.flexmark.util.misc.Extension;
035
036import java.lang.reflect.InvocationTargetException;
037import java.util.ArrayList;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Set;
041
042import org.jdrupes.mdoclet.MarkdownProcessor;
043import org.jdrupes.mdoclet.processors.flexmark.TopAnchorLinkExtension;
044
045/**
046 * This class provides an adapter for the 
047 * [flexmark-java](https://github.com/vsch/flexmark-java) Markdown
048 * processor.
049 * 
050 * The adapter supports the following flags:
051 * 
052 * `--parser-profile` 
053 * :   Sets one of the profiles defined in
054 *     `com.vladsch.flexmark.parser.ParserEmulationProfile`. The name of the
055 *     profle is the lower case name of the enum value. At the time of this
056 *     writing supported names are:
057 *      - commonmark (default)
058 *      - fixed_indent
059 *      - kramdown
060 *      - markdown
061 *      - github_doc
062 *      - multi_markdown
063 *      - pegdown
064 * 
065 * `--clear-extensions`
066 * :   Clears the list of extensions. The following extensions are predefined:
067 *       - [Abbreviation](https://github.com/vsch/flexmark-java/wiki/Extensions#abbreviation)
068 *       - [AnchorLink](https://github.com/vsch/flexmark-java/wiki/Extensions#anchorlink)
069 *       - [Definition](https://github.com/vsch/flexmark-java/wiki/Extensions#definition-lists) 
070 *         (Definition Lists)[^DefLists]
071 *       - [Footnote](https://github.com/vsch/flexmark-java/wiki/Extensions#footnotes)
072 *       - [Tables](https://github.com/vsch/flexmark-java/wiki/Extensions#tables)
073 *       - [Table of Content](https://github.com/vsch/flexmark-java/wiki/Extensions#table-of-contents-1)
074 *       - {@link TopAnchorLinkExtension TopAnchorLink} (provided by MDoclet)
075 * 
076 * `--extension <name>`
077 * :   Adds the flexmark extension with the given name to the list of extensions.
078 *     If the name contains a dot, it is assumed to be a fully qualified class
079 *     name. Else, it is expanded using the naming pattern used by flexmark.
080 *     
081 * The parser also supports disabling the automatic highlight feature.
082 * 
083 * [^DefLists]: If you use this extension, you'll most likely want to supply a
084 *     modified style sheet because the standard stylesheet assumes all definition
085 *     lists to be parameter defintion lists and formats them accordingly.
086 *     
087 *     Here are the changes made for this documentation:
088 *     ```css
089 *     /* [MOD] {@literal *}/
090 *     /* .contentContainer .description dl dd, {@literal *}/ .contentContainer .details dl dt, ...
091 *         font-size:12px;
092 *         font-weight:bold;
093 *         margin:10px 0 0 0;
094 *         color:#4E4E4E;
095 *     }
096 *     /* [MOD] Added {@literal *}/
097 *     dl dt {
098 *         margin:10px 0 0 0;
099 *     }
100 *     
101 *     /* [MOD] {@literal *}/
102 *     /* .contentContainer .description dl dd, {@literal *}/ .contentContainer .details dl dd, ...
103 *         margin:5px 0 10px 0px;
104 *         font-size:14px;
105 *         font-family:'DejaVu Sans Mono',monospace;
106 *     }
107 *     ```
108 * 
109 */
110public class FlexmarkProcessor implements MarkdownProcessor {
111
112    private static final String OPT_PROFILE = "--parser-profile";
113    private static final String OPT_CLEAR_EXTENSIONS = "--clear-extensions";
114    private static final String OPT_EXTENSION = "--extension";
115
116    private Parser parser;
117    private HtmlRenderer renderer;
118
119    @Override
120    public int isSupportedOption(String option) {
121        switch (option) {
122        case OPT_CLEAR_EXTENSIONS:
123        case INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT:
124            return 0;
125
126        case OPT_PROFILE:
127        case OPT_EXTENSION:
128            return 1;
129        default:
130            return -1;
131        }
132    }
133
134    @Override
135    public void start(String[] options) {
136        Set<Class<? extends Extension>> extensions = new HashSet<>();
137        extensions.add(AbbreviationExtension.class);
138        extensions.add(AnchorLinkExtension.class);
139        extensions.add(DefinitionExtension.class);
140        extensions.add(FootnoteExtension.class);
141        extensions.add(TablesExtension.class);
142        extensions.add(TypographicExtension.class);
143        extensions.add(TocExtension.class);
144        extensions.add(WikiLinkExtension.class);
145        extensions.add(TopAnchorLinkExtension.class);
146
147        MutableDataSet flexmarkOpts = new MutableDataSet();
148        flexmarkOpts.set(HtmlRenderer.GENERATE_HEADER_ID, true);
149
150        for (String opt : options) {
151            String[] optAndArgs = opt.split("[= ]");
152            switch (optAndArgs[0]) {
153            case OPT_PROFILE:
154                setFromProfile(flexmarkOpts, optAndArgs[1]);
155                continue;
156
157            case OPT_CLEAR_EXTENSIONS:
158                extensions.clear();
159                continue;
160
161            case OPT_EXTENSION:
162                try {
163                    String clsName = optAndArgs[1];
164                    if (!clsName.contains(".")) {
165                        clsName = "com.vladsch.flexmark.ext."
166                            + optAndArgs[1].toLowerCase() + "." + optAndArgs[1]
167                            + "Extension";
168                    }
169                    @SuppressWarnings("unchecked")
170                    Class<? extends Extension> cls
171                        = (Class<? extends Extension>) getClass()
172                            .getClassLoader().loadClass(clsName);
173                    extensions.add(cls);
174                    continue;
175                } catch (ClassNotFoundException | ClassCastException e) {
176                    throw new IllegalArgumentException("Cannot find extension "
177                        + optAndArgs[1] + " (check spelling and classpath).");
178                }
179
180            case INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT:
181                flexmarkOpts.set(
182                    HtmlRenderer.FENCED_CODE_NO_LANGUAGE_CLASS, "nohighlight");
183                continue;
184
185            default:
186                throw new IllegalArgumentException(
187                    "Unknown option: " + optAndArgs[0]);
188            }
189        }
190
191        List<Extension> extObjs = new ArrayList<>();
192        for (Class<? extends Extension> cls : extensions) {
193            try {
194                extObjs.add((Extension) cls.getMethod("create").invoke(null));
195            } catch (IllegalAccessException | IllegalArgumentException
196                    | InvocationTargetException | NoSuchMethodException
197                    | SecurityException e) {
198                throw new IllegalArgumentException(
199                    "Cannot create extension of type " + cls + ".");
200            }
201        }
202        if (!extObjs.isEmpty()) {
203            flexmarkOpts.set(Parser.EXTENSIONS, extObjs);
204        }
205        parser = Parser.builder(flexmarkOpts).build();
206        renderer = HtmlRenderer.builder(flexmarkOpts).build();
207    }
208
209    private void setFromProfile(MutableDataSet fmOpts, String profileName) {
210        for (ParserEmulationProfile p : ParserEmulationProfile.values()) {
211            if (p.toString().equalsIgnoreCase(profileName)) {
212                fmOpts.setFrom(p);
213                return;
214            }
215        }
216        throw new IllegalArgumentException("Unknown profile: " + profileName);
217    }
218
219    /*
220     * (non-Javadoc)
221     * 
222     * @see org.jdrupes.mdoclet.MarkdownProcessor#toHtml(java.lang.String)
223     */
224    @Override
225    public String toHtml(String markdown) {
226        Node document = parser.parse(markdown);
227        return renderer.render(document);
228    }
229
230    /*
231     * (non-Javadoc)
232     * 
233     * @see
234     * org.jdrupes.mdoclet.MarkdownProcessor#toHtmlFragment(java.lang.String)
235     */
236    @Override
237    public String toHtmlFragment(String markdown) {
238        markdown = markdown.trim();
239        String result = toHtml(markdown).trim();
240        if (markdown.startsWith("<")) {
241            // We should check if a surrounding tag was added. But until
242            // this pops up as an issue, let's keep this simple.
243            return result;
244        }
245        if (result.toUpperCase().startsWith("<P>")
246            && result.toUpperCase().endsWith("</P>")) {
247            return result.substring(3, result.length() - 4);
248        }
249        return result;
250    }
251
252}