From d42dae270ae38ef06395720bbb2448c6dca4da53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Sat, 16 Jun 2012 13:20:31 +0200 Subject: [PATCH] Maven folder structure, dropped ant build. --- .gitignore | 2 + README.md | 13 - pom.xml | 3 - .../com/github/rjeschke/txtmark/Block.java | 296 ++++++ .../github/rjeschke/txtmark/BlockEmitter.java | 70 ++ .../github/rjeschke/txtmark/BlockType.java | 45 + .../rjeschke/txtmark/Configuration.java | 198 ++++ .../github/rjeschke/txtmark/Decorator.java | 281 ++++++ .../rjeschke/txtmark/DefaultDecorator.java | 230 +++++ .../com/github/rjeschke/txtmark/Emitter.java | 895 +++++++++++++++++ .../com/github/rjeschke/txtmark/HTML.java | 220 ++++ .../github/rjeschke/txtmark/HTMLElement.java | 45 + .../com/github/rjeschke/txtmark/Line.java | 492 +++++++++ .../com/github/rjeschke/txtmark/LineType.java | 41 + .../com/github/rjeschke/txtmark/LinkRef.java | 51 + .../github/rjeschke/txtmark/MarkToken.java | 71 ++ .../github/rjeschke/txtmark/Processor.java | 940 ++++++++++++++++++ .../java/com/github/rjeschke/txtmark/Run.java | 93 ++ .../com/github/rjeschke/txtmark/Utils.java | 548 ++++++++++ 19 files changed, 4518 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/github/rjeschke/txtmark/Block.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/BlockEmitter.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/BlockType.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Configuration.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Decorator.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/DefaultDecorator.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Emitter.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/HTML.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/HTMLElement.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Line.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/LineType.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/LinkRef.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/MarkToken.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Processor.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Run.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/Utils.java diff --git a/.gitignore b/.gitignore index 7cc57ef..d207b86 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ release/ .classpath .project target/ +*~ + diff --git a/README.md b/README.md index 2b0516a..2c0af4d 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,6 @@ For an in-depth explanation of markdown have a look at the original [Markdown Sy Txtmark is now available as a maven artifact. Have a look [here] (http://renejeschke.de/maven/). -*** - -### Build instructions - -1. Clone the [repo] or download the sources as [tar] or [zip] -2. Install [Apache Ant(TM)] -3. Do - - ant release - - and you will find everything you need inside the `release` folder. - - *** ### Txtmark extensions diff --git a/pom.xml b/pom.xml index 1eccc59..13091ef 100644 --- a/pom.xml +++ b/pom.xml @@ -80,9 +80,6 @@ 2.2 - - ${basedir}/src/java - diff --git a/src/main/java/com/github/rjeschke/txtmark/Block.java b/src/main/java/com/github/rjeschke/txtmark/Block.java new file mode 100644 index 0000000..9b53ca9 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/Block.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * This class represents a block of lines. + * + * @author René Jeschke + */ +class Block +{ + /** This block's type. */ + public BlockType type = BlockType.NONE; + /** Head and tail of linked lines. */ + public Line lines = null, lineTail = null; + /** Head and tail of child blocks. */ + public Block blocks = null, blockTail = null; + /** Next block. */ + public Block next = null; + /** Depth of headline BlockType. */ + public int hlDepth = 0; + /** ID for headlines and list items */ + public String id = null; + + /** Constructor. */ + public Block() + { + // + } + + /** + * @return 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 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.util.List; + +/** + * Block emitter interface. An example for a code block emitter would look like + * this: + * + *
+ * 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("&amp;");
+ *                 break;
+ *             case '<':
+ *                 out.append("&lt;");
+ *                 break;
+ *             case '>':
+ *                 out.append("&gt;");
+ *                 break;
+ *             default:
+ *                 out.append(c);
+ *                 break;
+ *             }
+ *         }
+ *         out.append('\n');
+ *     }
+ * }
+ * 
+ * 
+ * + * + * @author René Jeschke + * @since 0.7 + */ +public interface BlockEmitter +{ + /** + * This method is responsible for outputting a markdown block. All + * processing must be done inside this method. + * + * @param out + * The StringBuilder to append to + * @param lines + * List of lines + */ + public void emitBlock(StringBuilder out, List lines); +} diff --git a/src/main/java/com/github/rjeschke/txtmark/BlockType.java b/src/main/java/com/github/rjeschke/txtmark/BlockType.java new file mode 100644 index 0000000..d2a662c --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/BlockType.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Block type enum. + * + * @author René Jeschke + */ +enum BlockType +{ + /** Unspecified. Used for root block and list items without paragraphs. */ + NONE, + /** A block quote. */ + BLOCKQUOTE, + /** A code block. */ + CODE, + /** A headline. */ + HEADLINE, + /** A list item. */ + LIST_ITEM, + /** An ordered list. */ + ORDERED_LIST, + /** A paragraph. */ + PARAGRAPH, + /** A horizontal ruler. */ + RULER, + /** An unordered list. */ + UNORDERED_LIST, + /** A XML block. */ + XML +} diff --git a/src/main/java/com/github/rjeschke/txtmark/Configuration.java b/src/main/java/com/github/rjeschke/txtmark/Configuration.java new file mode 100644 index 0000000..680541e --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/Configuration.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Txtmark configuration. + * + * @author René Jeschke + * @since 0.7 + */ +public class Configuration +{ + final boolean safeMode; + final String encoding; + final Decorator decorator; + final BlockEmitter codeBlockEmitter; + + /** + *

+ * This is the default configuration for txtmark's process + * methods + *

+ * + *
    + *
  • safeMode = false
  • + *
  • encoding = UTF-8
  • + *
  • decorator = DefaultDecorator
  • + *
  • codeBlockEmitter = null
  • + *
+ */ + public final static Configuration DEFAULT = Configuration.builder().build(); + + /** + *

+ * Default safe configuration + *

+ * + *
    + *
  • safeMode = true
  • + *
  • encoding = UTF-8
  • + *
  • decorator = DefaultDecorator
  • + *
  • codeBlockEmitter = null
  • + *
+ */ + public final static Configuration DEFAULT_SAFE = Configuration.builder().enableSafeMode().build(); + + /** + * Constructor. + * + * @param safeMode + * @param encoding + * @param decorator + */ + Configuration(boolean safeMode, String encoding, Decorator decorator, BlockEmitter codeBlockEmitter) + { + this.safeMode = safeMode; + this.encoding = encoding; + this.decorator = decorator; + this.codeBlockEmitter = codeBlockEmitter; + } + + /** + * Creates a new Builder instance. + * + * @return A new Builder instance. + */ + public static Builder builder() + { + return new Builder(); + } + + /** + * Configuration builder. + * + * @author René Jeschke + * @since 0.7 + */ + public static class Builder + { + private boolean safeMode = false; + private String encoding = "UTF-8"; + private Decorator decorator = new DefaultDecorator(); + private BlockEmitter codeBlockEmitter = null; + + /** + * Constructor. + * + */ + Builder() + { + // empty + } + + /** + * Enables HTML safe mode. + * + * Default: false + * + * @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 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Decorator interface. + * + * @author René Jeschke + */ +public interface Decorator +{ + /** + * Called when a paragraph is opened. + * + *

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 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Default Decorator implementation. + * + *

+ * 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 + */ +public class DefaultDecorator implements Decorator +{ + /** Constructor. */ + public DefaultDecorator() + { + // empty + } + + /** @see com.github.rjeschke.txtmark.Decorator#openParagraph(StringBuilder) */ + @Override + public void openParagraph(StringBuilder out) + { + out.append("

"); + } + + /** @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("\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openStrong(StringBuilder) */ + @Override + public void openStrong(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeStrong(StringBuilder) */ + @Override + public void closeStrong(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openEmphasis(StringBuilder) */ + @Override + public void openEmphasis(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeEmphasis(StringBuilder) */ + @Override + public void closeEmphasis(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openSuper(StringBuilder) */ + @Override + public void openSuper(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeSuper(StringBuilder) */ + @Override + public void closeSuper(StringBuilder out) + { + out.append(""); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openOrderedList(StringBuilder) */ + @Override + public void openOrderedList(StringBuilder out) + { + out.append("
    \n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeOrderedList(StringBuilder) */ + @Override + public void closeOrderedList(StringBuilder out) + { + out.append("
\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openUnorderedList(StringBuilder) */ + @Override + public void openUnorderedList(StringBuilder out) + { + out.append("
    \n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#closeUnorderedList(StringBuilder) */ + @Override + public void closeUnorderedList(StringBuilder out) + { + out.append("
\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openListItem(StringBuilder) */ + @Override + public void openListItem(StringBuilder out) + { + out.append("\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#horizontalRuler(StringBuilder) */ + @Override + public void horizontalRuler(StringBuilder out) + { + out.append("
\n"); + } + + /** @see com.github.rjeschke.txtmark.Decorator#openLink(StringBuilder) */ + @Override + public void openLink(StringBuilder out) + { + out.append(" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Emitter class responsible for generating HTML output. + * + * @author René Jeschke + */ +class Emitter +{ + /** Link references. */ + private final HashMap linkRefs = new HashMap(); + /** The configuration. */ + private final Configuration config; + /** Extension flag. */ + public boolean useExtensions = false; + + /** Constructor. */ + public Emitter(final Configuration config) + { + this.config = config; + } + + /** + * Adds a LinkRef to this set of LinkRefs. + * + * @param key The key/id. + * @param linkRef The LinkRef. + */ + public void addLinkRef(final String key, final LinkRef linkRef) + { + this.linkRefs.put(key.toLowerCase(), linkRef); + } + + /** + * Transforms the given block recursively into HTML. + * + * @param out The StringBuilder to write to. + * @param root The Block to process. + */ + public void emit(final StringBuilder out, final Block root) + { + root.removeSurroundingEmptyLines(); + + switch(root.type) + { + case RULER: + this.config.decorator.horizontalRuler(out); + return; + case NONE: + case XML: + break; + case HEADLINE: + this.config.decorator.openHeadline(out, root.hlDepth); + if(this.useExtensions && root.id != null) + { + out.append(" id=\""); + Utils.appendCode(out, root.id, 0, root.id.length()); + out.append('"'); + } + out.append('>'); + break; + case PARAGRAPH: + this.config.decorator.openParagraph(out); + break; + case CODE: + this.config.decorator.openCodeBlock(out); + break; + case BLOCKQUOTE: + this.config.decorator.openBlockquote(out); + break; + case UNORDERED_LIST: + this.config.decorator.openUnorderedList(out); + break; + case ORDERED_LIST: + this.config.decorator.openOrderedList(out); + break; + case LIST_ITEM: + this.config.decorator.openListItem(out); + if(this.useExtensions && root.id != null) + { + out.append(" id=\""); + Utils.appendCode(out, root.id, 0, root.id.length()); + out.append('"'); + } + out.append('>'); + break; + } + + if(root.hasLines()) + { + this.emitLines(out, root); + } + else + { + Block block = root.blocks; + while(block != null) + { + this.emit(out, block); + block = block.next; + } + } + + switch(root.type) + { + case RULER: + case NONE: + case XML: + break; + case HEADLINE: + this.config.decorator.closeHeadline(out, root.hlDepth); + break; + case PARAGRAPH: + this.config.decorator.closeParagraph(out); + break; + case CODE: + this.config.decorator.closeCodeBlock(out); + break; + case BLOCKQUOTE: + this.config.decorator.closeBlockquote(out); + break; + case UNORDERED_LIST: + this.config.decorator.closeUnorderedList(out); + break; + case ORDERED_LIST: + this.config.decorator.closeOrderedList(out); + break; + case LIST_ITEM: + this.config.decorator.closeListItem(out); + break; + } + } + + /** + * Transforms lines into HTML. + * + * @param out The StringBuilder to write to. + * @param block The Block to process. + */ + private void emitLines(final StringBuilder out, final Block block) + { + switch(block.type) + { + case CODE: + this.emitCodeLines(out, block.lines); + break; + case XML: + this.emitRawLines(out, block.lines); + break; + default: + this.emitMarkedLines(out, block.lines); + break; + } + } + + /** + * Finds the position of the given Token in the given String. + * + * @param in The String to search on. + * @param start The starting character position. + * @param token The token to find. + * @return The position of the token or -1 if none could be found. + */ + private int findToken(final String in, int start, MarkToken token) + { + int pos = start; + while(pos < in.length()) + { + if(this.getToken(in, pos) == token) + return pos; + pos++; + } + return -1; + } + + /** + * Checks if there is a valid markdown link definition. + * + * @param out The StringBuilder containing the generated output. + * @param in Input String. + * @param start Starting position. + * @param token Either LINK or IMAGE. + * @return The new position or -1 if there is no valid markdown link. + */ + private int checkLink(final StringBuilder out, final String in, int start, MarkToken token) + { + boolean isAbbrev = false; + int pos = start + (token == MarkToken.LINK ? 1 : 2); + final StringBuilder temp = new StringBuilder(); + + temp.setLength(0); + pos = Utils.readMdLinkId(temp, in, pos); + if(pos < start) + return -1; + + String name = temp.toString(), link = null, comment = null; + final int oldPos = pos++; + pos = Utils.skipSpaces(in, pos); + if(pos < start) + { + final LinkRef lr = this.linkRefs.get(name.toLowerCase()); + if(lr != null) + { + isAbbrev = lr.isAbbrev; + link = lr.link; + comment = lr.title; + pos = oldPos; + } + else + { + return -1; + } + } + else if(in.charAt(pos) == '(') + { + pos++; + pos = Utils.skipSpaces(in, pos); + if(pos < start) + return -1; + temp.setLength(0); + boolean useLt = in.charAt(pos) == '<'; + pos = useLt ? Utils.readUntil(temp, in, pos + 1, '>') : Utils.readMdLink(temp, in, pos); + if(pos < start) + return -1; + if(useLt) + pos++; + link = temp.toString(); + + if(in.charAt(pos) == ' ') + { + pos = Utils.skipSpaces(in, pos); + if(pos > start && in.charAt(pos) == '"') + { + pos++; + temp.setLength(0); + pos = Utils.readUntil(temp, in, pos, '"'); + if(pos < start) + return -1; + comment = temp.toString(); + pos++; + pos = Utils.skipSpaces(in, pos); + if(pos == -1) + return -1; + } + } + if(in.charAt(pos) != ')') + return -1; + } + else if(in.charAt(pos) == '[') + { + pos++; + temp.setLength(0); + pos = Utils.readRawUntil(temp, in, pos, ']'); + if(pos < start) + return -1; + final String id = temp.length() > 0 ? temp.toString() : name; + final LinkRef lr = this.linkRefs.get(id.toLowerCase()); + if(lr != null) + { + link = lr.link; + comment = lr.title; + } + } + else + { + final LinkRef lr = this.linkRefs.get(name.toLowerCase()); + if(lr != null) + { + isAbbrev = lr.isAbbrev; + link = lr.link; + comment = lr.title; + pos = oldPos; + } + else + { + return -1; + } + } + + if(link == null) + return -1; + + if(token == MarkToken.LINK) + { + if(isAbbrev && comment != null) + { + if(!this.useExtensions) + return -1; + out.append(""); + this.recursiveEmitLine(out, name, 0, MarkToken.NONE); + out.append(""); + } + else + { + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append('"'); + if(comment != null) + { + out.append(" title=\""); + Utils.appendValue(out, comment, 0, comment.length()); + out.append('"'); + } + out.append('>'); + this.recursiveEmitLine(out, name, 0, MarkToken.NONE); + out.append(""); + } + } + else + { + this.config.decorator.openImage(out); + out.append(" src=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append("\" alt=\""); + Utils.appendValue(out, name, 0, name.length()); + out.append('"'); + if(comment != null) + { + out.append(" title=\""); + Utils.appendValue(out, comment, 0, comment.length()); + out.append('"'); + } + out.append(" />"); + } + + return pos; + } + + /** + * Check if there is a valid HTML tag here. + * This method also transforms auto links and mailto auto links. + * + * @param out The StringBuilder to write to. + * @param in Input String. + * @param start Starting position. + * @return The new position or -1 if nothing valid has been found. + */ + private int checkHtml(final StringBuilder out, final String in, int start) + { + final StringBuilder temp = new StringBuilder(); + int pos; + + // Check for auto links + temp.setLength(0); + pos = Utils.readUntil(temp, in, start + 1, ':', ' ', '>', '\n'); + if(pos != -1 && in.charAt(pos) == ':' && HTML.isLinkPrefix(temp.toString())) + { + pos = Utils.readUntil(temp, in, pos, '>'); + if(pos != -1) + { + final String link = temp.toString(); + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append("\">"); + Utils.appendValue(out, link, 0, link.length()); + out.append(""); + return pos; + } + } + + // Check for mailto auto link + temp.setLength(0); + pos = Utils.readUntil(temp, in, start + 1, '@', ' ', '>', '\n'); + if(pos != -1 && in.charAt(pos) == '@') + { + pos = Utils.readUntil(temp, in, pos, '>'); + if(pos != -1) + { + final String link = temp.toString(); + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendMailto(out, "mailto:", 0, 7); + Utils.appendMailto(out, link, 0, link.length()); + out.append("\">"); + Utils.appendMailto(out, link, 0, link.length()); + out.append(""); + return pos; + } + } + + // Check for inline html + if(start + 2 < in.length()) + { + temp.setLength(0); + return Utils.readXML(out, in, start, this.config.safeMode); + } + + return -1; + } + + /** + * Check if this is a valid XML/HTML entity. + * + * @param out The StringBuilder to write to. + * @param in Input String. + * @param start Starting position + * @return The new position or -1 if this entity in invalid. + */ + private int checkEntity(final StringBuilder out, final String in, int start) + { + int pos = Utils.readUntil(out, in, start, ';'); + if(pos < 0 || out.length() < 3) + return -1; + if(out.charAt(1) == '#') + { + if(out.charAt(2) == 'x' || out.charAt(2) == 'X') + { + if(out.length() < 4) + return -1; + for(int i = 3; i < out.length(); i++) + { + final char c = out.charAt(i); + if((c < '0' || c > '9') && ((c < 'a' || c > 'f') && (c < 'A' || c > 'F'))) + return -1; + } + } + else + { + for(int i = 2; i < out.length(); i++) + { + final char c = out.charAt(i); + if(c < '0' || c > '9') + return -1; + } + } + out.append(';'); + } + else + { + for(int i = 1; i < out.length(); i++) + { + final char c = out.charAt(i); + if((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) + return -1; + } + out.append(';'); + return HTML.isEntity(out.toString()) ? pos : -1; + } + + return pos; + } + + /** + * Recursively scans through the given line, taking care of any markdown stuff. + * + * @param out The StringBuilder to write to. + * @param in Input String. + * @param start Start position. + * @param token The matching Token (for e.g. '*') + * @return The position of the matching Token or -1 if token was NONE or no Token could be found. + */ + private int recursiveEmitLine(final StringBuilder out, final String in, int start, MarkToken token) + { + int pos = start, a, b; + final StringBuilder temp = new StringBuilder(); + while(pos < in.length()) + { + final MarkToken mt = this.getToken(in, pos); + if(token != MarkToken.NONE && (mt == token || token == MarkToken.EM_STAR && mt == MarkToken.STRONG_STAR || token == MarkToken.EM_UNDERSCORE && mt == MarkToken.STRONG_UNDERSCORE)) + return pos; + + switch(mt) + { + case IMAGE: + case LINK: + temp.setLength(0); + b = this.checkLink(temp, in, pos, mt); + if(b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case EM_STAR: + case EM_UNDERSCORE: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 1, mt); + if(b > 0) + { + this.config.decorator.openEmphasis(out); + out.append(temp); + this.config.decorator.closeEmphasis(out); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case STRONG_STAR: + case STRONG_UNDERSCORE: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 2, mt); + if(b > 0) + { + this.config.decorator.openStrong(out); + out.append(temp); + this.config.decorator.closeStrong(out); + pos = b + 1; + } + else + { + out.append(in.charAt(pos)); + } + break; + case SUPER: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 1, mt); + if(b > 0) + { + this.config.decorator.openSuper(out); + out.append(temp); + this.config.decorator.closeSuper(out); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case CODE_SINGLE: + case CODE_DOUBLE: + a = pos + (mt == MarkToken.CODE_DOUBLE ? 2 : 1); + b = this.findToken(in, a, mt); + if(b > 0) + { + pos = b + (mt == MarkToken.CODE_DOUBLE ? 1 : 0); + while(a < b && in.charAt(a) == ' ') + a++; + if(a < b) + { + while(in.charAt(b - 1) == ' ') + b--; + this.config.decorator.openCodeSpan(out); + Utils.appendCode(out, in, a, b); + this.config.decorator.closeCodeSpan(out); + } + } + else + { + out.append(in.charAt(pos)); + } + break; + case HTML: + temp.setLength(0); + b = this.checkHtml(temp, in, pos); + if(b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append("<"); + } + break; + case ENTITY: + temp.setLength(0); + b = this.checkEntity(temp, in, pos); + if(b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append("&"); + } + break; + case X_COPY: + out.append("©"); + pos += 2; + break; + case X_REG: + out.append("®"); + pos += 2; + break; + case X_TRADE: + out.append("™"); + pos += 3; + break; + case X_NDASH: + out.append("–"); + pos++; + break; + case X_MDASH: + out.append("—"); + pos += 2; + break; + case X_HELLIP: + out.append("…"); + pos += 2; + break; + case X_LAQUO: + out.append("«"); + pos++; + break; + case X_RAQUO: + out.append("»"); + pos++; + break; + case X_RDQUO: + out.append("”"); + break; + case X_LDQUO: + out.append("“"); + break; + case ESCAPE: + pos++; + //$FALL-THROUGH$ + default: + out.append(in.charAt(pos)); + break; + } + pos++; + } + return -1; + } + + /** + * Check if there is any markdown Token. + * + * @param in Input String. + * @param pos Starting position. + * @return The Token. + */ + private MarkToken getToken(final String in, final int pos) + { + final char c0 = pos > 0 ? in.charAt(pos - 1) : ' '; + final char c = in.charAt(pos); + final char c1 = pos + 1 < in.length() ? in.charAt(pos + 1) : ' '; + final char c2 = pos + 2 < in.length() ? in.charAt(pos + 2) : ' '; + final char c3 = pos + 3 < in.length() ? in.charAt(pos + 3) : ' '; + + switch(c) + { + case '*': + if(c1 == '*') + { + return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_STAR : MarkToken.EM_STAR; + } + return c0 != ' ' || c1 != ' ' ? MarkToken.EM_STAR : MarkToken.NONE; + case '_': + if(c1 == '_') + { + return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_UNDERSCORE : MarkToken.EM_UNDERSCORE; + } + if(this.useExtensions) + { + return c0 != ' ' && c0 != '_' && c1 != ' ' ? MarkToken.NONE : MarkToken.EM_UNDERSCORE; + } + return c0 != ' ' || c1 != ' ' ? MarkToken.EM_UNDERSCORE : MarkToken.NONE; + case '!': + if(c1 == '[') + return MarkToken.IMAGE; + return MarkToken.NONE; + case '[': + return MarkToken.LINK; + case '`': + return c1 == '`' ? MarkToken.CODE_DOUBLE : MarkToken.CODE_SINGLE; + case '\\': + switch(c1) + { + case '\\': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '#': + case '"': + case '\'': + case '.': + case '>': + case '<': + case '*': + case '+': + case '-': + case '_': + case '!': + case '`': + case '^': + return MarkToken.ESCAPE; + default: + return MarkToken.NONE; + } + case '<': + if(this.useExtensions && c1 == '<') + return MarkToken.X_LAQUO; + return MarkToken.HTML; + case '&': + return MarkToken.ENTITY; + default: + if(this.useExtensions) + { + switch(c) + { + case '-': + if(c1 == '-') + return c2 == '-' ? MarkToken.X_MDASH : MarkToken.X_NDASH; + break; + case '^': + return c0 == '^' || c1 == '^' ? MarkToken.NONE : MarkToken.SUPER; + case '>': + if(c1 == '>') + return MarkToken.X_RAQUO; + break; + case '.': + if(c1 == '.' && c2 == '.') + return MarkToken.X_HELLIP; + break; + case '(': + if(c1 == 'C' && c2 == ')') + return MarkToken.X_COPY; + if(c1 == 'R' && c2 == ')') + return MarkToken.X_REG; + if(c1 == 'T' & c2 == 'M' & c3 == ')') + return MarkToken.X_TRADE; + break; + case '"': + if(!Character.isLetterOrDigit(c0) && c1 != ' ') + return MarkToken.X_LDQUO; + if(c0 != ' ' && !Character.isLetterOrDigit(c1)) + return MarkToken.X_RDQUO; + break; + } + } + return MarkToken.NONE; + } + } + + /** + * Writes a set of markdown lines into the StringBuilder. + * + * @param out The StringBuilder to write to. + * @param lines The lines to write. + */ + private void emitMarkedLines(final StringBuilder out, final Line lines) + { + final StringBuilder in = new StringBuilder(); + Line line = lines; + while(line != null) + { + if(!line.isEmpty) + { + in.append(line.value.substring(line.leading, line.value.length() - line.trailing)); + if(line.trailing >= 2) + in.append("
"); + } + if(line.next != null) + in.append('\n'); + line = line.next; + } + + this.recursiveEmitLine(out, in.toString(), 0, MarkToken.NONE); + } + + /** + * Writes a set of raw lines into the StringBuilder. + * + * @param out The StringBuilder to write to. + * @param lines The lines to write. + */ + private void emitRawLines(final StringBuilder out, final Line lines) + { + Line line = lines; + if(this.config.safeMode) + { + final StringBuilder temp = new StringBuilder(); + while(line != null) + { + if(!line.isEmpty) + { + temp.append(line.value); + } + temp.append('\n'); + line = line.next; + } + final String in = temp.toString(); + for(int pos = 0; pos < in.length(); pos++) + { + if(in.charAt(pos) == '<') + { + temp.setLength(0); + final int t = Utils.readXML(temp, in, pos, this.config.safeMode); + if(t != -1) + { + out.append(temp); + pos = t; + } + else + { + out.append(in.charAt(pos)); + } + } + else + { + out.append(in.charAt(pos)); + } + } + } + else + { + while(line != null) + { + if(!line.isEmpty) + { + out.append(line.value); + } + out.append('\n'); + line = line.next; + } + } + } + + /** + * Writes a code block into the StringBuilder. + * + * @param out The StringBuilder to write to. + * @param lines The lines to write. + */ + private void emitCodeLines(final StringBuilder out, final Line lines) + { + Line line = lines; + if(this.config.codeBlockEmitter != null) + { + final ArrayList list = new ArrayList(); + while(line != null) + { + if(line.isEmpty) + list.add(""); + else + list.add(line.value.substring(4)); + line = line.next; + } + this.config.codeBlockEmitter.emitBlock(out, list); + } + else + { + while(line != null) + { + if(!line.isEmpty) + { + for(int i = 4; i < line.value.length(); i++) + { + final char c; + switch(c = line.value.charAt(i)) + { + case '&': + out.append("&"); + break; + case '<': + out.append("<"); + break; + case '>': + out.append(">"); + break; + default: + out.append(c); + break; + } + } + } + out.append('\n'); + line = line.next; + } + } + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/HTML.java b/src/main/java/com/github/rjeschke/txtmark/HTML.java new file mode 100644 index 0000000..2a205ee --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/HTML.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.util.HashMap; +import java.util.HashSet; + +/** + * HTML utility class. + * + * @author René Jeschke + */ +class HTML +{ + /** List of valid HTML/XML entity names. */ + private final static String[] ENTITY_NAMES = { + "Â", "â", "´", "Æ", "æ", "À", "à", "ℵ", + "Α", "α", "&", "∧", "∠", "'", "Å", "å", + "≈", "Ã", "ã", "Ä", "ä", "„", "Β", "β", + "¦", "•", "∩", "Ç", "ç", "¸", "¢", "Χ", + "χ", "ˆ", "♣", "≅", "©", "↵", "∪", "¤", + "‡", "†", "⇓", "↓", "°", "Δ", "δ", "♦", + "÷", "É", "é", "Ê", "ê", "È", "è", "∅", + " ", " ", "Ε", "ε", "≡", "Η", "η", "Ð", + "ð", "Ë", "ë", "€", "∃", "ƒ", "∀", "½", + "¼", "¾", "⁄", "Γ", "γ", "≥", ">", "⇔", + "↔", "♥", "…", "Í", "í", "Î", "î", "¡", + "Ì", "ì", "ℑ", "∞", "∫", "Ι", "ι", "¿", + "∈", "Ï", "ï", "Κ", "κ", "Λ", "λ", "⟨", + "«", "⇐", "←", "⌈", "“", "≤", "⌊", "∗", + "◊", "‎", "‹", "‘", "<", "¯", "—", "µ", + "·", "−", "Μ", "μ", "∇", " ", "–", "≠", + "∋", "¬", "∉", "⊄", "Ñ", "ñ", "Ν", "ν", + "Ó", "ó", "Ô", "ô", "Œ", "œ", "Ò", "ò", + "‾", "Ω", "ω", "Ο", "ο", "⊕", "∨", "ª", + "º", "Ø", "ø", "Õ", "õ", "⊗", "Ö", "ö", + "¶", "∂", "‰", "⊥", "Φ", "φ", "Π", "π", + "ϖ", "±", "£", "″", "′", "∏", "∝", "Ψ", + "ψ", """, "√", "⟩", "»", "⇒", "→", "⌉", + "”", "ℜ", "®", "⌋", "Ρ", "ρ", "‏", "›", + "’", "‚", "Š", "š", "⋅", "§", "­", "Σ", + "σ", "ς", "∼", "♠", "⊂", "⊆", "∑", "⊃", + "¹", "²", "³", "⊇", "ß", "Τ", "τ", "∴", + "Θ", "θ", "ϑ", " ", "þ", "˜", "×", "™", + "Ú", "ú", "⇑", "↑", "Û", "û", "Ù", "ù", + "¨", "ϒ", "Υ", "υ", "Ü", "ü", "℘", "Ξ", + "ξ", "Ý", "ý", "¥", "Ÿ", "ÿ", "Ζ", "ζ", + "‍", "‌" + }; + /** Characters corresponding to ENTITY_NAMES. */ + private final static char[] ENTITY_CHARS = { + '\u00C2', '\u00E2', '\u00B4', '\u00C6', '\u00E6', '\u00C0', '\u00E0', '\u2135', + '\u0391', '\u03B1', '\u0026', '\u2227', '\u2220', '\'', '\u00C5', '\u00E5', + '\u2248', '\u00C3', '\u00E3', '\u00C4', '\u00E4', '\u201E', '\u0392', '\u03B2', + '\u00A6', '\u2022', '\u2229', '\u00C7', '\u00E7', '\u00B8', '\u00A2', '\u03A7', + '\u03C7', '\u02C6', '\u2663', '\u2245', '\u00A9', '\u21B5', '\u222A', '\u00A4', + '\u2021', '\u2020', '\u21D3', '\u2193', '\u00B0', '\u0394', '\u03B4', '\u2666', + '\u00F7', '\u00C9', '\u00E9', '\u00CA', '\u00EA', '\u00C8', '\u00E8', '\u2205', + '\u2003', '\u2002', '\u0395', '\u03B5', '\u2261', '\u0397', '\u03B7', '\u00D0', + '\u00F0', '\u00CB', '\u00EB', '\u20AC', '\u2203', '\u0192', '\u2200', '\u00BD', + '\u00BC', '\u00BE', '\u2044', '\u0393', '\u03B3', '\u2265', '\u003E', '\u21D4', + '\u2194', '\u2665', '\u2026', '\u00CD', '\u00ED', '\u00CE', '\u00EE', '\u00A1', + '\u00CC', '\u00EC', '\u2111', '\u221E', '\u222B', '\u0399', '\u03B9', '\u00BF', + '\u2208', '\u00CF', '\u00EF', '\u039A', '\u03BA', '\u039B', '\u03BB', '\u2329', + '\u00AB', '\u21D0', '\u2190', '\u2308', '\u201C', '\u2264', '\u230A', '\u2217', + '\u25CA', '\u200E', '\u2039', '\u2018', '\u003C', '\u00AF', '\u2014', '\u00B5', + '\u00B7', '\u2212', '\u039C', '\u03BC', '\u2207', '\u00A0', '\u2013', '\u2260', + '\u220B', '\u00AC', '\u2209', '\u2284', '\u00D1', '\u00F1', '\u039D', '\u03BD', + '\u00D3', '\u00F3', '\u00D4', '\u00F4', '\u0152', '\u0153', '\u00D2', '\u00F2', + '\u203E', '\u03A9', '\u03C9', '\u039F', '\u03BF', '\u2295', '\u2228', '\u00AA', + '\u00BA', '\u00D8', '\u00F8', '\u00D5', '\u00F5', '\u2297', '\u00D6', '\u00F6', + '\u00B6', '\u2202', '\u2030', '\u22A5', '\u03A6', '\u03C6', '\u03A0', '\u03C0', + '\u03D6', '\u00B1', '\u00A3', '\u2033', '\u2032', '\u220F', '\u221D', '\u03A8', + '\u03C8', '\u0022', '\u221A', '\u232A', '\u00BB', '\u21D2', '\u2192', '\u2309', + '\u201D', '\u211C', '\u00AE', '\u230B', '\u03A1', '\u03C1', '\u200F', '\u203A', + '\u2019', '\u201A', '\u0160', '\u0161', '\u22C5', '\u00A7', '\u00AD', '\u03A3', + '\u03C3', '\u03C2', '\u223C', '\u2660', '\u2282', '\u2286', '\u2211', '\u2283', + '\u00B9', '\u00B2', '\u00B3', '\u2287', '\u00DF', '\u03A4', '\u03C4', '\u2234', + '\u0398', '\u03B8', '\u03D1', '\u00DE', '\u00FE', '\u02DC', '\u00D7', '\u2122', + '\u00DA', '\u00FA', '\u21D1', '\u2191', '\u00DB', '\u00FB', '\u00D9', '\u00F9', + '\u00A8', '\u03D2', '\u03A5', '\u03C5', '\u00DC', '\u00FC', '\u2118', '\u039E', + '\u03BE', '\u00DD', '\u00FD', '\u00A5', '\u0178', '\u00FF', '\u0396', '\u03B6', + '\u200D', '\u200C' + }; + /** Valid markdown link prefixes for auto links. */ + private final static String[] LINK_PREFIXES = { + "http", "https", + "ftp", "ftps" + }; + + /** HTML block level elements. */ + private final static HTMLElement[] BLOCK_ELEMENTS = { + HTMLElement.address, + HTMLElement.blockquote, + HTMLElement.del, HTMLElement.div, HTMLElement.dl, + HTMLElement.fieldset, HTMLElement.form, + HTMLElement.h1, HTMLElement.h2, HTMLElement.h3, HTMLElement.h4, HTMLElement.h5, HTMLElement.h6, HTMLElement.hr, + HTMLElement.ins, + HTMLElement.noscript, + HTMLElement.ol, + HTMLElement.p, HTMLElement.pre, + HTMLElement.table, + HTMLElement.ul + }; + + /** HTML unsafe elements. */ + private final static HTMLElement[] UNSAFE_ELEMENTS = { + HTMLElement.applet, + HTMLElement.head, + HTMLElement.html, + HTMLElement.body, + HTMLElement.frame, + HTMLElement.frameset, + HTMLElement.iframe, + HTMLElement.script, + HTMLElement.object, + }; + + /** Character to entity encoding map. */ + private final static HashMap encodeMap = new HashMap(); + /** Entity to character decoding map. */ + private final static HashMap decodeMap = new HashMap(); + /** Set of valid HTML tags. */ + private final static HashSet HTML_ELEMENTS = new HashSet(); + /** Set of unsafe HTML tags. */ + private final static HashSet HTML_UNSAFE = new HashSet(); + /** Set of HTML block level tags. */ + private final static HashSet HTML_BLOCK_ELEMENTS = new HashSet(); + /** Set of valid markdown link prefixes. */ + private final static HashSet LINK_PREFIX = new HashSet(); + + static + { + for(final HTMLElement h : HTMLElement.values()) + { + HTML_ELEMENTS.add(h.toString()); + } + for(final HTMLElement h : UNSAFE_ELEMENTS) + { + HTML_UNSAFE.add(h.toString()); + } + for(final HTMLElement h : BLOCK_ELEMENTS) + { + HTML_BLOCK_ELEMENTS.add(h.toString()); + } + for(int i = 0; i < ENTITY_NAMES.length; i++) + { + encodeMap.put(ENTITY_CHARS[i], ENTITY_NAMES[i]); + decodeMap.put(ENTITY_NAMES[i], ENTITY_CHARS[i]); + } + for(int i = 0; i < LINK_PREFIXES.length; i++) + { + LINK_PREFIX.add(LINK_PREFIXES[i]); + } + } + + /** Constructor. (Singleton) */ + private HTML() + { + // + } + + /** + * @param value String to check. + * @return Returns 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 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Enum of HTML tags. + * + * @author René Jeschke + */ +enum HTMLElement +{ + NONE, + a, abbr, acronym, address, applet, area, + b, base, basefont, bdo, big, blockquote, body, br, button, + caption, cite, code, col, colgroup, + dd, del, dfn, div, dl, dt, + em, + fieldset, font, form, frame, frameset, + h1, h2, h3, h4, h5, h6, head, hr, html, + i, iframe, img, input, ins, + kbd, + label, legend, li, link, + map, meta, + noscript, + object, ol, optgroup, option, + p, param, pre, + q, + s, samp, script, select, small, span, strike, strong, style, sub, sup, + table, tbody, td, textarea, tfoot, th, thead, title, tr, tt, + u, ul, + var +} diff --git a/src/main/java/com/github/rjeschke/txtmark/Line.java b/src/main/java/com/github/rjeschke/txtmark/Line.java new file mode 100644 index 0000000..92c61ff --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/Line.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.util.LinkedList; + +/** + * This class represents a text line. + * + *

It also provides methods for processing + * and analyzing a line.

+ * + * @author René Jeschke + */ +class Line +{ + /** Current cursor position. */ + public int pos; + /** Leading and trailing spaces. */ + public int leading = 0, trailing = 0; + /** Is this line empty? */ + public boolean isEmpty = true; + /** This line's value. */ + public String value = null; + /** Previous and next line. */ + public Line previous = null, next = null; + /** Is previous/next line empty? */ + public boolean prevEmpty, nextEmpty; + /** Final line of a XML block. */ + public Line xmlEndLine; + /** Constructor. */ + public Line() + { + // + } + + /** + * Calculates leading and trailing spaces. Also sets empty if needed. + */ + public void init() + { + this.leading = 0; + while(this.leading < this.value.length() && this.value.charAt(this.leading) == ' ') + this.leading++; + + if(this.leading == this.value.length()) + { + this.setEmpty(); + } + else + { + this.isEmpty = false; + this.trailing = 0; + while(this.value.charAt(this.value.length() - this.trailing - 1) == ' ') + this.trailing++; + } + } + + /** + * Recalculate leading spaces. + */ + public void initLeading() + { + this.leading = 0; + while(this.leading < this.value.length() && this.value.charAt(this.leading) == ' ') + this.leading++; + + if(this.leading == this.value.length()) + { + this.setEmpty(); + } + } + + /** + * Skips spaces. + * + * @return false 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 LinkedList tags = new LinkedList(); + final StringBuilder temp = new StringBuilder(); + int pos = this.leading; + if(this.value.charAt(this.leading + 1) == '!') + { + if(this.readXMLComment(this, this.leading) > 0) + return true; + } + pos = Utils.readXML(temp, this.value, this.leading, false); + String element, tag; + if(pos > -1) + { + element = temp.toString(); + temp.setLength(0); + Utils.getXMLTag(temp, element); + tag = temp.toString().toLowerCase(); + if(!HTML.isHtmlBlockElement(tag)) + return false; + if(tag.equals("hr")) + { + this.xmlEndLine = this; + return true; + } + tags.add(tag); + + Line line = this; + while(line != null) + { + while(pos < line.value.length() && line.value.charAt(pos) != '<') + { + pos++; + } + if(pos >= line.value.length()) + { + line = line.next; + pos = 0; + } + else + { + temp.setLength(0); + final int newPos = Utils.readXML(temp, line.value, pos, false); + if(newPos > 0) + { + element = temp.toString(); + temp.setLength(0); + Utils.getXMLTag(temp, element); + tag = temp.toString().toLowerCase(); + if(HTML.isHtmlBlockElement(tag) && !tag.equals("hr")) + { + if(element.charAt(1) == '/') + { + if(!tags.getLast().equals(tag)) + return false; + tags.removeLast(); + } + else + { + tags.addLast(tag); + } + } + if(tags.size() == 0) + { + this.xmlEndLine = line; + break; + } + pos = newPos; + } + else + { + pos++; + } + } + } + return tags.size() == 0; + } + return false; + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/LineType.java b/src/main/java/com/github/rjeschke/txtmark/LineType.java new file mode 100644 index 0000000..1478757 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/LineType.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Line type enumeration. + * + * @author René Jeschke + */ +enum LineType +{ + /** Empty line. */ + EMPTY, + /** Undefined content. */ + OTHER, + /** A markdown headline. */ + HEADLINE, HEADLINE1, HEADLINE2, + /** A code block line. */ + CODE, + /** A list. */ + ULIST, OLIST, + /** A block quote. */ + BQUOTE, + /** A horizontal ruler. */ + HR, + /** Start of a XML block. */ + XML +} diff --git a/src/main/java/com/github/rjeschke/txtmark/LinkRef.java b/src/main/java/com/github/rjeschke/txtmark/LinkRef.java new file mode 100644 index 0000000..5c702c8 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/LinkRef.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * A markdown link reference. + * + * @author René Jeschke + */ +class LinkRef +{ + /** The link. */ + public final String link; + /** The optional comment/title. */ + public String title; + /** Flag indicating that this is an abbreviation. */ + public final boolean isAbbrev; + + /** + * Constructor. + * + * @param link The link. + * @param title The title (may be null). + */ + 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 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +/** + * Markdown token enumeration. + * + * @author René Jeschke + */ +enum MarkToken +{ + /** No token. */ + NONE, + /** * */ + EM_STAR, // x*x + /** _ */ + EM_UNDERSCORE, // x_x + /** ** */ + STRONG_STAR, // x**x + /** __ */ + STRONG_UNDERSCORE, // x__x + /** ` */ + CODE_SINGLE, // ` + /** `` */ + CODE_DOUBLE, // `` + /** [ */ + LINK, // [ + /** < */ + HTML, // < + /** ![ */ + IMAGE, // ![ + /** & */ + ENTITY, // & + /** \ */ + ESCAPE, // \x + /** Extended: ^ */ + SUPER, // ^ + /** Extended: (C) */ + X_COPY, // (C) + /** Extended: (R) */ + X_REG, // (R) + /** Extended: (TM) */ + X_TRADE, // (TM) + /** Extended: << */ + X_LAQUO, // << + /** Extended: >> */ + X_RAQUO, // >> + /** Extended: -- */ + X_NDASH, // -- + /** Extended: --- */ + X_MDASH, // --- + /** Extended: ... */ + X_HELLIP, // ... + /** Extended: "x */ + X_RDQUO, // " + /** Extended: x" */ + X_LDQUO // " +} diff --git a/src/main/java/com/github/rjeschke/txtmark/Processor.java b/src/main/java/com/github/rjeschke/txtmark/Processor.java new file mode 100644 index 0000000..fa6d85a --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/Processor.java @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2011 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/** + * Markdown processor class. + * + *

+ * Example usage: + *

+ * + *
+ * String result = Processor.process("This is ***TXTMARK***");
+ * 
+ * 
+ * + * @author René Jeschke + */ +public class Processor +{ + /** The reader. */ + private final Reader reader; + /** The emitter. */ + private final Emitter emitter; + /** The Configuration. */ + final Configuration config; + /** Extension flag. */ + private boolean useExtensions = false; + + /** + * Constructor. + * + * @param reader + * The input reader. + */ + private Processor(final Reader reader, final Configuration config) + { + this.reader = reader; + this.config = config; + this.emitter = new Emitter(this.config); + } + + /** + * Transforms an input stream into HTML using the given Configuration. + * + * @param reader + * The Reader to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public static String process(final Reader reader, final Configuration configuration) throws IOException + { + final Processor p = new Processor(!(reader instanceof BufferedReader) ? new BufferedReader(reader) : reader, + configuration); + return p.process(); + } + + /** + * Transforms an input String into HTML using the given Configuration. + * + * @param input + * The String to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @since 0.7 + * @see Configuration + */ + public static String process(final String input, final Configuration configuration) + { + try + { + return process(new StringReader(input), configuration); + } + catch (IOException e) + { + // This _can never_ happen + return null; + } + } + + /** + * Transforms an input file into HTML using the given Configuration. + * + * @param file + * The File to process. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public static String process(final File file, final Configuration configuration) throws IOException + { + final FileInputStream input = new FileInputStream(file); + final String ret = process(input, configuration); + input.close(); + return ret; + } + + /** + * Transforms an input stream into HTML using the given Configuration. + * + * @param input + * The InputStream to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public static String process(final InputStream input, final Configuration configuration) throws IOException + { + final Processor p = new Processor(new BufferedReader(new InputStreamReader(input, configuration.encoding)), + configuration); + return p.process(); + } + + /** + * Transforms an input String into HTML using the default Configuration. + * + * @param input + * The String to process. + * @return The processed String. + * @see Configuration#DEFAULT + */ + public static String process(final String input) + { + return process(input, Configuration.DEFAULT); + } + + /** + * Transforms an input String into HTML. + * + * @param input + * The String to process. + * @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 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é Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.rjeschke.txtmark; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Simple class for processing markdown files on the command line. + * + *

Usage:

+ *
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 + */ +public class Run +{ + /** + * Static main. + * + * @param args Program arguments. + * @throws IOException If an IO error occurred. + */ + public static void main(String[] args) throws IOException + { + // This is just a _hack_ ... + BufferedReader reader = null; + if(args.length == 0) + { + System.err.println("No input file specified."); + System.exit(-1); + } + if(args.length > 1) + { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(args[1]), "UTF-8")); + String line = reader.readLine(); + while(line != null && !line.startsWith("