
com.github.rjeschke.txtmark.Processor Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2011 René Jeschke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.rjeschke.txtmark;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
/**
* Markdown processor class.
*
*
* Example usage:
*
*
*
* String result = Processor.process("This is ***TXTMARK***");
*
*
*
* @author René Jeschke
*/
public class Processor
{
/** The reader. */
private final Reader reader;
/** The emitter. */
private final Emitter emitter;
/** The Configuration. */
final Configuration config;
/** Extension flag. */
private boolean useExtensions = false;
/**
* Constructor.
*
* @param reader
* The input reader.
*/
protected Processor( final Reader reader, final Configuration config )
{
this.reader = reader;
this.config = config;
this.useExtensions = config.forceExtendedProfile;
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 final 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 final static String process( final String input, final Configuration configuration )
{
if ( input == null )
{
return null;
}
try
{
return process( new StringReader( input ), configuration );
}
catch ( final 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 final 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 final 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 final static String process( final String input )
{
return process( input, Configuration.DEFAULT );
}
/**
* Transforms an input String into HTML.
*
* @param input
* The String to process.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final String input, final Decorator decorator )
{
return process( input, Configuration.builder()
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input String into HTML.
*
* @param input
* The String to process.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final File file )
throws IOException
{
return process( file, Configuration.DEFAULT );
}
/**
* Transforms an input file into HTML.
*
* @param file
* The File to process.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final File file, final Decorator decorator )
throws IOException
{
return process( file, Configuration.builder()
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input file into HTML.
*
* @param file
* The File to process.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final File file, final String encoding )
throws IOException
{
return process( file, Configuration.builder()
.setEncoding( encoding )
.build() );
}
/**
* Transforms an input file into HTML.
*
* @param file
* The File to process.
* @param encoding
* The encoding to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final File file, final String encoding, final Decorator decorator )
throws IOException
{
return process( file, Configuration.builder()
.setEncoding( encoding )
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input file into HTML.
*
* @param file
* The File to process.
* @param encoding
* The encoding to use.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final InputStream input )
throws IOException
{
return process( input, Configuration.DEFAULT );
}
/**
* Transforms an input stream into HTML.
*
* @param input
* The InputStream to process.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final InputStream input, final Decorator decorator )
throws IOException
{
return process( input, Configuration.builder()
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input stream into HTML.
*
* @param input
* The InputStream to process.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final InputStream input, final String encoding )
throws IOException
{
return process( input, Configuration.builder()
.setEncoding( encoding )
.build() );
}
/**
* Transforms an input stream into HTML.
*
* @param input
* The InputStream to process.
* @param encoding
* The encoding to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final InputStream input, final String encoding, final Decorator decorator )
throws IOException
{
return process( input, Configuration.builder()
.setEncoding( encoding )
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input stream into HTML.
*
* @param input
* The InputStream to process.
* @param encoding
* The encoding to use.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final Reader reader )
throws IOException
{
return process( reader, Configuration.DEFAULT );
}
/**
* Transforms an input stream into HTML.
*
* @param reader
* The Reader to process.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final 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 final static String process( final Reader reader, final Decorator decorator )
throws IOException
{
return process( reader, Configuration.builder()
.setDecorator( decorator )
.build() );
}
/**
* Transforms an input stream into HTML.
*
* @param reader
* The Reader to process.
* @param decorator
* The decorator to use.
* @param safeMode
* Set to true
to escape unsafe HTML tags.
* @return The processed String.
* @throws IOException
* if an IO error occurs
* @see Configuration#DEFAULT
*/
public final static String process( final Reader reader, final Decorator decorator, final boolean safeMode )
throws IOException
{
return process( reader, Configuration.builder()
.setDecorator( decorator )
.setSafeMode( safeMode )
.build() );
}
/**
* Reads all lines from our reader.
*
* Takes care of markdown link references.
*
*
* @return A Block containing all lines.
* @throws IOException
* If an IO error occurred.
*/
private Block readLines()
throws IOException
{
final Block block = new Block();
final StringBuilder sb = new StringBuilder( 80 );
int c = this.reader.read();
LinkRef lastLinkRef = null;
while ( c != -1 )
{
sb.setLength( 0 );
int pos = 0;
boolean eol = false;
while ( !eol )
{
switch ( c )
{
case -1:
eol = true;
break;
case '\n':
c = this.reader.read();
if ( c == '\r' )
{
c = this.reader.read();
}
eol = true;
break;
case '\r':
c = this.reader.read();
if ( c == '\n' )
{
c = this.reader.read();
}
eol = true;
break;
case '\t':
{
final int np = pos + ( 4 - ( pos & 3 ) );
while ( pos < np )
{
sb.append( ' ' );
pos++;
}
c = this.reader.read();
break;
}
default:
pos++;
sb.append( (char) c );
c = this.reader.read();
break;
}
}
final Line line = new Line();
line.value = sb.toString();
line.init();
// Check for link definitions
boolean isLinkRef = false;
String id = null, link = null, comment = null;
if ( !line.isEmpty && line.leading < 4 && line.value.charAt( line.leading ) == '[' )
{
line.pos = line.leading + 1;
// Read ID up to ']'
id = line.readUntil( ']' );
// Is ID valid and are there any more characters?
if ( id != null && line.pos + 2 < line.value.length() )
{
// Check for ':' ([...]:...)
if ( line.value.charAt( line.pos + 1 ) == ':' )
{
line.pos += 2;
line.skipSpaces();
// Check for link syntax
if ( line.value.charAt( line.pos ) == '<' )
{
line.pos++;
link = line.readUntil( '>' );
line.pos++;
}
else
{
link = line.readUntil( ' ', '\n' );
}
// Is link valid?
if ( link != null )
{
// Any non-whitespace characters following?
if ( line.skipSpaces() )
{
final char ch = line.value.charAt( line.pos );
// Read comment
if ( ch == '\"' || ch == '\'' || ch == '(' )
{
line.pos++;
comment = line.readUntil( ch == '(' ? ')' : ch );
// Valid linkRef only if comment is valid
if ( comment != null )
{
isLinkRef = true;
}
}
}
else
{
isLinkRef = true;
}
}
}
}
}
// To make compiler happy: add != null checks
if ( isLinkRef && id != null && link != null )
{
if ( id.toLowerCase()
.equals( "$profile$" ) )
{
this.emitter.useExtensions = this.useExtensions = link.toLowerCase()
.equals( "extended" );
lastLinkRef = null;
}
else
{
// Store linkRef and skip line
final LinkRef lr = new LinkRef( link, comment, comment != null && ( link.length() == 1 && link.charAt( 0 ) == '*' ) );
this.emitter.addLinkRef( id, lr );
if ( comment == null )
{
lastLinkRef = lr;
}
}
}
else
{
comment = null;
// Check for multi-line linkRef
if ( !line.isEmpty && lastLinkRef != null )
{
line.pos = line.leading;
final char ch = line.value.charAt( line.pos );
if ( ch == '\"' || ch == '\'' || ch == '(' )
{
line.pos++;
comment = line.readUntil( ch == '(' ? ')' : ch );
}
if ( comment != null )
{
lastLinkRef.title = comment;
}
lastLinkRef = null;
}
// No multi-line linkRef, store line
if ( comment == null )
{
line.pos = 0;
block.appendLine( line );
}
}
}
return block;
}
/**
* Initializes a list block by separating it into list item blocks.
*
* @param root
* The Block to process.
*/
private void initListBlock( final Block root )
{
Line line = root.lines;
line = line.next;
while ( line != null )
{
final LineType t = line.getLineType( this.useExtensions );
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, final boolean listMode )
{
Block block, list;
Line line = root.lines;
if ( listMode )
{
root.removeListIndent( this.useExtensions );
if ( this.useExtensions && root.lines != null && root.lines.getLineType( this.useExtensions ) != 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( this.useExtensions );
switch ( type )
{
case OTHER:
{
final boolean wasEmpty = line.prevEmpty;
while ( line != null && !line.isEmpty )
{
final LineType t = line.getLineType( this.useExtensions );
if ( ( listMode || this.useExtensions ) && ( t == LineType.OLIST || t == LineType.ULIST ) )
{
break;
}
if ( this.useExtensions && ( t == LineType.CODE || t == LineType.FENCED_CODE || t == LineType.PLUGIN ) )
{
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( this.useExtensions ) != 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 FENCED_CODE:
line = line.next;
while ( line != null )
{
if ( line.getLineType( this.useExtensions ) == LineType.FENCED_CODE )
{
break;
}
// TODO ... is this really necessary? Maybe add a special
// flag?
line = line.next;
}
if ( line != null )
{
line = line.next;
}
block = root.split( line != null ? line.previous : root.lineTail );
block.type = BlockType.FENCED_CODE;
block.meta = Utils.getMetaFromFence( block.lines.value );
block.lines.setEmpty();
if ( block.lineTail.getLineType( this.useExtensions ) == LineType.FENCED_CODE )
{
block.lineTail.setEmpty();
}
block.removeSurroundingEmptyLines();
break;
case PLUGIN:
line = line.next;
while ( line != null )
{
if ( line.getLineType( this.useExtensions ) == LineType.PLUGIN )
{
break;
}
// TODO ... is this really necessary? Maybe add a special
// flag?
line = line.next;
}
if ( line != null )
{
line = line.next;
}
block = root.split( line != null ? line.previous : root.lineTail );
block.type = BlockType.PLUGIN;
block.meta = Utils.getMetaFromFence( block.lines.value );
block.lines.setEmpty();
if ( block.lineTail.getLineType( this.useExtensions ) == LineType.PLUGIN )
{
block.lineTail.setEmpty();
}
block.removeSurroundingEmptyLines();
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( this.useExtensions );
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;
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();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy