summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java
diff options
context:
space:
mode:
Diffstat (limited to 'base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java')
-rw-r--r--base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java948
1 files changed, 948 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java b/base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java
new file mode 100644
index 0000000..24965e8
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/MarkdownEditorKit.java
@@ -0,0 +1,948 @@
+package bjc.utils.gui;
+
+import javax.swing.*;
+import javax.swing.text.*;
+
+import java.awt.*;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Basic Markdown -> StyledDocument renderer for Swing.
+ *
+ * Supported:
+ * - Headings #..###
+ * - Bold **...** / __...__
+ * - Italic *...* / _..._
+ * - Inline code `...`
+ * - Fenced code blocks ``` or ~~~
+ * - Blockquotes >
+ * - Lists (unordered and ordered)
+ * - Links [text](url) (href stored in attribute)
+ * - Tables (pipe tables) with inline Markdown inside cells
+ *
+ * Usage:
+ * JTextPane pane = new JTextPane();
+ * pane.setEditorKit(new MarkdownEditorKit());
+ * ((MarkdownDocument)pane.getDocument()).setMarkdown(markdownString);
+ */
+public class MarkdownEditorKit extends StyledEditorKit {
+ private static final long serialVersionUID = 350927457022623776L;
+
+ @Override
+ public String getContentType() {
+ return "text/markdown";
+ }
+
+ @Override
+ public Document createDefaultDocument() {
+ return new MarkdownDocument();
+ }
+
+ /**
+ * Set the Markdown contents of a JTextPane
+ *
+ * @param pane The JTextPane to set the contents of
+ * @param markdown The Markdown to set as the contents
+ */
+ public void setMarkdown(JTextPane pane, String markdown) {
+ if (!(pane.getDocument() instanceof MarkdownDocument)) {
+ pane.setDocument(createDefaultDocument());
+ }
+ ((MarkdownDocument) pane.getDocument()).setMarkdown(markdown);
+ }
+
+ @Override
+ public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
+ String markdown = readAll(in);
+ if (doc instanceof StyledDocument) {
+ MarkdownRenderer.render((StyledDocument) doc, markdown, pos, false);
+ } else {
+ super.read(new StringReader(markdown), doc, pos);
+ }
+ }
+
+ @Override
+ public void write(Writer out, Document doc, int pos, int len) throws IOException, BadLocationException {
+ // Outputs rendered text, not original Markdown.
+ out.write(doc.getText(pos, len));
+ }
+
+ private static String readAll(Reader r) throws IOException {
+ StringBuilder sb = new StringBuilder(4096);
+ char[] buf = new char[4096];
+ int n;
+ while ((n = r.read(buf)) >= 0)
+ sb.append(buf, 0, n);
+ return sb.toString();
+ }
+
+ // -------------------------------------------------------------------------
+ // Document
+ // -------------------------------------------------------------------------
+
+ /**
+ * A instance of {@link StyledDocument} for Markdown
+ */
+ public static class MarkdownDocument extends DefaultStyledDocument {
+ private static final long serialVersionUID = 3602531682908814242L;
+
+ /**
+ * The name for the source property
+ */
+ public static final String PROP_SOURCE_MARKDOWN = "markdown.source";
+ /**
+ * The name for the Link attribute
+ */
+ public static final String ATTR_LINK_HREF = "markdown.link.href";
+
+ // Base / inline
+ final SimpleAttributeSet base = new SimpleAttributeSet();
+ final SimpleAttributeSet bold = new SimpleAttributeSet();
+ final SimpleAttributeSet italic = new SimpleAttributeSet();
+ final SimpleAttributeSet code = new SimpleAttributeSet();
+ final SimpleAttributeSet link = new SimpleAttributeSet();
+
+ // Headings
+ final SimpleAttributeSet h1 = new SimpleAttributeSet();
+ final SimpleAttributeSet h2 = new SimpleAttributeSet();
+ final SimpleAttributeSet h3 = new SimpleAttributeSet();
+
+ // Blockquote
+ final SimpleAttributeSet blockquoteText = new SimpleAttributeSet();
+ final SimpleAttributeSet blockquotePara = new SimpleAttributeSet();
+
+ // List
+ final SimpleAttributeSet listPara = new SimpleAttributeSet();
+
+ // Code block
+ final SimpleAttributeSet codeBlockText = new SimpleAttributeSet();
+ final SimpleAttributeSet codeBlockPara = new SimpleAttributeSet();
+
+ // Table
+ final SimpleAttributeSet tableText = new SimpleAttributeSet();
+ final SimpleAttributeSet tableHeaderText = new SimpleAttributeSet();
+ final SimpleAttributeSet tablePara = new SimpleAttributeSet();
+
+ /**
+ * Create a new Markdown document
+ */
+ public MarkdownDocument() {
+ super();
+
+ StyleConstants.setFontSize(base, 13);
+
+ StyleConstants.setBold(bold, true);
+ StyleConstants.setItalic(italic, true);
+
+ StyleConstants.setFontFamily(code, "Monospaced");
+ StyleConstants.setBackground(code, new Color(245, 245, 245));
+
+ StyleConstants.setForeground(link, new Color(0, 102, 204));
+ StyleConstants.setUnderline(link, true);
+
+ StyleConstants.setBold(h1, true);
+ StyleConstants.setFontSize(h1, 22);
+
+ StyleConstants.setBold(h2, true);
+ StyleConstants.setFontSize(h2, 18);
+
+ StyleConstants.setBold(h3, true);
+ StyleConstants.setFontSize(h3, 15);
+
+ StyleConstants.setForeground(blockquoteText, new Color(90, 90, 90));
+ StyleConstants.setItalic(blockquoteText, true);
+
+ StyleConstants.setLeftIndent(blockquotePara, 18f);
+ StyleConstants.setSpaceAbove(blockquotePara, 3f);
+ StyleConstants.setSpaceBelow(blockquotePara, 3f);
+
+ StyleConstants.setLeftIndent(listPara, 18f);
+ StyleConstants.setFirstLineIndent(listPara, -12f);
+
+ StyleConstants.setFontFamily(codeBlockText, "Monospaced");
+ StyleConstants.setBackground(codeBlockText, new Color(245, 245, 245));
+
+ StyleConstants.setLeftIndent(codeBlockPara, 18f);
+ StyleConstants.setSpaceAbove(codeBlockPara, 6f);
+ StyleConstants.setSpaceBelow(codeBlockPara, 6f);
+
+ // Tables: render like a code-ish block
+ StyleConstants.setFontFamily(tableText, "Monospaced");
+ StyleConstants.setBackground(tableText, new Color(245, 245, 245));
+
+ tableHeaderText.addAttributes(tableText);
+ StyleConstants.setBold(tableHeaderText, true);
+
+ StyleConstants.setLeftIndent(tablePara, 18f);
+ StyleConstants.setSpaceAbove(tablePara, 6f);
+ StyleConstants.setSpaceBelow(tablePara, 6f);
+ }
+
+ /**
+ * Set the contents of this document
+ *
+ * @param markdown The markdown to render
+ */
+ public void setMarkdown(String markdown) {
+ putProperty(PROP_SOURCE_MARKDOWN, markdown);
+ try {
+ MarkdownRenderer.render(this, markdown, 0, true);
+ } catch (BadLocationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Renderer
+ // -------------------------------------------------------------------------
+
+ static final class MarkdownRenderer {
+
+ static void render(StyledDocument doc, String markdown, int argInsertPos, boolean clear)
+ throws BadLocationException {
+
+ int insertPos = argInsertPos;
+ if (clear) {
+ doc.remove(0, doc.getLength());
+ insertPos = 0;
+ }
+
+ Attr a = Attr.from(doc);
+
+ String[] lines = markdown.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1);
+
+ int offset = insertPos;
+ boolean inFence = false;
+ String fenceDelim = null;
+
+ for (int li = 0; li < lines.length; li++) {
+ String line = lines[li];
+
+ // Fenced code blocks
+ String trimmed = line.trim();
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
+ String delim = trimmed.substring(0, 3);
+ if (!inFence) {
+ inFence = true;
+ fenceDelim = delim;
+ } else if (delim.equals(fenceDelim)) {
+ inFence = false;
+ fenceDelim = null;
+ if (li != lines.length - 1) {
+ offset = insert(doc, offset, "\n", a.base);
+ }
+ }
+ continue;
+ }
+
+ if (inFence) {
+ int start = offset;
+ offset = insert(doc, offset, line, a.codeBlockText);
+ offset = insert(doc, offset, "\n", a.codeBlockText);
+ applyParagraph(doc, start, offset - start, a.codeBlockPara);
+ continue;
+ }
+
+ // Blank line
+ if (isBlank(line)) {
+ offset = insert(doc, offset, "\n", a.base);
+ continue;
+ }
+
+ // Tables: detect header + divider
+ if (li + 1 < lines.length) {
+ TableHeader th = TableHeader.tryParse(line, lines[li + 1]);
+ if (th != null) {
+ // Collect rows
+ List<List<String>> rows = new ArrayList<>();
+ rows.add(th.headerCells);
+
+ int r = li + 2;
+ while (r < lines.length && !isBlank(lines[r])) {
+ List<String> cells = splitPipeRow(lines[r]);
+ if (cells == null || cells.size() != th.colCount)
+ break;
+ rows.add(cells);
+ r++;
+ }
+
+ int start = offset;
+ offset = renderTable(doc, offset, rows, a);
+ applyParagraph(doc, start, offset - start, a.tablePara);
+
+ li = r - 1;
+ continue;
+ }
+ }
+
+ // Headings #, ##, ###
+ Heading h = Heading.parse(line);
+ if (h != null) {
+ AttributeSet hStyle;
+ if (h.level == 1)
+ hStyle = a.h1;
+ else if (h.level == 2)
+ hStyle = a.h2;
+ else
+ hStyle = a.h3;
+
+ int start = offset;
+ offset = renderInline(doc, offset, h.text, a, hStyle);
+ offset = insert(doc, offset, "\n", a.base);
+
+ SimpleAttributeSet para = new SimpleAttributeSet();
+ StyleConstants.setSpaceAbove(para, 6f);
+ StyleConstants.setSpaceBelow(para, 4f);
+ applyParagraph(doc, start, offset - start, para);
+ continue;
+ }
+
+ // Blockquote
+ if (line.startsWith(">")) {
+ String q = line;
+ while (q.startsWith(">"))
+ q = q.substring(1);
+ if (q.startsWith(" "))
+ q = q.substring(1);
+
+ int start = offset;
+ offset = insert(doc, offset, "▌ ", a.blockquoteText);
+ offset = renderInline(doc, offset, q, a, a.blockquoteText);
+ offset = insert(doc, offset, "\n", a.base);
+ applyParagraph(doc, start, offset - start, a.blockquotePara);
+ continue;
+ }
+
+ // Lists
+ ListItem item = ListItem.parse(line);
+ if (item != null) {
+ int start = offset;
+ offset = insert(doc, offset, item.prefix, a.base);
+ offset = renderInline(doc, offset, item.text, a, a.base);
+ offset = insert(doc, offset, "\n", a.base);
+ applyParagraph(doc, start, offset - start, a.listPara);
+ continue;
+ }
+
+ // Normal line = paragraph
+ int start = offset;
+ offset = renderInline(doc, offset, line, a, a.base);
+ offset = insert(doc, offset, "\n", a.base);
+
+ SimpleAttributeSet para = new SimpleAttributeSet();
+ StyleConstants.setSpaceBelow(para, 2f);
+ applyParagraph(doc, start, offset - start, para);
+ }
+ }
+
+ // --- table rendering with inline markdown inside cells ---
+
+ private static int renderTable(StyledDocument doc, int argOffset, List<List<String>> rows, Attr a)
+ throws BadLocationException {
+
+ int offset = argOffset;
+ int cols = rows.get(0).size();
+ int[] widths = new int[cols];
+
+ // compute widths based on visible (rendered) length
+ for (List<String> row : rows) {
+ for (int c = 0; c < cols; c++) {
+ String cell = safeCell(row, c);
+ int v = visibleLen(cell);
+ if (v > widths[c])
+ widths[c] = v;
+ }
+ }
+ for (int c = 0; c < cols; c++) {
+ if (widths[c] < 3)
+ widths[c] = 3;
+ }
+
+ // header row
+ offset = renderTableRow(doc, offset, rows.get(0), widths, a, a.tableHeaderText);
+
+ // separator row (plain dashes)
+ offset = renderTableSeparator(doc, offset, widths, a);
+
+ // body rows
+ for (int r = 1; r < rows.size(); r++) {
+ offset = renderTableRow(doc, offset, rows.get(r), widths, a, a.tableText);
+ }
+
+ return offset;
+ }
+
+ private static int renderTableRow(StyledDocument doc, int argOffset, List<String> cells, int[] widths, Attr a,
+ AttributeSet rowStyle) throws BadLocationException {
+
+ int offset = argOffset;
+ offset = insert(doc, offset, "|", rowStyle);
+
+ for (int c = 0; c < widths.length; c++) {
+ String cell = safeCell(cells, c);
+
+ offset = insert(doc, offset, " ", rowStyle);
+
+ // Render inline markdown inside the cell using the table rowStyle as the base.
+ offset = renderInline(doc, offset, cell, a, rowStyle);
+
+ int pad = widths[c] - visibleLen(cell);
+ if (pad > 0) {
+ offset = insert(doc, offset, repeat(' ', pad), rowStyle);
+ }
+
+ offset = insert(doc, offset, " |", rowStyle);
+ }
+
+ offset = insert(doc, offset, "\n", rowStyle);
+ return offset;
+ }
+
+ private static int renderTableSeparator(StyledDocument doc, int argOffset, int[] widths, Attr a)
+ throws BadLocationException {
+ int offset = argOffset;
+
+ AttributeSet s = a.tableText;
+
+ offset = insert(doc, offset, "|", s);
+ for (int c = 0; c < widths.length; c++) {
+ offset = insert(doc, offset, " ", s);
+ offset = insert(doc, offset, repeat('-', widths[c]), s);
+ offset = insert(doc, offset, " |", s);
+ }
+ offset = insert(doc, offset, "\n", s);
+ return offset;
+ }
+
+ private static String safeCell(List<String> row, int idx) {
+ if (row == null)
+ return "";
+ if (idx < 0 || idx >= row.size())
+ return "";
+ return row.get(idx);
+ }
+
+ /**
+ * Splits a pipe table row into cells. - Allows optional leading/trailing pipes.
+ * - Does NOT split on escaped pipes (\|). - Does NOT split on pipes inside
+ * inline code spans (`...`).
+ */
+ private static List<String> splitPipeRow(String line) {
+ if (line == null)
+ return null;
+ if (line.indexOf('|') < 0)
+ return null;
+
+ String s = line.trim();
+
+ if (s.startsWith("|"))
+ s = s.substring(1);
+ if (s.endsWith("|"))
+ s = s.substring(0, s.length() - 1);
+
+ List<String> cells = new ArrayList<>();
+ StringBuilder cell = new StringBuilder();
+
+ boolean escaped = false;
+ boolean inCode = false;
+
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+
+ if (escaped) {
+ cell.append(ch);
+ escaped = false;
+ continue;
+ }
+
+ if (ch == '\\') {
+ escaped = true;
+ continue;
+ }
+
+ if (ch == '`') {
+ inCode = !inCode;
+ cell.append(ch); // keep backticks so inline parsing still sees them
+ continue;
+ }
+
+ if (ch == '|' && !inCode) {
+ cells.add(cell.toString().trim());
+ cell.setLength(0);
+ continue;
+ }
+
+ cell.append(ch);
+ }
+
+ if (escaped)
+ cell.append('\\');
+ cells.add(cell.toString().trim());
+
+ return cells;
+ }
+
+ // --- visible length for table width computation (basic markdown stripping) ---
+
+ private static int visibleLen(String text) {
+ if (text == null || text.isEmpty())
+ return 0;
+
+ int i = 0;
+ int len = 0;
+
+ while (i < text.length()) {
+ char c = text.charAt(i);
+
+ // escape: \x counts as x
+ if (c == '\\' && i + 1 < text.length()) {
+ len += 1;
+ i += 2;
+ continue;
+ }
+
+ // code span: `...`
+ if (c == '`') {
+ int end = findNext(text, i + 1, '`');
+ if (end > i + 1) {
+ len += (end - (i + 1));
+ i = end + 1;
+ continue;
+ }
+ }
+
+ // link: [label](url) => count label
+ if (c == '[') {
+ int closeBracket = text.indexOf(']', i + 1);
+ if (closeBracket > i + 1 && closeBracket + 1 < text.length()
+ && text.charAt(closeBracket + 1) == '(') {
+ int closeParen = text.indexOf(')', closeBracket + 2);
+ if (closeParen > closeBracket + 2) {
+ String label = text.substring(i + 1, closeBracket);
+ len += visibleLen(label);
+ i = closeParen + 1;
+ continue;
+ }
+ }
+ }
+
+ // bold: **...** or __...__
+ if (startsWith(text, i, "**") || startsWith(text, i, "__")) {
+ String delim = text.substring(i, i + 2);
+ int end = text.indexOf(delim, i + 2);
+ if (end > i + 2) {
+ String inner = text.substring(i + 2, end);
+ len += visibleLen(inner);
+ i = end + 2;
+ continue;
+ }
+ }
+
+ // italic: *...* or _..._
+ if (c == '*' || c == '_') {
+ int end = text.indexOf(c, i + 1);
+ if (end > i + 1) {
+ String inner = text.substring(i + 1, end);
+ if (!isBlank(inner)) {
+ len += visibleLen(inner);
+ i = end + 1;
+ continue;
+ }
+ }
+ }
+
+ len += 1;
+ i += 1;
+ }
+
+ return len;
+ }
+
+ private static int findNext(String s, int start, char needle) {
+ // Finds next unescaped needle.
+ boolean escaped = false;
+ for (int i = start; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (escaped) {
+ escaped = false;
+ continue;
+ }
+ if (c == '\\') {
+ escaped = true;
+ continue;
+ }
+ if (c == needle)
+ return i;
+ }
+ return -1;
+ }
+
+ // --- inline rendering ---
+
+ private static int insert(StyledDocument doc, int offset, String s, AttributeSet attrs)
+ throws BadLocationException {
+ doc.insertString(offset, s, attrs);
+ return offset + s.length();
+ }
+
+ private static void applyParagraph(StyledDocument doc, int start, int length, AttributeSet paraAttrs) {
+ doc.setParagraphAttributes(start, Math.max(1, length), paraAttrs, false);
+ }
+
+ /**
+ * Render inline Markdown inside a single line. Priority: - code spans `...` -
+ * links [text](url) - bold **...** or __...__ - italic *...* or _..._
+ */
+ private static int renderInline(StyledDocument doc, int argOffset, String text, Attr a, AttributeSet baseStyle)
+ throws BadLocationException {
+ int offset = argOffset;
+
+ if (text == null || text.isEmpty())
+ return offset;
+
+ int i = 0;
+ StringBuilder plain = new StringBuilder();
+
+ while (i < text.length()) {
+ char c = text.charAt(i);
+
+ // Escape
+ if (c == '\\' && i + 1 < text.length()) {
+ plain.append(text.charAt(i + 1));
+ i += 2;
+ continue;
+ }
+
+ // Code span
+ if (c == '`') {
+ int end = text.indexOf('`', i + 1);
+ if (end > i + 1) {
+ offset = flushPlain(doc, offset, plain, baseStyle);
+ String code = text.substring(i + 1, end);
+ offset = insert(doc, offset, code, merge(baseStyle, a.code));
+ i = end + 1;
+ continue;
+ }
+ }
+
+ // Link [text](url)
+ if (c == '[') {
+ int closeBracket = text.indexOf(']', i + 1);
+ if (closeBracket > i + 1 && closeBracket + 1 < text.length()
+ && text.charAt(closeBracket + 1) == '(') {
+ int closeParen = text.indexOf(')', closeBracket + 2);
+ if (closeParen > closeBracket + 2) {
+ String label = text.substring(i + 1, closeBracket);
+ String href = text.substring(closeBracket + 2, closeParen);
+
+ offset = flushPlain(doc, offset, plain, baseStyle);
+
+ SimpleAttributeSet linkAttrs = new SimpleAttributeSet();
+ linkAttrs.addAttributes(baseStyle);
+ linkAttrs.addAttributes(a.link);
+ linkAttrs.addAttribute(MarkdownDocument.ATTR_LINK_HREF, href);
+
+ offset = insert(doc, offset, label, linkAttrs);
+ i = closeParen + 1;
+ continue;
+ }
+ }
+ }
+
+ // Bold **...** or __...__
+ if (startsWith(text, i, "**") || startsWith(text, i, "__")) {
+ String delim = text.substring(i, i + 2);
+ int end = text.indexOf(delim, i + 2);
+ if (end > i + 2) {
+ offset = flushPlain(doc, offset, plain, baseStyle);
+ String content = text.substring(i + 2, end);
+ offset = insert(doc, offset, content, merge(baseStyle, a.bold));
+ i = end + 2;
+ continue;
+ }
+ }
+
+ // Italic *...* or _..._
+ if (c == '*' || c == '_') {
+ int end = text.indexOf(c, i + 1);
+ if (end > i + 1) {
+ String content = text.substring(i + 1, end);
+ if (!isBlank(content)) {
+ offset = flushPlain(doc, offset, plain, baseStyle);
+ offset = insert(doc, offset, content, merge(baseStyle, a.italic));
+ i = end + 1;
+ continue;
+ }
+ }
+ }
+
+ // Default: plain
+ plain.append(c);
+ i++;
+ }
+
+ return flushPlain(doc, offset, plain, baseStyle);
+ }
+
+ private static int flushPlain(StyledDocument doc, int argOffset, StringBuilder plain, AttributeSet baseStyle)
+ throws BadLocationException {
+ int offset = argOffset;
+
+ if (plain.length() == 0)
+ return offset;
+ offset = insert(doc, offset, plain.toString(), baseStyle);
+ plain.setLength(0);
+ return offset;
+ }
+
+ private static boolean startsWith(String s, int i, String prefix) {
+ return i + prefix.length() <= s.length() && s.regionMatches(i, prefix, 0, prefix.length());
+ }
+
+ private static AttributeSet merge(AttributeSet base, AttributeSet extra) {
+ SimpleAttributeSet merged = new SimpleAttributeSet();
+ merged.addAttributes(base);
+ merged.addAttributes(extra);
+ return merged;
+ }
+
+ // --- line parsers / helpers ---
+
+ private static boolean isBlank(String s) {
+ if (s == null || s.isEmpty())
+ return true;
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i)))
+ return false;
+ }
+ return true;
+ }
+
+ private static String repeat(char ch, int count) {
+ if (count <= 0)
+ return "";
+ StringBuilder sb = new StringBuilder(count);
+ for (int i = 0; i < count; i++)
+ sb.append(ch);
+ return sb.toString();
+ }
+
+ private static String stripLeading(String s) {
+ if (s == null)
+ return "";
+ int i = 0;
+ while (i < s.length() && Character.isWhitespace(s.charAt(i)))
+ i++;
+ return s.substring(i);
+ }
+
+ private static final class Heading {
+ final int level;
+ final String text;
+
+ Heading(int level, String text) {
+ this.level = level;
+ this.text = text;
+ }
+
+ static Heading parse(String line) {
+ if (line == null || line.isEmpty())
+ return null;
+ int i = 0;
+ while (i < line.length() && line.charAt(i) == '#')
+ i++;
+ if (i == 0 || i > 6)
+ return null;
+ if (i < line.length() && line.charAt(i) == ' ') {
+ String t = line.substring(i + 1);
+ int level = i;
+ if (level > 3)
+ level = 3; // cap for styling
+ return new Heading(level, t);
+ }
+ return null;
+ }
+ }
+
+ private static final class ListItem {
+ final String prefix;
+ final String text;
+
+ ListItem(String prefix, String text) {
+ this.prefix = prefix;
+ this.text = text;
+ }
+
+ static ListItem parse(String line) {
+ String s = stripLeading(line);
+
+ if (s.startsWith("- ") || s.startsWith("* ") || s.startsWith("+ ")) {
+ return new ListItem("• ", s.substring(2));
+ }
+
+ int n = 0;
+ while (n < s.length() && Character.isDigit(s.charAt(n)))
+ n++;
+ if (n > 0 && n + 1 < s.length()) {
+ char sep = s.charAt(n);
+ if ((sep == '.' || sep == ')') && s.charAt(n + 1) == ' ') {
+ String num = s.substring(0, n);
+ return new ListItem(num + sep + " ", s.substring(n + 2));
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private static final class TableHeader {
+ final List<String> headerCells;
+ final int colCount;
+
+ TableHeader(List<String> headerCells, int colCount) {
+ this.headerCells = headerCells;
+ this.colCount = colCount;
+ }
+
+ static TableHeader tryParse(String headerLine, String dividerLine) {
+ List<String> header = splitPipeRow(headerLine);
+ if (header == null || header.isEmpty())
+ return null;
+
+ List<String> div = splitPipeRow(dividerLine);
+ if (div == null || div.size() != header.size())
+ return null;
+
+ for (int i = 0; i < div.size(); i++) {
+ String d = div.get(i).replace(" ", "");
+ // allow :---:, ---:, :---, ---
+ if (!d.matches(":?-{3,}:?"))
+ return null;
+ }
+
+ return new TableHeader(header, header.size());
+ }
+ }
+
+ private static final class Attr {
+ final SimpleAttributeSet base;
+ final SimpleAttributeSet bold;
+ final SimpleAttributeSet italic;
+ final SimpleAttributeSet code;
+ final SimpleAttributeSet link;
+
+ final SimpleAttributeSet h1;
+ final SimpleAttributeSet h2;
+ final SimpleAttributeSet h3;
+
+ final SimpleAttributeSet blockquoteText;
+ final SimpleAttributeSet blockquotePara;
+
+ final SimpleAttributeSet listPara;
+
+ final SimpleAttributeSet codeBlockText;
+ final SimpleAttributeSet codeBlockPara;
+
+ final SimpleAttributeSet tableText;
+ final SimpleAttributeSet tableHeaderText;
+ final SimpleAttributeSet tablePara;
+
+ Attr(SimpleAttributeSet base, SimpleAttributeSet bold, SimpleAttributeSet italic, SimpleAttributeSet code,
+ SimpleAttributeSet link, SimpleAttributeSet h1, SimpleAttributeSet h2, SimpleAttributeSet h3,
+ SimpleAttributeSet blockquoteText, SimpleAttributeSet blockquotePara, SimpleAttributeSet listPara,
+ SimpleAttributeSet codeBlockText, SimpleAttributeSet codeBlockPara, SimpleAttributeSet tableText,
+ SimpleAttributeSet tableHeaderText, SimpleAttributeSet tablePara) {
+ this.base = base;
+ this.bold = bold;
+ this.italic = italic;
+ this.code = code;
+ this.link = link;
+ this.h1 = h1;
+ this.h2 = h2;
+ this.h3 = h3;
+ this.blockquoteText = blockquoteText;
+ this.blockquotePara = blockquotePara;
+ this.listPara = listPara;
+ this.codeBlockText = codeBlockText;
+ this.codeBlockPara = codeBlockPara;
+ this.tableText = tableText;
+ this.tableHeaderText = tableHeaderText;
+ this.tablePara = tablePara;
+ }
+
+ static Attr from(StyledDocument doc) {
+ if (doc instanceof MarkdownDocument) {
+ MarkdownDocument md = (MarkdownDocument) doc;
+ return new Attr(md.base, md.bold, md.italic, md.code, md.link, md.h1, md.h2, md.h3,
+ md.blockquoteText, md.blockquotePara, md.listPara, md.codeBlockText, md.codeBlockPara,
+ md.tableText, md.tableHeaderText, md.tablePara);
+ }
+
+ // fallback defaults
+ SimpleAttributeSet base = new SimpleAttributeSet();
+ StyleConstants.setFontSize(base, 13);
+
+ SimpleAttributeSet bold = new SimpleAttributeSet();
+ StyleConstants.setBold(bold, true);
+
+ SimpleAttributeSet italic = new SimpleAttributeSet();
+ StyleConstants.setItalic(italic, true);
+
+ SimpleAttributeSet code = new SimpleAttributeSet();
+ StyleConstants.setFontFamily(code, "Monospaced");
+ StyleConstants.setBackground(code, new Color(245, 245, 245));
+
+ SimpleAttributeSet link = new SimpleAttributeSet();
+ StyleConstants.setForeground(link, new Color(0, 102, 204));
+ StyleConstants.setUnderline(link, true);
+
+ SimpleAttributeSet h1 = new SimpleAttributeSet();
+ StyleConstants.setBold(h1, true);
+ StyleConstants.setFontSize(h1, 22);
+
+ SimpleAttributeSet h2 = new SimpleAttributeSet();
+ StyleConstants.setBold(h2, true);
+ StyleConstants.setFontSize(h2, 18);
+
+ SimpleAttributeSet h3 = new SimpleAttributeSet();
+ StyleConstants.setBold(h3, true);
+ StyleConstants.setFontSize(h3, 15);
+
+ SimpleAttributeSet bqt = new SimpleAttributeSet();
+ StyleConstants.setForeground(bqt, new Color(90, 90, 90));
+ StyleConstants.setItalic(bqt, true);
+
+ SimpleAttributeSet bqPara = new SimpleAttributeSet();
+ StyleConstants.setLeftIndent(bqPara, 18f);
+
+ SimpleAttributeSet listPara = new SimpleAttributeSet();
+ StyleConstants.setLeftIndent(listPara, 18f);
+ StyleConstants.setFirstLineIndent(listPara, -12f);
+
+ SimpleAttributeSet cbText = new SimpleAttributeSet();
+ StyleConstants.setFontFamily(cbText, "Monospaced");
+ StyleConstants.setBackground(cbText, new Color(245, 245, 245));
+
+ SimpleAttributeSet cbPara = new SimpleAttributeSet();
+ StyleConstants.setLeftIndent(cbPara, 18f);
+
+ SimpleAttributeSet tableText = new SimpleAttributeSet();
+ StyleConstants.setFontFamily(tableText, "Monospaced");
+ StyleConstants.setBackground(tableText, new Color(245, 245, 245));
+
+ SimpleAttributeSet tableHeader = new SimpleAttributeSet();
+ tableHeader.addAttributes(tableText);
+ StyleConstants.setBold(tableHeader, true);
+
+ SimpleAttributeSet tablePara = new SimpleAttributeSet();
+ StyleConstants.setLeftIndent(tablePara, 18f);
+
+ return new Attr(base, bold, italic, code, link, h1, h2, h3, bqt, bqPara, listPara, cbText, cbPara,
+ tableText, tableHeader, tablePara);
+ }
+ }
+ }
+} \ No newline at end of file