/*####################################################### * * Maintained 2018-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing * https://www.apache.org/licenses/LICENSE-2.0 * #########################################################*/ /* * Parses most common markdown tags. Only inline tags are supported, multiline/block syntax * is not supported (citation, multiline code, ..). This is intended to stay as easy as possible. * * You can e.g. apply a accent color by replacing #000001 with your accentColor string. * * FILTER_ANDROID_TEXTVIEW output is intended to be used at simple Android TextViews, * were a limited set of _html tags is supported. This allow to still display e.g. a simple * CHANGELOG.md file without including a WebView for showing HTML, or other additional UI-libraries. * * FILTER_WEB is intended to be used at engines understanding most common HTML tags. */ package net.gsantner.opoc.format.markdown; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; /** * Simple Markdown Parser */ @SuppressWarnings({"WeakerAccess", "CaughtExceptionImmediatelyRethrown", "SameParameterValue", "unused", "SpellCheckingInspection", "RepeatedSpace", "SingleCharAlternation", "Convert2Lambda"}) public class SimpleMarkdownParser { //######################## //## Statics //######################## public interface SmpFilter { String filter(String text); } public final static SmpFilter FILTER_ANDROID_TEXTVIEW = new SmpFilter() { @Override public String filter(String text) { // TextView supports a limited set of html tags, most notably // a href, b, big, font size&color, i, li, small, u // Don't start new line if 2 empty lines and heading while (text.contains("\n\n#")) { text = text.replace("\n\n#", "\n#"); } return text .replaceAll("(?s)", "") // HTML comments .replace("\n\n", "\n
\n") // Start new line if 2 empty lines .replace("~°", "  ") // double space/half tab .replaceAll("(?m)^### (.*)$", "
$1
") // h3 .replaceAll("(?m)^## (.*)$", "
$1

") // h2 (DEP: h3) .replaceAll("(?m)^# (.*)$", "
$1

") // h1 (DEP: h2,h3) .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) .replaceAll("(?m)^([-*] )(.*)$", " $2
") // unordered list + end line .replaceAll("(?m)^ (-|\\*) ([^<]*)$", "   $2
") // unordered list2 + end line .replaceAll("`([^<]*)`", "$1") // code .replace("\\*", "●") // temporary replace escaped star symbol .replaceAll("(?m)\\*\\*(.*)\\*\\*", "$1") // bold (DEP: temp star) .replaceAll("(?m)\\*(.*)\\*", "$1") // italic (DEP: temp star code) .replace("●", "*") // restore escaped star symbol (DEP: b,i) .replaceAll("(?m) $", "
") // new line (DEP: ul) ; } }; public final static SmpFilter FILTER_WEB = new SmpFilter() { @Override public String filter(String text) { // Don't start new line if 2 empty lines and heading while (text.contains("\n\n#")) { text = text.replace("\n\n#", "\n#"); } text = text .replaceAll("(?s)", "") // HTML comments .replace("\n\n", "\n
\n") // Start new line if 2 empty lines .replaceAll("~°", "  ") // double space/half tab .replaceAll("(?m)^### (.*)$", "

$1

") // h3 .replaceAll("(?m)^## (.*)$", "

$1

") /// h2 (DEP: h3) .replaceAll("(?m)^# (.*)$", "

$1

") // h1 (DEP: h2,h3) .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) .replaceAll("(?m)^[-*] (.*)$", " $1 ") // unordered list + end line .replaceAll("(?m)^ [-*] (.*)$", "   $1 ") // unordered list2 + end line .replaceAll("`([^<]*)`", "$1") // code .replace("\\*", "●") // temporary replace escaped star symbol .replaceAll("(?m)\\*\\*(.*)\\*\\*", "$1") // bold (DEP: temp star) .replaceAll("(?m)\\*(.*)\\*", "$1") // italic (DEP: temp star code) .replace("●", "*") // restore escaped star symbol (DEP: b,i) .replaceAll("(?m) $", "
") // new line (DEP: ul) ; return text; } }; public final static SmpFilter FILTER_CHANGELOG = new SmpFilter() { @Override public String filter(String text) { text = text .replace("New:", "New:") .replace("New features:", "New:") .replace("Added:", "Added:") .replace("Add:", "Add:") .replace("Fixed:", "Fixed:") .replace("Fix:", "Fix:") .replace("Removed:", "Removed:") .replace("Updated:", "Updated:") .replace("Improved:", "Improved:") .replace("Modified:", "Modified:") .replace("Mod:", "Mod:") ; return text; } }; public final static SmpFilter FILTER_H_TO_SUP = new SmpFilter() { @Override public String filter(String text) { text = text .replace("

", "") .replace("

", "") .replace("

", "") .replace("

", "") .replace("

", "") .replace("

", "") ; return text; } }; public final static SmpFilter FILTER_NONE = new SmpFilter() { @Override public String filter(String text) { return text; } }; //######################## //## Singleton //######################## private static SimpleMarkdownParser __instance; public static SimpleMarkdownParser get() { if (__instance == null) { __instance = new SimpleMarkdownParser(); } return __instance; } //######################## //## Members, Constructors //######################## private SmpFilter _defaultSmpFilter; private String _html; public SimpleMarkdownParser() { setDefaultSmpFilter(FILTER_WEB); } //######################## //## Methods //######################## public SimpleMarkdownParser setDefaultSmpFilter(SmpFilter defaultSmpFilter) { _defaultSmpFilter = defaultSmpFilter; return this; } public SimpleMarkdownParser parse(String filepath, SmpFilter... smpFilters) throws IOException { return parse(new FileInputStream(filepath), "", smpFilters); } public SimpleMarkdownParser parse(InputStream inputStream, String lineMdPrefix, SmpFilter... smpFilters) throws IOException { StringBuilder sb = new StringBuilder(); BufferedReader br = null; String line; try { br = new BufferedReader(new InputStreamReader(inputStream)); while ((line = br.readLine()) != null) { sb.append(lineMdPrefix); sb.append(line); sb.append("\n"); } } catch (IOException rethrow) { _html = ""; throw rethrow; } finally { if (br != null) { try { br.close(); } catch (IOException ignored) { } } } _html = parse(sb.toString(), "", smpFilters).getHtml(); return this; } public SimpleMarkdownParser parse(String markdown, String lineMdPrefix, SmpFilter... smpFilters) throws IOException { _html = markdown; if (smpFilters.length == 0) { smpFilters = new SmpFilter[]{_defaultSmpFilter}; } for (SmpFilter smpFilter : smpFilters) { _html = smpFilter.filter(_html).trim(); } return this; } public String getHtml() { return _html; } public SimpleMarkdownParser setHtml(String html) { _html = html; return this; } public SimpleMarkdownParser removeMultiNewlines() { _html = _html.replace("\n", "").replaceAll("(
){3,}", "

"); return this; } public SimpleMarkdownParser replaceBulletCharacter(String replacment) { _html = _html.replace("•", replacment); return this; } public SimpleMarkdownParser replaceColor(String hexColor, int newIntColor) { _html = _html.replace(hexColor, String.format("#%06X", 0xFFFFFF & newIntColor)); return this; } @Override public String toString() { return _html != null ? _html : ""; } }