true if this block contains lines.
+ */
+ public boolean hasLines()
+ {
+ return this.lines != null;
+ }
+
+ /**
+ * Removes leading and trailing empty lines.
+ */
+ public void removeSurroundingEmptyLines()
+ {
+ if(this.lines != null)
+ {
+ this.removeTrailingEmptyLines();
+ this.removeLeadingEmptyLines();
+ }
+ }
+
+ /**
+ * Sets hlDepth and takes care of '#' chars.
+ */
+ public void transfromHeadline()
+ {
+ if(this.hlDepth > 0)
+ return;
+ int level = 0;
+ final Line line = this.lines;
+ if(line.isEmpty)
+ return;
+ int start = line.leading;
+ while(start < line.value.length() && line.value.charAt(start) == '#')
+ {
+ level++;
+ start++;
+ }
+ while(start < line.value.length() && line.value.charAt(start) == ' ')
+ start++;
+ if(start >= line.value.length())
+ {
+ line.setEmpty();
+ }
+ else
+ {
+ int end = line.value.length() - line.trailing - 1;
+ while(line.value.charAt(end) == '#')
+ end--;
+ while(line.value.charAt(end) == ' ')
+ end--;
+ line.value = line.value.substring(start, end + 1);
+ line.leading = line.trailing = 0;
+ }
+ this.hlDepth = Math.min(level, 6);
+ }
+
+ /**
+ * Used for nested lists. Removes list markers and up to 4 leading spaces.
+ */
+ public void removeListIndent()
+ {
+ Line line = this.lines;
+ while(line != null)
+ {
+ if(!line.isEmpty)
+ {
+ switch(line.getLineType())
+ {
+ case ULIST:
+ line.value = line.value.substring(line.leading + 2);
+ break;
+ case OLIST:
+ line.value = line.value.substring(line.value.indexOf('.') + 2);
+ break;
+ default:
+ line.value = line.value.substring(Math.min(line.leading, 4));
+ break;
+ }
+ line.initLeading();
+ }
+ line = line.next;
+ }
+ }
+
+ /**
+ * Used for nested block quotes. Removes '>' char.
+ */
+ public void removeBlockQuotePrefix()
+ {
+ Line line = this.lines;
+ while(line != null)
+ {
+ if(!line.isEmpty)
+ {
+ if(line.value.charAt(line.leading) == '>')
+ {
+ int rem = line.leading + 1;
+ if(line.leading + 1 < line.value.length() && line.value.charAt(line.leading + 1) == ' ')
+ rem++;
+ line.value = line.value.substring(rem);
+ line.initLeading();
+ }
+ }
+ line = line.next;
+ }
+ }
+
+ /**
+ * Removes leading empty lines.
+ * @return true if an empty line was removed.
+ */
+ public boolean removeLeadingEmptyLines()
+ {
+ boolean wasEmpty = false;
+ Line line = this.lines;
+ while(line != null && line.isEmpty)
+ {
+ this.removeLine(line);
+ line = this.lines;
+ wasEmpty = true;
+ }
+ return wasEmpty;
+ }
+
+ /**
+ * Removes trailing empty lines.
+ */
+ public void removeTrailingEmptyLines()
+ {
+ Line line = this.lineTail;
+ while(line != null && line.isEmpty)
+ {
+ this.removeLine(line);
+ line = this.lineTail;
+ }
+ }
+
+ /**
+ * Splits this block's lines, creating a new child block having 'line' as it's lineTail.
+ * @param line The line to split from.
+ * @return The newly created Block.
+ */
+ public Block split(final Line line)
+ {
+ final Block block = new Block();
+
+ block.lines = this.lines;
+ block.lineTail = line;
+ this.lines = line.next;
+ line.next = null;
+ if(this.lines == null)
+ this.lineTail = null;
+ else
+ this.lines.previous = null;
+
+ if(this.blocks == null)
+ this.blocks = this.blockTail = block;
+ else
+ {
+ this.blockTail.next = block;
+ this.blockTail = block;
+ }
+
+ return block;
+ }
+
+ /**
+ * Removes the given line from this block.
+ *
+ * @param line Line to remove.
+ */
+ public void removeLine(final Line line)
+ {
+ if(line.previous == null)
+ this.lines = line.next;
+ else
+ line.previous.next = line.next;
+ if(line.next == null)
+ this.lineTail = line.previous;
+ else
+ line.next.previous = line.previous;
+ line.previous = line.next = null;
+ }
+
+ /**
+ * Appends the given line to this block.
+ *
+ * @param line Line to append.
+ */
+ public void appendLine(final Line line)
+ {
+ if(this.lineTail == null)
+ this.lines = this.lineTail = line;
+ else
+ {
+ this.lineTail.nextEmpty = line.isEmpty;
+ line.prevEmpty = this.lineTail.isEmpty;
+ line.previous = this.lineTail;
+ this.lineTail.next = line;
+ this.lineTail = line;
+ }
+ }
+
+ /**
+ * Changes all Blocks of type NONE to PARAGRAPH if this Block
+ * is a List and any of the ListItems contains a paragraph.
+ */
+ public void expandListParagraphs()
+ {
+ if(this.type != BlockType.ORDERED_LIST && this.type != BlockType.UNORDERED_LIST)
+ {
+ return;
+ }
+ Block outer = this.blocks, inner;
+ boolean hasParagraph = false;
+ while(outer != null && !hasParagraph)
+ {
+ if(outer.type == BlockType.LIST_ITEM)
+ {
+ inner = outer.blocks;
+ while(inner != null && !hasParagraph)
+ {
+ if(inner.type == BlockType.PARAGRAPH)
+ {
+ hasParagraph = true;
+ }
+ inner = inner.next;
+ }
+ }
+ outer = outer.next;
+ }
+ if(hasParagraph)
+ {
+ outer = this.blocks;
+ while(outer != null)
+ {
+ if(outer.type == BlockType.LIST_ITEM)
+ {
+ inner = outer.blocks;
+ while(inner != null)
+ {
+ if(inner.type == BlockType.NONE)
+ {
+ inner.type = BlockType.PARAGRAPH;
+ }
+ inner = inner.next;
+ }
+ }
+ outer = outer.next;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/github/rjeschke/txtmark/BlockEmitter.java b/src/main/java/com/github/rjeschke/txtmark/BlockEmitter.java
new file mode 100644
index 0000000..6da1dd6
--- /dev/null
+++ b/src/main/java/com/github/rjeschke/txtmark/BlockEmitter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2011 René Jeschke
+ * public void emitBlock(StringBuilder out, List<String> lines)
+ * {
+ * for(final String s : lines)
+ * {
+ * for(int i = 0; i < s.length(); i++)
+ * {
+ * final char c = s.charAt(i);
+ * switch(c)
+ * {
+ * case '&':
+ * out.append("&");
+ * break;
+ * case '<':
+ * out.append("<");
+ * break;
+ * case '>':
+ * out.append(">");
+ * break;
+ * default:
+ * out.append(c);
+ * break;
+ * }
+ * }
+ * out.append('\n');
+ * }
+ * }
+ *
+ *
+ *
+ *
+ * @author René Jeschke
+ * This is the default configuration for txtmark's process
+ * methods
+ *
safeMode = falseencoding = UTF-8decorator = DefaultDecoratorcodeBlockEmitter = null+ * Default safe configuration + *
+ * + *safeMode = trueencoding = UTF-8decorator = DefaultDecoratorcodeBlockEmitter = nullfalse
+ *
+ * @return This builder
+ * @since 0.7
+ */
+ public Builder enableSafeMode()
+ {
+ this.safeMode = true;
+ return this;
+ }
+
+ /**
+ * Sets the HTML safe mode flag.
+ *
+ * Default: false
+ *
+ * @param flag
+ * true to enable safe mode
+ * @return This builder
+ * @since 0.7
+ */
+ public Builder setSafeMode(boolean flag)
+ {
+ this.safeMode = flag;
+ return this;
+ }
+
+ /**
+ * Sets the character encoding for txtmark.
+ *
+ * Default: "UTF-8"
+ *
+ * @param encoding
+ * The encoding
+ * @return This builder
+ * @since 0.7
+ */
+ public Builder setEncoding(String encoding)
+ {
+ this.encoding = encoding;
+ return this;
+ }
+
+ /**
+ * Sets the decorator for txtmark.
+ *
+ * Default: DefaultDecorator()
+ *
+ * @param decorator
+ * The decorator
+ * @return This builder
+ * @see DefaultDecorator
+ * @since 0.7
+ */
+ public Builder setDecorator(Decorator decorator)
+ {
+ this.decorator = decorator;
+ return this;
+ }
+
+ /**
+ * Sets the code block emitter.
+ *
+ * Default: null
+ *
+ * @param emitter
+ * The BlockEmitter
+ * @return This builder
+ * @see BlockEmitter
+ * @since 0.7
+ */
+ public Builder setCodeBlockEmitter(BlockEmitter emitter)
+ {
+ this.codeBlockEmitter = emitter;
+ return this;
+ }
+
+ /**
+ * Builds a configuration instance.
+ *
+ * @return a Configuration instance
+ * @since 0.7
+ */
+ public Configuration build()
+ {
+ return new Configuration(this.safeMode, this.encoding, this.decorator, this.codeBlockEmitter);
+ }
+ }
+}
diff --git a/src/main/java/com/github/rjeschke/txtmark/Decorator.java b/src/main/java/com/github/rjeschke/txtmark/Decorator.java
new file mode 100644
index 0000000..a5d6fe0
--- /dev/null
+++ b/src/main/java/com/github/rjeschke/txtmark/Decorator.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2011 René Jeschke Default implementation is:
+ *out.append("<p>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openParagraph(final StringBuilder out);
+
+ /**
+ * Called when a paragraph is closed.
+ *
+ * Default implementation is:
+ *out.append("</p>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeParagraph(final StringBuilder out);
+
+ /**
+ * Called when a blockquote is opened.
+ *
+ * Default implementation is:
+ * out.append("<blockquote>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openBlockquote(final StringBuilder out);
+
+ /**
+ * Called when a blockquote is closed.
+ *
+ * Default implementation is:
+ *out.append("</blockquote>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeBlockquote(final StringBuilder out);
+
+ /**
+ * Called when a code block is opened.
+ *
+ * Default implementation is:
+ *out.append("<pre><code>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openCodeBlock(final StringBuilder out);
+
+ /**
+ * Called when a code block is closed.
+ *
+ * Default implementation is:
+ *out.append("</code></pre>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeCodeBlock(final StringBuilder out);
+
+ /**
+ * Called when a code span is opened.
+ *
+ * Default implementation is:
+ *out.append("<code>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openCodeSpan(final StringBuilder out);
+
+ /**
+ * Called when a code span is closed.
+ *
+ * Default implementation is:
+ *out.append("</code>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeCodeSpan(final StringBuilder out);
+
+ /**
+ * Called when a headline is opened.
+ *
+ * Note: Don't close the HTML tag!
+ *Default implementation is:
+ * out.append("<h");
+ * out.append(level);
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openHeadline(final StringBuilder out, int level);
+
+ /**
+ * Called when a headline is closed.
+ *
+ * Default implementation is:
+ * out.append("</h");
+ * out.append(level);
+ * out.append(">\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeHeadline(final StringBuilder out, int level);
+
+ /**
+ * Called when a strong span is opened.
+ *
+ * Default implementation is:
+ *out.append("<strong>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openStrong(final StringBuilder out);
+
+ /**
+ * Called when a strong span is closed.
+ *
+ * Default implementation is:
+ *out.append("</strong>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeStrong(final StringBuilder out);
+
+ /**
+ * Called when an emphasis span is opened.
+ *
+ * Default implementation is:
+ *out.append("<em>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openEmphasis(final StringBuilder out);
+
+ /**
+ * Called when an emphasis span is closed.
+ *
+ * Default implementation is:
+ *out.append("</em>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeEmphasis(final StringBuilder out);
+
+ /**
+ * Called when a superscript span is opened.
+ *
+ * Default implementation is:
+ *out.append("<sup>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openSuper(final StringBuilder out);
+
+ /**
+ * Called when a superscript span is closed.
+ *
+ * Default implementation is:
+ *out.append("</sup>");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeSuper(final StringBuilder out);
+
+ /**
+ * Called when an ordered list is opened.
+ *
+ * Default implementation is:
+ *out.append("<ol>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openOrderedList(final StringBuilder out);
+
+ /**
+ * Called when an ordered list is closed.
+ *
+ * Default implementation is:
+ *out.append("</ol>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeOrderedList(final StringBuilder out);
+
+ /**
+ * Called when an unordered list is opened.
+ *
+ * Default implementation is:
+ *out.append("<ul>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openUnorderedList(final StringBuilder out);
+
+ /**
+ * Called when an unordered list is closed.
+ *
+ * Default implementation is:
+ *out.append("</ul>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeUnorderedList(final StringBuilder out);
+
+ /**
+ * Called when a list item is opened.
+ *
+ * Note: Don't close the HTML tag!
+ *Default implementation is:
+ *out.append("<li");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openListItem(final StringBuilder out);
+
+ /**
+ * Called when a list item is closed.
+ *
+ * Default implementation is:
+ *out.append("</li>\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void closeListItem(final StringBuilder out);
+
+ /**
+ * Called when a horizontal ruler is encountered.
+ *
+ * Default implementation is:
+ *out.append("<hr />\n");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void horizontalRuler(final StringBuilder out);
+
+ /**
+ * Called when a link is opened.
+ *
+ * Note: Don't close the HTML tag!
+ *Default implementation is:
+ *out.append("<a");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openLink(final StringBuilder out);
+
+ /**
+ * Called when an image is opened.
+ *
+ * Note: Don't close the HTML tag!
+ *Default implementation is:
+ *out.append("<img");
+ *
+ * @param out The StringBuilder to write to.
+ */
+ public void openImage(final StringBuilder out);
+}
diff --git a/src/main/java/com/github/rjeschke/txtmark/DefaultDecorator.java b/src/main/java/com/github/rjeschke/txtmark/DefaultDecorator.java
new file mode 100644
index 0000000..503bfb4
--- /dev/null
+++ b/src/main/java/com/github/rjeschke/txtmark/DefaultDecorator.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2011 René Jeschke + * Example for a user Decorator having a class attribute on <p> tags. + *
+ * + *
+ * public class MyDecorator extends DefaultDecorator
+ * {
+ * @Override
+ * public void openParagraph(StringBuilder out)
+ * {
+ * out.append("<p class=\"myclass\">");
+ * }
+ * }
+ *
+ *
+ *
+ * @author René Jeschke "); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeParagraph(StringBuilder) */ + @Override + public void closeParagraph(StringBuilder out) + { + out.append("
\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openBlockquote(StringBuilder) */ + @Override + public void openBlockquote(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeBlockquote(StringBuilder) */ + @Override + public void closeBlockquote(StringBuilder out) + { + out.append("\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openCodeBlock(StringBuilder) */ + @Override + public void openCodeBlock(StringBuilder out) + { + out.append("
");
+ }
+
+ /** @see com.github.rjeschke.txtmark.Decorator#closeCodeBlock(StringBuilder) */
+ @Override
+ public void closeCodeBlock(StringBuilder out)
+ {
+ out.append("\n");
+ }
+
+ /** @see com.github.rjeschke.txtmark.Decorator#openCodeSpan(StringBuilder) */
+ @Override
+ public void openCodeSpan(StringBuilder out)
+ {
+ out.append("");
+ }
+
+ /** @see com.github.rjeschke.txtmark.Decorator#closeCodeSpan(StringBuilder) */
+ @Override
+ public void closeCodeSpan(StringBuilder out)
+ {
+ out.append("");
+ }
+
+ /**
+ * @see com.github.rjeschke.txtmark.Decorator#openHeadline(StringBuilder,
+ * int)
+ */
+ @Override
+ public void openHeadline(StringBuilder out, int level)
+ {
+ out.append("true if the given String is a link prefix.
+ */
+ public static boolean isLinkPrefix(final String value)
+ {
+ return LINK_PREFIX.contains(value);
+ }
+
+ /**
+ * @param value String to check.
+ * @return Returns true if the given String is an entity.
+ */
+ public static boolean isEntity(final String value)
+ {
+ return decodeMap.containsKey(value);
+ }
+
+ /**
+ * @param value String to check.
+ * @return Returns true if the given String is a HTML tag.
+ */
+ public static boolean isHtmlElement(final String value)
+ {
+ return HTML_ELEMENTS.contains(value);
+ }
+
+ /**
+ * @param value String to check.
+ * @return Returns true if the given String is a HTML block level tag.
+ */
+ public static boolean isHtmlBlockElement(final String value)
+ {
+ return HTML_BLOCK_ELEMENTS.contains(value);
+ }
+
+ /**
+ * @param value String to check.
+ * @return Returns true if the given String is an unsafe HTML tag.
+ */
+ public static boolean isUnsafeHtmlElement(final String value)
+ {
+ return HTML_UNSAFE.contains(value);
+ }
+}
diff --git a/src/main/java/com/github/rjeschke/txtmark/HTMLElement.java b/src/main/java/com/github/rjeschke/txtmark/HTMLElement.java
new file mode 100644
index 0000000..2662d40
--- /dev/null
+++ b/src/main/java/com/github/rjeschke/txtmark/HTMLElement.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011 René Jeschke It also provides methods for processing + * and analyzing a line.
+ * + * @author René Jeschkefalse if end of line is reached
+ */
+ // TODO use Util#skipSpaces
+ public boolean skipSpaces()
+ {
+ while(this.pos < this.value.length() && this.value.charAt(this.pos) == ' ')
+ this.pos++;
+ return this.pos < this.value.length();
+ }
+
+ /**
+ * Reads chars from this line until any 'end' char is reached.
+ *
+ * @param end Delimiting character(s)
+ * @return The read String or null if no 'end' char was reached.
+ */
+ // TODO use Util#readUntil
+ public String readUntil(char... end)
+ {
+ final StringBuilder sb = new StringBuilder();
+ int pos = this.pos;
+ while(pos < this.value.length())
+ {
+ final char ch = this.value.charAt(pos);
+ if(ch == '\\' && pos + 1 < this.value.length())
+ {
+ final char c;
+ switch(c = this.value.charAt(pos + 1))
+ {
+ case '\\':
+ case '[':
+ case ']':
+ case '(':
+ case ')':
+ case '{':
+ case '}':
+ case '#':
+ case '"':
+ case '\'':
+ case '.':
+ case '>':
+ case '*':
+ case '+':
+ case '-':
+ case '_':
+ case '!':
+ case '`':
+ sb.append(c);
+ pos++;
+ break;
+ default:
+ sb.append(ch);
+ break;
+ }
+ }
+ else
+ {
+ boolean endReached = false;
+ for(int n = 0; n < end.length; n++)
+ {
+ if(ch == end[n])
+ {
+ endReached = true;
+ break;
+ }
+ }
+ if(endReached)
+ break;
+ sb.append(ch);
+ }
+ pos++;
+ }
+
+ final char ch = pos < this.value.length() ? this.value.charAt(pos) : '\n';
+ for(int n = 0; n < end.length; n++)
+ {
+ if(ch == end[n])
+ {
+ this.pos = pos;
+ return sb.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Marks this line empty. Also sets previous/next line's empty attributes.
+ */
+ public void setEmpty()
+ {
+ this.value = "";
+ this.leading = this.trailing = 0;
+ this.isEmpty = true;
+ if(this.previous != null)
+ this.previous.nextEmpty = true;
+ if(this.next != null)
+ this.next.prevEmpty = true;
+ }
+
+ /**
+ * Counts the amount of 'ch' in this line.
+ *
+ * @param ch The char to count.
+ * @return A value > 0 if this line only consists of 'ch' end spaces.
+ */
+ private int countChars(char ch)
+ {
+ int count = 0;
+ for(int i = 0; i < this.value.length(); i++)
+ {
+ final char c = this.value.charAt(i);
+ if(c == ' ')
+ continue;
+ if(c == ch)
+ {
+ count++;
+ continue;
+ }
+ count = 0;
+ break;
+ }
+ return count;
+ }
+
+ /**
+ * Gets this line's type.
+ *
+ * @return The LineType.
+ */
+ public LineType getLineType()
+ {
+ if(this.isEmpty)
+ return LineType.EMPTY;
+
+ if(this.leading > 3)
+ return LineType.CODE;
+
+ if(this.value.charAt(this.leading) == '#')
+ return LineType.HEADLINE;
+
+ if(this.value.charAt(this.leading) == '>')
+ return LineType.BQUOTE;
+
+ if(this.value.length() - this.leading - this.trailing > 2
+ && (this.value.charAt(this.leading) == '*' || this.value.charAt(this.leading) == '-' || this.value.charAt(this.leading) == '_'))
+ {
+ if(this.countChars(this.value.charAt(this.leading)) >= 3)
+ return LineType.HR;
+ }
+
+ if(this.value.length() - this.leading >= 2 && this.value.charAt(this.leading + 1) == ' ')
+ {
+ switch(this.value.charAt(this.leading))
+ {
+ case '*':
+ case '-':
+ case '+':
+ return LineType.ULIST;
+ }
+ }
+
+ if(this.value.length() - this.leading >= 3 && Character.isDigit(this.value.charAt(this.leading)))
+ {
+ int i = this.leading + 1;
+ while(i < this.value.length() && Character.isDigit(this.value.charAt(i)))
+ i++;
+ if(i + 1 < this.value.length() && this.value.charAt(i) == '.' && this.value.charAt(i + 1) == ' ')
+ return LineType.OLIST;
+ }
+
+ if(this.value.charAt(this.leading) == '<')
+ {
+ if(this.checkHTML())
+ return LineType.XML;
+ }
+
+ if(this.next != null && !this.next.isEmpty)
+ {
+ if((this.next.value.charAt(0) == '-') && (this.next.countChars('-') > 0))
+ return LineType.HEADLINE2;
+ if((this.next.value.charAt(0) == '=') && (this.next.countChars('=') > 0))
+ return LineType.HEADLINE1;
+ }
+
+ return LineType.OTHER;
+ }
+
+ /**
+ * Reads an XML comment. Sets xmlEndLine.
+ *
+ * @param firstLine The Line to start reading from.
+ * @param start The starting position.
+ * @return The new position or -1 if it is no valid comment.
+ */
+ private int readXMLComment(final Line firstLine, final int start)
+ {
+ Line line = firstLine;
+ if(start + 3 < line.value.length())
+ {
+ if(line.value.charAt(2) == '-' && line.value.charAt(3) == '-')
+ {
+ int pos = start + 4;
+ while(line != null)
+ {
+ while(pos < line.value.length() && line.value.charAt(pos) != '-')
+ {
+ pos++;
+ }
+ if(pos == line.value.length())
+ {
+ line = line.next;
+ pos = 0;
+ }
+ else
+ {
+ if(pos + 2 < line.value.length())
+ {
+ if(line.value.charAt(pos + 1) == '-' && line.value.charAt(pos + 2) == '>')
+ {
+ this.xmlEndLine = line;
+ return pos + 3;
+ }
+ }
+ pos++;
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Checks if this line contains an ID at it's end and removes it from the line.
+ *
+ * @return The ID or null if no valid ID exists.
+ */
+ // FIXME ... hack
+ public String stripID()
+ {
+ if(this.isEmpty || this.value.charAt(this.value.length() - this.trailing - 1) != '}')
+ return null;
+ int p = this.leading;
+ boolean found = false;
+ while(p < this.value.length() && !found)
+ {
+ switch(this.value.charAt(p))
+ {
+ case '\\':
+ if(p + 1 < this.value.length())
+ {
+ switch(this.value.charAt(p + 1))
+ {
+ case '{':
+ p++;
+ break;
+ }
+ }
+ p++;
+ break;
+ case '{':
+ found = true;
+ break;
+ default:
+ p++;
+ break;
+ }
+ }
+
+ if(found)
+ {
+ if(p + 1 < this.value.length() && this.value.charAt(p + 1) == '#')
+ {
+ final int start = p + 2;
+ p = start;
+ found = false;
+ while(p < this.value.length() && !found)
+ {
+ switch(this.value.charAt(p))
+ {
+ case '\\':
+ if(p + 1 < this.value.length())
+ {
+ switch(this.value.charAt(p + 1))
+ {
+ case '}':
+ p++;
+ break;
+ }
+ }
+ p++;
+ break;
+ case '}':
+ found = true;
+ break;
+ default:
+ p++;
+ break;
+ }
+ }
+ if(found)
+ {
+ final String id = this.value.substring(start, p).trim();
+ if(this.leading != 0)
+ {
+ this.value = this.value.substring(0, this.leading) + this.value.substring(this.leading, start - 2).trim();
+ }
+ else
+ {
+ this.value = this.value.substring(this.leading, start - 2).trim();
+ }
+ this.trailing = 0;
+ return id.length() > 0 ? id : null;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks for a valid HTML block. Sets xmlEndLine.
+ *
+ * @return true if it is a valid block.
+ */
+ private boolean checkHTML()
+ {
+ final LinkedListnull).
+ */
+ public LinkRef(final String link, final String title, final boolean isAbbrev)
+ {
+ this.link = link;
+ this.title = title;
+ this.isAbbrev = isAbbrev;
+ }
+
+ /** @see java.lang.Object#toString() */
+ @Override
+ public String toString()
+ {
+ return this.link + " \"" + this.title + "\"";
+ }
+}
diff --git a/src/main/java/com/github/rjeschke/txtmark/MarkToken.java b/src/main/java/com/github/rjeschke/txtmark/MarkToken.java
new file mode 100644
index 0000000..d7e0104
--- /dev/null
+++ b/src/main/java/com/github/rjeschke/txtmark/MarkToken.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 René Jeschke + * Example usage: + *
+ * + *
+ * String result = Processor.process("This is ***TXTMARK***");
+ *
+ *
+ *
+ * @author René Jeschke true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final String input, final boolean safeMode)
+ {
+ return process(input, Configuration.builder().setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input String into HTML.
+ *
+ * @param input
+ * The String to process.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final String input, final Decorator decorator)
+ {
+ return process(input, Configuration.builder().setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input String into HTML.
+ *
+ * @param input
+ * The String to process.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final String input, final Decorator decorator, final boolean safeMode)
+ {
+ return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input file into HTML using the default Configuration.
+ *
+ * @param file
+ * The File to process.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file) throws IOException
+ {
+ return process(file, Configuration.DEFAULT);
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final boolean safeMode) throws IOException
+ {
+ return process(file, Configuration.builder().setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final Decorator decorator) throws IOException
+ {
+ return process(file, Configuration.builder().setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final Decorator decorator, final boolean safeMode) throws IOException
+ {
+ return process(file, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param encoding
+ * The encoding to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final String encoding) throws IOException
+ {
+ return process(file, Configuration.builder().setEncoding(encoding).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param encoding
+ * The encoding to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final String encoding, final boolean safeMode) throws IOException
+ {
+ return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param encoding
+ * The encoding to use.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final String encoding, final Decorator decorator) throws IOException
+ {
+ return process(file, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input file into HTML.
+ *
+ * @param file
+ * The File to process.
+ * @param encoding
+ * The encoding to use.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final File file, final String encoding, final Decorator decorator,
+ final boolean safeMode) throws IOException
+ {
+ return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode)
+ .setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input) throws IOException
+ {
+ return process(input, Configuration.DEFAULT);
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final boolean safeMode) throws IOException
+ {
+ return process(input, Configuration.builder().setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final Decorator decorator) throws IOException
+ {
+ return process(input, Configuration.builder().setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final Decorator decorator, final boolean safeMode)
+ throws IOException
+ {
+ return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param encoding
+ * The encoding to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final String encoding) throws IOException
+ {
+ return process(input, Configuration.builder().setEncoding(encoding).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param encoding
+ * The encoding to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final String encoding, final boolean safeMode)
+ throws IOException
+ {
+ return process(input, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param encoding
+ * The encoding to use.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final String encoding, final Decorator decorator)
+ throws IOException
+ {
+ return process(input, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param input
+ * The InputStream to process.
+ * @param encoding
+ * The encoding to use.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final InputStream input, final String encoding, final Decorator decorator,
+ final boolean safeMode) throws IOException
+ {
+ return process(input,
+ Configuration.builder().setEncoding(encoding).setDecorator(decorator).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML using the default Configuration.
+ *
+ * @param reader
+ * The Reader to process.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final Reader reader) throws IOException
+ {
+ return process(reader, Configuration.DEFAULT);
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param reader
+ * The Reader to process.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final Reader reader, final boolean safeMode) throws IOException
+ {
+ return process(reader, Configuration.builder().setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param reader
+ * The Reader to process.
+ * @param decorator
+ * The decorator to use.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final Reader reader, final Decorator decorator) throws IOException
+ {
+ return process(reader, Configuration.builder().setDecorator(decorator).build());
+ }
+
+ /**
+ * Transforms an input stream into HTML.
+ *
+ * @param reader
+ * The Reader to process.
+ * @param decorator
+ * The decorator to use.
+ * @param safeMode
+ * Set to true to escape unsafe HTML tags.
+ * @return The processed String.
+ * @throws IOException
+ * if an IO error occurs
+ * @see Configuration#DEFAULT
+ */
+ public static String process(final Reader reader, final Decorator decorator, final boolean safeMode)
+ throws IOException
+ {
+ return process(reader, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
+ }
+
+ /**
+ * Reads all lines from our reader.
+ * + * Takes care of markdown link references. + *
+ * + * @return A Block containing all lines. + * @throws IOException + * If an IO error occurred. + */ + private Block readLines() throws IOException + { + final Block block = new Block(); + final StringBuilder sb = new StringBuilder(80); + int c = this.reader.read(); + LinkRef lastLinkRef = null; + while(c != -1) + { + sb.setLength(0); + int pos = 0; + boolean eol = false; + while(!eol) + { + switch(c) + { + case -1: + eol = true; + break; + case '\n': + c = this.reader.read(); + if(c == '\r') + c = this.reader.read(); + eol = true; + break; + case '\r': + c = this.reader.read(); + if(c == '\n') + c = this.reader.read(); + eol = true; + break; + case '\t': + { + final int np = pos + (4 - (pos & 3)); + while(pos < np) + { + sb.append(' '); + pos++; + } + c = this.reader.read(); + } + break; + default: + pos++; + sb.append((char) c); + c = this.reader.read(); + break; + } + } + + final Line line = new Line(); + line.value = sb.toString(); + line.init(); + + // Check for link definitions + boolean isLinkRef = false; + String id = null, link = null, comment = null; + if(!line.isEmpty && line.leading < 4 && line.value.charAt(line.leading) == '[') + { + line.pos = line.leading + 1; + // Read ID up to ']' + id = line.readUntil(']'); + // Is ID valid and are there any more characters? + if(id != null && line.pos + 2 < line.value.length()) + { + // Check for ':' ([...]:...) + if(line.value.charAt(line.pos + 1) == ':') + { + line.pos += 2; + line.skipSpaces(); + // Check for link syntax + if(line.value.charAt(line.pos) == '<') + { + line.pos++; + link = line.readUntil('>'); + line.pos++; + } + else + link = line.readUntil(' ', '\n'); + + // Is link valid? + if(link != null) + { + // Any non-whitespace characters following? + if(line.skipSpaces()) + { + final char ch = line.value.charAt(line.pos); + // Read comment + if(ch == '\"' || ch == '\'' || ch == '(') + { + line.pos++; + comment = line.readUntil(ch == '(' ? ')' : ch); + // Valid linkRef only if comment is valid + if(comment != null) + isLinkRef = true; + } + } + else + isLinkRef = true; + } + } + } + } + + // To make compiler happy: add != null checks + if(isLinkRef && id != null && link != null) + { + if(id.toLowerCase().equals("$profile$")) + { + this.emitter.useExtensions = this.useExtensions = link.toLowerCase().equals("extended"); + lastLinkRef = null; + } + else + { + // Store linkRef and skip line + final LinkRef lr = new LinkRef(link, comment, comment != null + && (link.length() == 1 && link.charAt(0) == '*')); + this.emitter.addLinkRef(id, lr); + if(comment == null) + lastLinkRef = lr; + } + } + else + { + comment = null; + // Check for multi-line linkRef + if(!line.isEmpty && lastLinkRef != null) + { + line.pos = line.leading; + final char ch = line.value.charAt(line.pos); + if(ch == '\"' || ch == '\'' || ch == '(') + { + line.pos++; + comment = line.readUntil(ch == '(' ? ')' : ch); + } + if(comment != null) + lastLinkRef.title = comment; + + lastLinkRef = null; + } + + // No multi-line linkRef, store line + if(comment == null) + { + line.pos = 0; + block.appendLine(line); + } + } + } + + return block; + } + + /** + * Initializes a list block by separating it into list item blocks. + * + * @param root + * The Block to process. + */ + private void initListBlock(final Block root) + { + Line line = root.lines; + line = line.next; + while(line != null) + { + final LineType t = line.getLineType(); + if((t == LineType.OLIST || t == LineType.ULIST) + || (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST)))) + { + root.split(line.previous).type = BlockType.LIST_ITEM; + } + line = line.next; + } + root.split(root.lineTail).type = BlockType.LIST_ITEM; + } + + /** + * Recursively process the given Block. + * + * @param root + * The Block to process. + * @param listMode + * Flag indicating that we're in a list item block. + */ + private void recurse(final Block root, boolean listMode) + { + Block block, list; + Line line = root.lines; + + if(listMode) + { + root.removeListIndent(); + if(this.useExtensions && root.lines != null && root.lines.getLineType() != LineType.CODE) + { + root.id = root.lines.stripID(); + } + } + + while(line != null && line.isEmpty) + line = line.next; + if(line == null) + return; + + while(line != null) + { + final LineType type = line.getLineType(); + switch(type) + { + case OTHER: + { + final boolean wasEmpty = line.prevEmpty; + while(line != null && !line.isEmpty) + { + final LineType t = line.getLineType(); + if((listMode || this.useExtensions) && (t == LineType.OLIST || t == LineType.ULIST)) + break; + if(this.useExtensions && (t == LineType.CODE)) + break; + if(t == LineType.HEADLINE || t == LineType.HEADLINE1 || t == LineType.HEADLINE2 || t == LineType.HR + || t == LineType.BQUOTE || t == LineType.XML) + break; + line = line.next; + } + final BlockType bt; + if(line != null && !line.isEmpty) + { + bt = (listMode && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH; + root.split(line.previous).type = bt; + root.removeLeadingEmptyLines(); + } + else + { + bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty) ? BlockType.NONE + : BlockType.PARAGRAPH; + root.split(line == null ? root.lineTail : line).type = bt; + root.removeLeadingEmptyLines(); + } + line = root.lines; + } + break; + case CODE: + while(line != null && (line.isEmpty || line.leading > 3)) + { + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.CODE; + block.removeSurroundingEmptyLines(); + break; + case XML: + if(line.previous != null) + { + // FIXME ... this looks wrong + root.split(line.previous); + } + root.split(line.xmlEndLine).type = BlockType.XML; + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case BQUOTE: + while(line != null) + { + if(!line.isEmpty && (line.prevEmpty && line.leading == 0 && line.getLineType() != LineType.BQUOTE)) + break; + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.BLOCKQUOTE; + block.removeSurroundingEmptyLines(); + block.removeBlockQuotePrefix(); + this.recurse(block, false); + line = root.lines; + break; + case HR: + if(line.previous != null) + { + // FIXME ... this looks wrong + root.split(line.previous); + } + root.split(line).type = BlockType.RULER; + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case HEADLINE: + case HEADLINE1: + case HEADLINE2: + if(line.previous != null) + { + root.split(line.previous); + } + if(type != LineType.HEADLINE) + { + line.next.setEmpty(); + } + block = root.split(line); + block.type = BlockType.HEADLINE; + if(type != LineType.HEADLINE) + block.hlDepth = type == LineType.HEADLINE1 ? 1 : 2; + if(this.useExtensions) + block.id = block.lines.stripID(); + block.transfromHeadline(); + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case OLIST: + case ULIST: + while(line != null) + { + final LineType t = line.getLineType(); + if(!line.isEmpty + && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST))) + break; + line = line.next; + } + list = root.split(line != null ? line.previous : root.lineTail); + list.type = type == LineType.OLIST ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST; + list.lines.prevEmpty = false; + list.lineTail.nextEmpty = false; + list.removeSurroundingEmptyLines(); + list.lines.prevEmpty = list.lineTail.nextEmpty = false; + this.initListBlock(list); + block = list.blocks; + while(block != null) + { + this.recurse(block, true); + block = block.next; + } + list.expandListParagraphs(); + break; + default: + line = line.next; + break; + } + } + } + + /** + * Does all the processing. + * + * @return The processed String. + * @throws IOException + * If an IO error occurred. + */ + private String process() throws IOException + { + final StringBuilder out = new StringBuilder(); + final Block parent = this.readLines(); + parent.removeSurroundingEmptyLines(); + + this.recurse(parent, false); + Block block = parent.blocks; + while(block != null) + { + this.emitter.emit(out, block); + block = block.next; + } + + return out.toString(); + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/Run.java b/src/main/java/com/github/rjeschke/txtmark/Run.java new file mode 100644 index 0000000..63f2ce6 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/Run.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 René JeschkeUsage:
+ *java -cp txtmark.jar txtmark.Run filename [header_footer_file]
+ *
+ *
+ * The header_footer_file is an optional UTF-8 encoded file containing
+ * a header and a footer to output around the generated HTML code.
Example:
+ * + *<?xml version="1.0" encoding="UTF-8"?>
+ *<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ *<html xmlns="http://www.w3.org/1999/xhtml">
+ *<head>
+ *<title>markdown</title>
+ *<link type="text/css" href="style.css" rel="stylesheet"/>
+ *<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
+ *</head>
+ *<body>
+ *<!-- the following line separates header from footer -->
+ *<!-- ### -->
+ *</body>
+ *</html>
+ *
+ *
+ * @author René Jeschke