Maven folder structure, dropped ant build.

This commit is contained in:
René Jeschke 2012-06-16 13:20:31 +02:00
parent 66ad9595d0
commit d42dae270a
19 changed files with 4518 additions and 16 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ release/
.classpath
.project
target/
*~

View File

@ -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

View File

@ -80,9 +80,6 @@
<version>2.2</version>
</extension>
</extensions>
<sourceDirectory>${basedir}/src/java</sourceDirectory>
</build>
<dependencies>

View File

@ -0,0 +1,296 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
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 <code>true</code> 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 <code>hlDepth</code> 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 <code>true</code> 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 <code>NONE</code> to <code>PARAGRAPH</code> 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;
}
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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:
*
* <pre>
* <code>public void emitBlock(StringBuilder out, List&lt;String&gt; 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;amp;");
* break;
* case '&lt;':
* out.append("&amp;lt;");
* break;
* case '&gt;':
* out.append("&amp;gt;");
* break;
* default:
* out.append(c);
* break;
* }
* }
* out.append('\n');
* }
* }
* </code>
* </pre>
*
*
* @author René Jeschke <rene_jeschke@yahoo.de>
* @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<String> lines);
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
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
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
* @since 0.7
*/
public class Configuration
{
final boolean safeMode;
final String encoding;
final Decorator decorator;
final BlockEmitter codeBlockEmitter;
/**
* <p>
* This is the default configuration for txtmark's <code>process</code>
* methods
* </p>
*
* <ul>
* <li><code>safeMode = false</code></li>
* <li><code>encoding = UTF-8</code></li>
* <li><code>decorator = DefaultDecorator</code></li>
* <li><code>codeBlockEmitter = null</code></li>
* </ul>
*/
public final static Configuration DEFAULT = Configuration.builder().build();
/**
* <p>
* Default safe configuration
* </p>
*
* <ul>
* <li><code>safeMode = true</code></li>
* <li><code>encoding = UTF-8</code></li>
* <li><code>decorator = DefaultDecorator</code></li>
* <li><code>codeBlockEmitter = null</code></li>
* </ul>
*/
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 <rene_jeschke@yahoo.de>
* @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: <code>false</code>
*
* @return This builder
* @since 0.7
*/
public Builder enableSafeMode()
{
this.safeMode = true;
return this;
}
/**
* Sets the HTML safe mode flag.
*
* Default: <code>false</code>
*
* @param flag
* <code>true</code> 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: <code>&quot;UTF-8&quot;</code>
*
* @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: <code>DefaultDecorator()</code>
*
* @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: <code>null</code>
*
* @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);
}
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
public interface Decorator
{
/**
* Called when a paragraph is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;p>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openParagraph(final StringBuilder out);
/**
* Called when a paragraph is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/p>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeParagraph(final StringBuilder out);
/**
* Called when a blockquote is opened.
*
* Default implementation is:
* <pre><code>out.append("&lt;blockquote>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openBlockquote(final StringBuilder out);
/**
* Called when a blockquote is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/blockquote>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeBlockquote(final StringBuilder out);
/**
* Called when a code block is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;pre>&lt;code>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openCodeBlock(final StringBuilder out);
/**
* Called when a code block is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/code>&lt;/pre>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeCodeBlock(final StringBuilder out);
/**
* Called when a code span is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;code>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openCodeSpan(final StringBuilder out);
/**
* Called when a code span is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/code>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeCodeSpan(final StringBuilder out);
/**
* Called when a headline is opened.
*
* <p><strong>Note:</strong> Don't close the HTML tag!</p>
* <p>Default implementation is:</p>
* <pre><code> out.append("&lt;h");
* out.append(level);</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openHeadline(final StringBuilder out, int level);
/**
* Called when a headline is closed.
*
* <p>Default implementation is:</p>
* <pre><code> out.append("&lt;/h");
* out.append(level);
* out.append(">\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeHeadline(final StringBuilder out, int level);
/**
* Called when a strong span is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;strong>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openStrong(final StringBuilder out);
/**
* Called when a strong span is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/strong>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeStrong(final StringBuilder out);
/**
* Called when an emphasis span is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;em>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openEmphasis(final StringBuilder out);
/**
* Called when an emphasis span is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/em>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeEmphasis(final StringBuilder out);
/**
* Called when a superscript span is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;sup>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openSuper(final StringBuilder out);
/**
* Called when a superscript span is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/sup>");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeSuper(final StringBuilder out);
/**
* Called when an ordered list is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;ol>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openOrderedList(final StringBuilder out);
/**
* Called when an ordered list is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/ol>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeOrderedList(final StringBuilder out);
/**
* Called when an unordered list is opened.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;ul>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openUnorderedList(final StringBuilder out);
/**
* Called when an unordered list is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/ul>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeUnorderedList(final StringBuilder out);
/**
* Called when a list item is opened.
*
* <p><strong>Note:</strong> Don't close the HTML tag!</p>
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;li");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openListItem(final StringBuilder out);
/**
* Called when a list item is closed.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;/li>\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void closeListItem(final StringBuilder out);
/**
* Called when a horizontal ruler is encountered.
*
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;hr />\n");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void horizontalRuler(final StringBuilder out);
/**
* Called when a link is opened.
*
* <p><strong>Note:</strong> Don't close the HTML tag!</p>
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;a");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openLink(final StringBuilder out);
/**
* Called when an image is opened.
*
* <p><strong>Note:</strong> Don't close the HTML tag!</p>
* <p>Default implementation is:</p>
* <pre><code>out.append("&lt;img");</code></pre>
*
* @param out The StringBuilder to write to.
*/
public void openImage(final StringBuilder out);
}

View File

@ -0,0 +1,230 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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.
*
* <p>
* Example for a user Decorator having a class attribute on &lt;p> tags.
* </p>
*
* <pre>
* <code>public class MyDecorator extends DefaultDecorator
* {
* &#64;Override
* public void openParagraph(StringBuilder out)
* {
* out.append("&lt;p class=\"myclass\">");
* }
* }
* </code>
* </pre>
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
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("<p>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeParagraph(StringBuilder) */
@Override
public void closeParagraph(StringBuilder out)
{
out.append("</p>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openBlockquote(StringBuilder) */
@Override
public void openBlockquote(StringBuilder out)
{
out.append("<blockquote>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeBlockquote(StringBuilder) */
@Override
public void closeBlockquote(StringBuilder out)
{
out.append("</blockquote>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openCodeBlock(StringBuilder) */
@Override
public void openCodeBlock(StringBuilder out)
{
out.append("<pre><code>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeCodeBlock(StringBuilder) */
@Override
public void closeCodeBlock(StringBuilder out)
{
out.append("</code></pre>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openCodeSpan(StringBuilder) */
@Override
public void openCodeSpan(StringBuilder out)
{
out.append("<code>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeCodeSpan(StringBuilder) */
@Override
public void closeCodeSpan(StringBuilder out)
{
out.append("</code>");
}
/**
* @see com.github.rjeschke.txtmark.Decorator#openHeadline(StringBuilder,
* int)
*/
@Override
public void openHeadline(StringBuilder out, int level)
{
out.append("<h");
out.append(level);
}
/**
* @see com.github.rjeschke.txtmark.Decorator#closeHeadline(StringBuilder,
* int)
*/
@Override
public void closeHeadline(StringBuilder out, int level)
{
out.append("</h");
out.append(level);
out.append(">\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openStrong(StringBuilder) */
@Override
public void openStrong(StringBuilder out)
{
out.append("<strong>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeStrong(StringBuilder) */
@Override
public void closeStrong(StringBuilder out)
{
out.append("</strong>");
}
/** @see com.github.rjeschke.txtmark.Decorator#openEmphasis(StringBuilder) */
@Override
public void openEmphasis(StringBuilder out)
{
out.append("<em>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeEmphasis(StringBuilder) */
@Override
public void closeEmphasis(StringBuilder out)
{
out.append("</em>");
}
/** @see com.github.rjeschke.txtmark.Decorator#openSuper(StringBuilder) */
@Override
public void openSuper(StringBuilder out)
{
out.append("<sup>");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeSuper(StringBuilder) */
@Override
public void closeSuper(StringBuilder out)
{
out.append("</sup>");
}
/** @see com.github.rjeschke.txtmark.Decorator#openOrderedList(StringBuilder) */
@Override
public void openOrderedList(StringBuilder out)
{
out.append("<ol>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeOrderedList(StringBuilder) */
@Override
public void closeOrderedList(StringBuilder out)
{
out.append("</ol>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openUnorderedList(StringBuilder) */
@Override
public void openUnorderedList(StringBuilder out)
{
out.append("<ul>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeUnorderedList(StringBuilder) */
@Override
public void closeUnorderedList(StringBuilder out)
{
out.append("</ul>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openListItem(StringBuilder) */
@Override
public void openListItem(StringBuilder out)
{
out.append("<li");
}
/** @see com.github.rjeschke.txtmark.Decorator#closeListItem(StringBuilder) */
@Override
public void closeListItem(StringBuilder out)
{
out.append("</li>\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#horizontalRuler(StringBuilder) */
@Override
public void horizontalRuler(StringBuilder out)
{
out.append("<hr />\n");
}
/** @see com.github.rjeschke.txtmark.Decorator#openLink(StringBuilder) */
@Override
public void openLink(StringBuilder out)
{
out.append("<a");
}
/** @see com.github.rjeschke.txtmark.Decorator#openImage(StringBuilder) */
@Override
public void openImage(StringBuilder out)
{
out.append("<img");
}
}

View File

@ -0,0 +1,895 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
class Emitter
{
/** Link references. */
private final HashMap<String, LinkRef> linkRefs = new HashMap<String, LinkRef>();
/** 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("<abbr title=\"");
Utils.appendValue(out, comment, 0, comment.length());
out.append("\">");
this.recursiveEmitLine(out, name, 0, MarkToken.NONE);
out.append("</abbr>");
}
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("</a>");
}
}
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("</a>");
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("</a>");
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("&lt;");
}
break;
case ENTITY:
temp.setLength(0);
b = this.checkEntity(temp, in, pos);
if(b > 0)
{
out.append(temp);
pos = b;
}
else
{
out.append("&amp;");
}
break;
case X_COPY:
out.append("&copy;");
pos += 2;
break;
case X_REG:
out.append("&reg;");
pos += 2;
break;
case X_TRADE:
out.append("&trade;");
pos += 3;
break;
case X_NDASH:
out.append("&ndash;");
pos++;
break;
case X_MDASH:
out.append("&mdash;");
pos += 2;
break;
case X_HELLIP:
out.append("&hellip;");
pos += 2;
break;
case X_LAQUO:
out.append("&laquo;");
pos++;
break;
case X_RAQUO:
out.append("&raquo;");
pos++;
break;
case X_RDQUO:
out.append("&rdquo;");
break;
case X_LDQUO:
out.append("&ldquo;");
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("<br />");
}
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<String> list = new ArrayList<String>();
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("&amp;");
break;
case '<':
out.append("&lt;");
break;
case '>':
out.append("&gt;");
break;
default:
out.append(c);
break;
}
}
}
out.append('\n');
line = line.next;
}
}
}
}

View File

@ -0,0 +1,220 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
class HTML
{
/** List of valid HTML/XML entity names. */
private final static String[] ENTITY_NAMES = {
"&Acirc;", "&acirc;", "&acute;", "&AElig;", "&aelig;", "&Agrave;", "&agrave;", "&alefsym;",
"&Alpha;", "&alpha;", "&amp;", "&and;", "&ang;", "&apos;", "&Aring;", "&aring;",
"&asymp;", "&Atilde;", "&atilde;", "&Auml;", "&auml;", "&bdquo;", "&Beta;", "&beta;",
"&brvbar;", "&bull;", "&cap;", "&Ccedil;", "&ccedil;", "&cedil;", "&cent;", "&Chi;",
"&chi;", "&circ;", "&clubs;", "&cong;", "&copy;", "&crarr;", "&cup;", "&curren;",
"&Dagger;", "&dagger;", "&dArr;", "&darr;", "&deg;", "&Delta;", "&delta;", "&diams;",
"&divide;", "&Eacute;", "&eacute;", "&Ecirc;", "&ecirc;", "&Egrave;", "&egrave;", "&empty;",
"&emsp;", "&ensp;", "&Epsilon;", "&epsilon;", "&equiv;", "&Eta;", "&eta;", "&ETH;",
"&eth;", "&Euml;", "&euml;", "&euro;", "&exist;", "&fnof;", "&forall;", "&frac12;",
"&frac14;", "&frac34;", "&frasl;", "&Gamma;", "&gamma;", "&ge;", "&gt;", "&hArr;",
"&harr;", "&hearts;", "&hellip;", "&Iacute;", "&iacute;", "&Icirc;", "&icirc;", "&iexcl;",
"&Igrave;", "&igrave;", "&image;", "&infin;", "&int;", "&Iota;", "&iota;", "&iquest;",
"&isin;", "&Iuml;", "&iuml;", "&Kappa;", "&kappa;", "&Lambda;", "&lambda;", "&lang;",
"&laquo;", "&lArr;", "&larr;", "&lceil;", "&ldquo;", "&le;", "&lfloor;", "&lowast;",
"&loz;", "&lrm;", "&lsaquo;", "&lsquo;", "&lt;", "&macr;", "&mdash;", "&micro;",
"&middot;", "&minus;", "&Mu;", "&mu;", "&nabla;", "&nbsp;", "&ndash;", "&ne;",
"&ni;", "&not;", "&notin;", "&nsub;", "&Ntilde;", "&ntilde;", "&Nu;", "&nu;",
"&Oacute;", "&oacute;", "&Ocirc;", "&ocirc;", "&OElig;", "&oelig;", "&Ograve;", "&ograve;",
"&oline;", "&Omega;", "&omega;", "&Omicron;", "&omicron;", "&oplus;", "&or;", "&ordf;",
"&ordm;", "&Oslash;", "&oslash;", "&Otilde;", "&otilde;", "&otimes;", "&Ouml;", "&ouml;",
"&para;", "&part;", "&permil;", "&perp;", "&Phi;", "&phi;", "&Pi;", "&pi;",
"&piv;", "&plusmn;", "&pound;", "&Prime;", "&prime;", "&prod;", "&prop;", "&Psi;",
"&psi;", "&quot;", "&radic;", "&rang;", "&raquo;", "&rArr;", "&rarr;", "&rceil;",
"&rdquo;", "&real;", "&reg;", "&rfloor;", "&Rho;", "&rho;", "&rlm;", "&rsaquo;",
"&rsquo;", "&sbquo;", "&Scaron;", "&scaron;", "&sdot;", "&sect;", "&shy;", "&Sigma;",
"&sigma;", "&sigmaf;", "&sim;", "&spades;", "&sub;", "&sube;", "&sum;", "&sup;",
"&sup1;", "&sup2;", "&sup3;", "&supe;", "&szlig;", "&Tau;", "&tau;", "&there4;",
"&Theta;", "&theta;", "&thetasym;", "&thinsp;", "&thorn;", "&tilde;", "&times;", "&trade;",
"&Uacute;", "&uacute;", "&uArr;", "&uarr;", "&Ucirc;", "&ucirc;", "&Ugrave;", "&ugrave;",
"&uml;", "&upsih;", "&Upsilon;", "&upsilon;", "&Uuml;", "&uuml;", "&weierp;", "&Xi;",
"&xi;", "&Yacute;", "&yacute;", "&yen;", "&Yuml;", "&yuml;", "&Zeta;", "&zeta;",
"&zwj;", "&zwnj;"
};
/** 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<Character, String> encodeMap = new HashMap<Character, String>();
/** Entity to character decoding map. */
private final static HashMap<String, Character> decodeMap = new HashMap<String, Character>();
/** Set of valid HTML tags. */
private final static HashSet<String> HTML_ELEMENTS = new HashSet<String>();
/** Set of unsafe HTML tags. */
private final static HashSet<String> HTML_UNSAFE = new HashSet<String>();
/** Set of HTML block level tags. */
private final static HashSet<String> HTML_BLOCK_ELEMENTS = new HashSet<String>();
/** Set of valid markdown link prefixes. */
private final static HashSet<String> LINK_PREFIX = new HashSet<String>();
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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> if the given String is an unsafe HTML tag.
*/
public static boolean isUnsafeHtmlElement(final String value)
{
return HTML_UNSAFE.contains(value);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
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
}

View File

@ -0,0 +1,492 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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.
*
* <p>It also provides methods for processing
* and analyzing a line.</p>
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
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 <code>false</code> 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 <code>null</code> 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 <code>xmlEndLine</code>.
*
* @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 <code>null</code> 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 <code>xmlEndLine</code>.
*
* @return <code>true</code> if it is a valid block.
*/
private boolean checkHTML()
{
final LinkedList<String> tags = new LinkedList<String>();
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;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
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
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
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 <code>null</code>).
*/
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 + "\"";
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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 <rene_jeschke@yahoo.de>
*/
enum MarkToken
{
/** No token. */
NONE,
/** &#x2a; */
EM_STAR, // x*x
/** _ */
EM_UNDERSCORE, // x_x
/** &#x2a;&#x2a; */
STRONG_STAR, // x**x
/** __ */
STRONG_UNDERSCORE, // x__x
/** ` */
CODE_SINGLE, // `
/** `` */
CODE_DOUBLE, // ``
/** [ */
LINK, // [
/** &lt; */
HTML, // <
/** ![ */
IMAGE, // ![
/** &amp; */
ENTITY, // &
/** \ */
ESCAPE, // \x
/** Extended: ^ */
SUPER, // ^
/** Extended: (C) */
X_COPY, // (C)
/** Extended: (R) */
X_REG, // (R)
/** Extended: (TM) */
X_TRADE, // (TM)
/** Extended: &lt;&lt; */
X_LAQUO, // <<
/** Extended: >> */
X_RAQUO, // >>
/** Extended: -- */
X_NDASH, // --
/** Extended: --- */
X_MDASH, // ---
/** Extended: &#46;&#46;&#46; */
X_HELLIP, // ...
/** Extended: "x */
X_RDQUO, // "
/** Extended: x" */
X_LDQUO // "
}

View File

@ -0,0 +1,940 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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.
*
* <p>
* Example usage:
* </p>
*
* <pre>
* <code>String result = Processor.process("This is ***TXTMARK***");
* </code>
* </pre>
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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.
* <p>
* Takes care of markdown link references.
* </p>
*
* @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();
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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.
*
* <p>Usage:</p>
* <pre><code>java -cp txtmark.jar txtmark.Run filename [header_footer_file]
* </code></pre>
*
* <p>The <code>header_footer_file</code> is an optional UTF-8 encoded file containing
* a header and a footer to output around the generated HTML code.</p>
*
* <p>Example:</p>
*
* <pre><code>&lt;?xml version="1.0" encoding="UTF-8"?>
*&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
* "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
*&lt;html xmlns="http://www.w3.org/1999/xhtml">
*&lt;head>
*&lt;title>markdown&lt;/title>
*&lt;link type="text/css" href="style.css" rel="stylesheet"/>
*&lt;meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
*&lt;/head>
*&lt;body>
*&lt;!-- the following line separates header from footer -->
*&lt;!-- ### -->
*&lt;/body>
*&lt;/html>
*</code></pre>
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
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("<!-- ###"))
{
System.out.println(line);
line = reader.readLine();
}
}
System.out.println(Processor.process(new File(args[0])));
if(args.length > 1 && reader != null)
{
String line = reader.readLine();
while(line != null)
{
System.out.println(line);
line = reader.readLine();
}
reader.close();
}
}
}

View File

@ -0,0 +1,548 @@
/*
* Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de>
*
* 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;
/**
* Utilities.
*
* @author René Jeschke <rene_jeschke@yahoo.de>
*/
class Utils
{
/** Random number generator value. */
private static int RND = (int)System.nanoTime();
/**
* LCG random number generator.
*
* @return A pseudo random number between 0 and 1023
*/
public static int rnd()
{
return (RND = RND * 1664525 + 1013904223) >>> 22;
}
/**
* Skips spaces in the given String.
*
* @param in Input String.
* @param start Starting position.
* @return The new position or -1 if EOL has been reached.
*/
public static int skipSpaces(final String in, final int start)
{
int pos = start;
while(pos < in.length() && (in.charAt(pos) == ' ' || in.charAt(pos) == '\n'))
pos++;
return pos < in.length() ? pos : -1;
}
/**
* Processed the given escape sequence.
*
* @param out The StringBuilder to write to.
* @param ch The character.
* @param pos Current parsing position.
* @return The new position.
*/
public static int escape(final StringBuilder out, final char ch, final int pos)
{
switch(ch)
{
case '\\':
case '[':
case ']':
case '(':
case ')':
case '{':
case '}':
case '#':
case '"':
case '\'':
case '.':
case '>':
case '<':
case '*':
case '+':
case '-':
case '_':
case '!':
case '`':
case '^':
out.append(ch);
return pos + 1;
default:
out.append('\\');
return pos;
}
}
/**
* Reads characters until any 'end' character is encountered.
*
* @param out The StringBuilder to write to.
* @param in The Input String.
* @param start Starting position.
* @param end End characters.
* @return The new position or -1 if no 'end' char was found.
*/
public static int readUntil(final StringBuilder out, final String in, final int start, final char... end)
{
int pos = start;
while(pos < in.length())
{
final char ch = in.charAt(pos);
if(ch == '\\' && pos + 1 < in.length())
{
pos = escape(out, in.charAt(pos + 1), pos);
}
else
{
boolean endReached = false;
for(int n = 0; n < end.length; n++)
{
if(ch == end[n])
{
endReached = true;
break;
}
}
if(endReached)
break;
out.append(ch);
}
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Reads characters until the 'end' character is encountered.
*
* @param out The StringBuilder to write to.
* @param in The Input String.
* @param start Starting position.
* @param end End characters.
* @return The new position or -1 if no 'end' char was found.
*/
public static int readUntil(final StringBuilder out, final String in, final int start, final char end)
{
int pos = start;
while(pos < in.length())
{
final char ch = in.charAt(pos);
if(ch == '\\' && pos + 1 < in.length())
{
pos = escape(out, in.charAt(pos + 1), pos);
}
else
{
if(ch == end)
break;
out.append(ch);
}
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Reads a markdown link.
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Starting position.
* @return The new position or -1 if this is no valid markdown link.
*/
public static int readMdLink(final StringBuilder out, final String in, final int start)
{
int pos = start;
int counter = 1;
while(pos < in.length())
{
final char ch = in.charAt(pos);
if(ch == '\\' && pos + 1 < in.length())
{
pos = escape(out, in.charAt(pos + 1), pos);
}
else
{
boolean endReached = false;
switch(ch)
{
case '(':
counter++;
break;
case ' ':
if(counter == 1)
endReached = true;
break;
case ')':
counter--;
if(counter == 0)
endReached = true;
break;
}
if(endReached)
break;
out.append(ch);
}
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Reads a markdown link ID.
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Starting position.
* @return The new position or -1 if this is no valid markdown link ID.
*/
public static int readMdLinkId(final StringBuilder out, final String in, final int start)
{
int pos = start;
int counter = 1;
while(pos < in.length())
{
final char ch = in.charAt(pos);
boolean endReached = false;
switch(ch)
{
case '\n':
out.append(' ');
break;
case '[':
counter++;
out.append(ch);
break;
case ']':
counter--;
if(counter == 0)
endReached = true;
else
out.append(ch);
break;
default:
out.append(ch);
break;
}
if(endReached)
break;
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Reads characters until any 'end' character is encountered, ignoring escape sequences.
*
* @param out The StringBuilder to write to.
* @param in The Input String.
* @param start Starting position.
* @param end End characters.
* @return The new position or -1 if no 'end' char was found.
*/
public static int readRawUntil(final StringBuilder out, final String in, final int start, final char... end)
{
int pos = start;
while(pos < in.length())
{
final char ch = in.charAt(pos);
boolean endReached = false;
for(int n = 0; n < end.length; n++)
{
if(ch == end[n])
{
endReached = true;
break;
}
}
if(endReached)
break;
out.append(ch);
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Reads characters until the end character is encountered, ignoring escape sequences.
*
* @param out The StringBuilder to write to.
* @param in The Input String.
* @param start Starting position.
* @param end End characters.
* @return The new position or -1 if no 'end' char was found.
*/
public static int readRawUntil(final StringBuilder out, final String in, final int start, final char end)
{
int pos = start;
while(pos < in.length())
{
final char ch = in.charAt(pos);
if(ch == end)
break;
out.append(ch);
pos++;
}
return (pos == in.length()) ? -1 : pos;
}
/**
* Appends the given string encoding special HTML characters.
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Input String starting position.
* @param end Input String end position.
*/
public static void appendCode(final StringBuilder out, final String in, final int start, final int end)
{
for(int i = start; i < end; i++)
{
final char c;
switch(c = in.charAt(i))
{
case '&':
out.append("&amp;");
break;
case '<':
out.append("&lt;");
break;
case '>':
out.append("&gt;");
break;
default:
out.append(c);
break;
}
}
}
/**
* Appends the given string encoding special HTML characters (used in HTML attribute values).
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Input String starting position.
* @param end Input String end position.
*/
public static void appendValue(final StringBuilder out, final String in, final int start, final int end)
{
for(int i = start; i < end; i++)
{
final char c;
switch(c = in.charAt(i))
{
case '&':
out.append("&amp;");
break;
case '<':
out.append("&lt;");
break;
case '>':
out.append("&gt;");
break;
case '"':
out.append("&quot;");
break;
case '\'':
out.append("&apos;");
break;
default:
out.append(c);
break;
}
}
}
/**
* Append the given char as a decimal HTML entity.
*
* @param out The StringBuilder to write to.
* @param value The character.
*/
public static void appendDecEntity(final StringBuilder out, final char value)
{
out.append("&#");
out.append((int)value);
out.append(';');
}
/**
* Append the given char as a hexadecimal HTML entity.
*
* @param out The StringBuilder to write to.
* @param value The character.
*/
public static void appendHexEntity(final StringBuilder out, final char value)
{
out.append("&#x");
out.append(Integer.toHexString(value));
out.append(';');
}
/**
* Appends the given mailto link using obfuscation.
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Input String starting position.
* @param end Input String end position.
*/
public static void appendMailto(final StringBuilder out, final String in, final int start, final int end)
{
for(int i = start; i < end; i++)
{
final char c;
final int r = rnd();
switch(c = in.charAt(i))
{
case '&':
case '<':
case '>':
case '"':
case '\'':
case '@':
if(r < 512)
appendDecEntity(out, c);
else
appendHexEntity(out, c);
break;
default:
if(r < 32)
out.append(c);
else if(r < 520)
appendDecEntity(out, c);
else
appendHexEntity(out, c);
break;
}
}
}
/**
* Extracts the tag from an XML element.
*
* @param out The StringBuilder to write to.
* @param in Input StringBuilder.
*/
public static void getXMLTag(final StringBuilder out, final StringBuilder in)
{
int pos = 1;
if(in.charAt(1) == '/')
pos++;
while(Character.isLetterOrDigit(in.charAt(pos)))
{
out.append(in.charAt(pos++));
}
}
/**
* Extracts the tag from an XML element.
*
* @param out The StringBuilder to write to.
* @param in Input String.
*/
public static void getXMLTag(final StringBuilder out, final String in)
{
int pos = 1;
if(in.charAt(1) == '/')
pos++;
while(Character.isLetterOrDigit(in.charAt(pos)))
{
out.append(in.charAt(pos++));
}
}
/**
* Reads an XML element.
*
* @param out The StringBuilder to write to.
* @param in Input String.
* @param start Starting position.
* @param safeMode Whether to escape unsafe HTML tags or not
* @return The new position or -1 if this is no valid XML element.
*/
public static int readXML(final StringBuilder out, final String in, final int start, final boolean safeMode)
{
int pos;
final boolean isCloseTag;
try {
if(in.charAt(start + 1) == '/')
{
isCloseTag = true;
pos = start + 2;
}
else if(in.charAt(start + 1) == '!')
{
out.append("<!");
return start + 1;
}
else
{
isCloseTag = false;
pos = start + 1;
}
if(safeMode)
{
final StringBuilder temp = new StringBuilder();
pos = readRawUntil(temp, in, pos, ' ', '/', '>');
if(pos == -1) return -1;
final String tag = temp.toString().trim().toLowerCase();
if(HTML.isUnsafeHtmlElement(tag))
{
out.append("&lt;");
if(isCloseTag)
out.append('/');
out.append(temp);
}
}
else
{
out.append('<');
if(isCloseTag)
out.append('/');
pos = readRawUntil(out, in, pos, ' ', '/', '>');
}
if(pos == -1) return -1;
pos = readRawUntil(out, in, pos, '/', '>');
if(in.charAt(pos) == '/')
{
out.append(" /");
pos = readRawUntil(out, in, pos + 1, '>');
if(pos == -1)
return -1;
}
if(in.charAt(pos) == '>')
{
out.append('>');
return pos;
}
} catch (StringIndexOutOfBoundsException e) {
return -1;
}
return -1;
}
}