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> rows = new ArrayList<>(); rows.add(th.headerCells); int r = li + 2; while (r < lines.length && !isBlank(lines[r])) { List 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> 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 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 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 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 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 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 headerCells; final int colCount; TableHeader(List headerCells, int colCount) { this.headerCells = headerCells; this.colCount = colCount; } static TableHeader tryParse(String headerLine, String dividerLine) { List header = splitPipeRow(headerLine); if (header == null || header.isEmpty()) return null; List 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); } } } }