Hidden files are ignored. Added --version option and logging.

* Added logging with SLF4J and Logback
* Added `--version` option.
* Mofidied the input file rules. When an input object is a directory, JLPMain is
  adding all the files in that directory and its subdirectories. Now JLPMain is
  ignoring hidden files in the directory and subdirs. A file named explicitly on
  the command line is still included regardless of if it is hidden or not.
* Documentation continues.
This commit is contained in:
Jonathan Bernard 2011-12-29 10:53:14 -06:00
parent f5c7ac64e3
commit bfc0c12127
22 changed files with 393 additions and 116 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
#Sun, 25 Dec 2011 23:23:16 -0600
#Wed, 28 Dec 2011 15:44:01 -0600
name=jlp
version=1.1
build.number=7
version=1.2
build.number=10
lib.local=true
release.dir=release
main.class=com.jdblabs.jlp.JLPMain

View File

@ -0,0 +1,17 @@
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.core.ConsoleAppender
import ch.qos.logback.core.FileAppender
import static ch.qos.logback.classic.Level.*
appender("stdout", ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%level %msg%n" }}
appender("logfile", FileAppender) {
file = "jlp.log"
encoder(PatternLayoutEncoder) {
pattern = "%date %level %logger{10} [%file:%line] %msg%n" }}
root(WARN, ["stdout"])
//logger("com.jdblabs.jlp", TRACE, ["file"])

View File

@ -9,15 +9,47 @@ import java.util.List
import java.util.Map
/**
* This class defines the interface for JLP Generators and implements basic
* functionality for the parse phase.
* @org jlp.jdb-labs.com/JLPBaseGenerator
*/
public abstract class JLPBaseGenerator {
/**
* The generator works in close conjunction with the JLP Processor.
* This tight coupling in intended for these two classes. The distiction
* between the two classes is scope. The Processor class is concerned with
* data and behavior common to the whole documentation process whereas the
* Generator is concerned only with one file. The Generator does have
* access to information about the overall process through this link to the
* Processor. One example of the need for this link is the resolution of
* JLP link targets which may be defined in a different file but referenced
* in the file the Generator is processing.
* @org jlp.jdb-labs.com/notes/processor-generator-coupling
*/
protected Processor processor
protected JLPBaseGenerator(Processor processor) {
this.processor = processor }
/**
* ### Generator phases
*
* There are two phases for JLP Generators: **parse** and **emit**.
*
* The **parse** phase allows the Generator to build up facts about the
* file being processed before emitting the documentation in the **emit**
* phase. There is a `parse` method for each `ASTNode` type. The default
* implementation for JLPBaseGenerator calls the `parse` methods in such a
* way that it visits each ASTNode in the file.
*
* The **emit** phase is where the Generator creates the documentation for
* each AST node. Unlike the parse phase, there is no default implementation
* for the emit phase as emitting the final result will be very dependant on
* the emitter.
*
* @org com.jdb-labs.jlp.JLPBaseGenerator/phases
*/
protected void parse(SourceFile sourceFile) {
sourceFile.blocks.each { block -> parse(block) } }

View File

@ -8,6 +8,8 @@ import com.jdblabs.jlp.ast.ASTNode
import com.jdblabs.jlp.ast.SourceFile
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* @api JLPMain is the entrypoint for the system. It is responsible for parsing
@ -16,6 +18,10 @@ import org.parboiled.parserunners.ReportingParseRunner
*/
public class JLPMain {
public static final String VERSION = "1.2"
private static Logger log = LoggerFactory.getLogger(JLPMain.class)
public static void main(String[] args) {
/// #### Define command-line options.
@ -33,18 +39,24 @@ public class JLPMain {
longOpt: 'css-file', args: 1, required: false, argName: 'css-file')
cli._(longOpt: 'relative-path-root', args: 1, required: false,
'Resolve all relative paths against this root.')
cli._(longOpt: 'version', 'Display the JLP version information.')
/// #### Parse the options.
def opts = cli.parse(args)
/// Display help if requested.
/// Display help and version information if requested.
if (opts.h) {
cli.usage()
return }
if (opts.version) {
println "JLP v$VERSION"
return }
/// Get the relative path root (or set to current directory if it was
/// not given)
def pathRoot = new File(opts."relative-path-root" ?: ".")
log.debug("Relative path root: '{}'.", pathRoot.canonicalPath)
/// If our root is non-existant we will print an error and exit.. This
/// is possible if a relative path root was passed as an option.
@ -63,6 +75,8 @@ public class JLPMain {
/// Create the output directory if it does not exist.
if (!outputDir.exists()) outputDir.mkdirs()
log.debug("Output directory: '{}'.", outputDir.canonicalPath)
/// Get the CSS theme to use. We will start by assuming the default will
/// be used.
def css = JLPMain.class.getResourceAsStream("/jlp.css")
@ -76,7 +90,10 @@ public class JLPMain {
cssFile = new File(pathRoot, cssFile.path) }
/// Finally, make sure the CSS file actually exists.
if (cssFile.exists()) { css = cssFile }
if (cssFile.exists()) {
css = cssFile
log.debug("Loading CSS from this file: '{}'.",
cssFile.canonicalPath) }
/// If it does not, we are going to warn the user and keep the
/// default.
@ -111,9 +128,9 @@ public class JLPMain {
/// If this file is a directory, we want to add all the files in it
/// to our input list, recursing into all the subdirectories and
/// adding their files as well.
/// adding their files as well. We will ignore hidden files.
if (file.isDirectory()) { file.eachFileRecurse {
if (it.isFile()) { inputFiles << it }}}
if (it.isFile() && !it.isHidden()) { inputFiles << it }}}
/// Not a directory, just add the file.
else { inputFiles << file } }

View File

@ -1,6 +1,17 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp;
import com.jdblabs.jlp.ast.SourceFile;
/**
* JLPParser is a simple interface. It has one method to return a parsed
* [`SourceFile`](jlp://jlp.jdb-labs.com/SourceFile) given an input string. It may
* be expanded in the future to be an abstract class implementing methods that
* take additional input for convenience.
* @org jlp.jdb-labs.com/JLPParser
*/
public interface JLPParser {
public SourceFile parse(String input); }

View File

@ -10,6 +10,10 @@ import org.parboiled.Rule;
import org.parboiled.annotations.*;
import org.parboiled.parserunners.ReportingParseRunner;
/**
*
* @org jlp.jdb-labs.com/JLPPegParser
*/
@BuildParseTree
public class JLPPegParser extends BaseParser<Object> implements JLPParser {
@ -38,77 +42,79 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SourceFile = (Block / DocBlock / CodeBlock)+
*
* SourceFile = (Block / DocBlock / CodeBlock)+
*
* Pushes a SourceFile node on the stack.
*/
public Rule SourceFile() {
return Sequence(
// At the start of processing a new SourceFile, we need to set up
// our internal state.
/// At the start of processing a new SourceFile, we need to set up
/// our internal state.
// Clear the line count.
/// Clear the line count.
clearLineCount(),
// Add the top-level SourceFile AST node.
/// Add the top-level SourceFile AST node.
push(new SourceFile()),
// A SourceFile is made up of one or more Blocks
/// A SourceFile is made up of one or more Blocks
OneOrMore(Sequence(
// All of these options result in one new Block pushed onto the
// stack.
/// All of these options result in one new Block pushed onto the
/// stack.
FirstOf(
// Match a whole Block. This pushes a Block on the stack.
/// Match a whole Block. This pushes a Block on the stack.
Block(),
// A standalone DocBlock. We will create an empty CodeBlock
// to pair with it, then push a new Block onto the stack
// made from the DocBlock and the empty CodeBlock
/// A standalone DocBlock. We will create an empty CodeBlock
/// to pair with it, then push a new Block onto the stack
/// made from the DocBlock and the empty CodeBlock
Sequence(
// 1. We need to remember the line number to create the
// Block
/// 1. We need to remember the line number to create the
/// Block
push(curLineNum),
// 2. Match the DocBlock.
/// 2. Match the DocBlock.
DocBlock(),
// 3. Create the empty CodeBlock.
/// 3. Create the empty CodeBlock.
push(new CodeBlock(curLineNum)),
// 4. Create the Block and push it onto the stack.
/// 4. Create the Block and push it onto the stack.
push(new Block((CodeBlock) pop(), (DocBlock) pop(),
popAsInt()))),
// A standalone CodeBlock. Similar to the standalone
// DocBlock, we will create an empty DocBlock to pair with
// the CodeBlock to make a Block, which is pushed onto the
// stack:
//
// *Note: With the way the parser is currently written,
// this will only match a CodeBlock that occurs
// before any DocBlock.*
/// A standalone CodeBlock. Similar to the standalone
/// DocBlock, we will create an empty DocBlock to pair with
/// the CodeBlock to make a Block, which is pushed onto the
/// stack:
///
/// *Note: With the way the parser is currently written,
/// this will only match a CodeBlock that occurs
/// before any DocBlock.*
Sequence(
// 1. Remember the line number for the Block.
/// 1. Remember the line number for the Block.
push(curLineNum),
// 2. Create the empty DocBlock.
/// 2. Create the empty DocBlock.
push(new DocBlock(curLineNum)),
// 3. Match the CodeBlock
/// 3. Match the CodeBlock
CodeBlock(),
// 4. Create the Block and push it onto the stack
/// 4. Create the Block and push it onto the stack
push(new Block((CodeBlock) pop(), (DocBlock) pop(),
popAsInt())))),
// pop the Block created by one of the above options and add it
// to the SourceFile
/// pop the Block created by one of the above options and add it
/// to the SourceFile
addBlockToSourceFile((Block) pop())))); }
/**
* Parses the rule:
* Block = DocBlock CodeBlock
*
* Block = DocBlock CodeBlock
*
* Pushes a Block onto the stack
*/
@ -117,14 +123,15 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
push(curLineNum),
DocBlock(), CodeBlock(),
// A DocBlock and a CodeBlock are pushed onto the stack by the
// above rules. Pop them off, along with the line number we pushed
// before that, and create a new Block node.
/// A DocBlock and a CodeBlock are pushed onto the stack by the
/// above rules. Pop them off, along with the line number we pushed
/// before that, and create a new Block node.
push(new Block((CodeBlock) pop(), (DocBlock) pop(), popAsInt()))); }
/**
* Parses the rule:
* DocBlock = SDocBlock / MDocBlock
*
* DocBlock = SDocBlock / MDocBlock
*
* Pushes a DocBlock onto the stack.
*/
@ -132,7 +139,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SDocBlock = (SDirective / SDocText)+
*
* SDocBlock = (SDirective / SDocText)+
*
* Pushes a DocBlock object onto the stack
*/
@ -145,7 +153,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MDocBlock = MDOC_START (MDirective / MDocText)+ MDOC_END
*
* MDocBlock = MDOC_START (MDirective / MDocText)+ MDOC_END
*
* Pushes a DocBlock object onto the stack
*/
@ -154,16 +163,17 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
push(new DocBlock(curLineNum)),
MDOC_START,
ZeroOrMore(Sequence(
// We need to be careful to exclude MDOC_END here, as there can
// be some confusion otherwise between the start of a line with
// MDOC_LINE_START and MDOC_END depending on what values the
// user has chosen for them
/// We need to be careful to exclude MDOC_END here, as there can
/// be some confusion otherwise between the start of a line with
/// MDOC_LINE_START and MDOC_END depending on what values the
/// user has chosen for them
TestNot(MDOC_END), FirstOf(MDirective(), MDocText()),
addToDocBlock((ASTNode) pop()))),
MDOC_END); }
/**
* Parses the rule:
* CodeBlock = (RemainingCodeLine)+
*
* CodeBlock = (RemainingCodeLine)+
*
* Pushes a CodeBlock onto the stack.
*/
@ -175,7 +185,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SDirective = SDocLineStart AT (SLongDirective / SShortDirective)
*
* SDirective = SDocLineStart AT (SLongDirective / SShortDirective)
*
* Pushes a Directive node on the stack.
*/
@ -185,7 +196,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MDirective = MDocLineStart? AT (MLongDirective / MShortDirective)
*
* MDirective = MDocLineStart? AT (MLongDirective / MShortDirective)
*
* Pushes a Directive node onto the stack.
*/
@ -196,7 +208,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SLongDirective =
*
* SLongDirective =
* (API_DIR / EXAMPLE_DIR) RemainingSDocLine SDocText?
*
* Pushes a Directive node onto the stack.
@ -217,7 +230,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MLongDirective =
*
* MLongDirective =
* (API_DIR / EXAMPLE_DIR) RemainingMDocLine MDocText?
*
* Pushes a Directive node onto the stack.
@ -238,7 +252,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SShortDirective = (AUTHOR_DIR / ORG_DIR / COPYRIGHT_DIR) RemainingSDocLine
*
* SShortDirective = (AUTHOR_DIR / ORG_DIR / COPYRIGHT_DIR) RemainingSDocLine
*
* Pushes a Directive node onto the stack.
*/
@ -252,7 +267,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MShortDirective = (AUTHOR_DIR / ORG_DIR / COPYRIGHT_DIR) RemainingMDocLine
*
* MShortDirective = (AUTHOR_DIR / ORG_DIR / COPYRIGHT_DIR) RemainingMDocLine
*
* Pushes a Directive node onto the stack.
*/
@ -266,7 +282,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SDocText = (SDocLineStart !AT RemainingSDocLine)+
*
* SDocText = (SDocLineStart !AT RemainingSDocLine)+
*
* Pushes a DocText node onto the stack.
*/
@ -279,7 +296,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MDocText = (MDocLineStart? !AT RemainingMDocLine)+
*
* MDocText = (MDocLineStart? !AT RemainingMDocLine)+
*
* Pushes a DocText node onto the stack.
*/
@ -293,7 +311,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* SDocLineStart = SPACE* SDOC_START SPACE?
*
* SDocLineStart = SPACE* SDOC_START SPACE?
*/
Rule SDocLineStart() {
return Sequence(
@ -301,7 +320,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* MDocLineStart = SPACE* !MDOC_END MDOC_LINE_START SPACE?
*
* MDocLineStart = SPACE* !MDOC_END MDOC_LINE_START SPACE?
*/
Rule MDocLineStart() {
return Sequence(
@ -309,7 +329,8 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* RemainingSDocLine = ((!EOL)* EOL) / ((!EOL)+ EOI)
*
* RemainingSDocLine = ((!EOL)* EOL) / ((!EOL)+ EOI)
*/
Rule RemainingSDocLine() {
return FirstOf(
@ -318,30 +339,32 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
/**
* Parses the rule:
* RemainingMDocLine =
*
* RemainingMDocLine =
* ((!(EOL / MDOC_END))* EOL) /
* ((!MDOC_END)+)
*/
Rule RemainingMDocLine() {
return FirstOf(
// End of line, still within the an M-style comment block
/// End of line, still within the an M-style comment block
Sequence(
ZeroOrMore(Sequence(TestNot(FirstOf(EOL, MDOC_END)), ANY)),
EOL,
incLineCount()),
// End of M-style comment block
/// End of M-style comment block
OneOrMore(Sequence(TestNot(MDOC_END), ANY))); }
/**
* Parses the rule:
* RemainingCodeLine =
*
* RemainingCodeLine =
* ((!(EOL / MDOC_START / SDocLineStart))* EOL) /
* (!(MDOC_START / SDocLineStart))+
*/
Rule RemainingCodeLine() {
return FirstOf(
// End of line, still within the code block.
/// End of line, still within the code block.
Sequence(
ZeroOrMore(Sequence(
TestNot(FirstOf(EOL, MDOC_START, SDocLineStart())),
@ -349,7 +372,7 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
EOL,
incLineCount()),
// Found an MDOC_START or SDocLineStart
/// Found an MDOC_START or SDocLineStart
OneOrMore(Sequence(TestNot(FirstOf(MDOC_START, SDocLineStart())), ANY))); }
Rule AT = Ch('@').label("AT");
@ -357,13 +380,13 @@ public class JLPPegParser extends BaseParser<Object> implements JLPParser {
Rule NOT_EOL = Sequence(TestNot(EOL), ANY).label("NOT_EOL");
Rule SPACE = AnyOf(" \t").label("SPACE");
// Configurable
/// Configurable
Rule MDOC_START;
Rule MDOC_END;
Rule MDOC_LINE_START;
Rule SDOC_START;
// directive terminals
/// directive terminals
Rule AUTHOR_DIR = IgnoreCase("author");
Rule COPYRIGHT_DIR = IgnoreCase("copyright");
Rule API_DIR = IgnoreCase("api");

View File

@ -1,10 +1,28 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.Directive
/**
* A *LinkAnchor* in the documentation is very similar to an HTML anchor. It
* creates a reference in the documentation that can used by the author to
* link to this point.
*
* The author uses a URL with the `jlp` protocol to refer to anchors. For
* example: `jlp://jlp.jdb-labs.com/LinkAnchor` refers to this documentation.
*
* @api LinkAnchor is a data class to hold information about the link anchors
* defined by `@org` directives.
* @org jlp.jdb-labs.com/LinkAnchor
*/
public class LinkAnchor {
/// The anchor id. This comes from the text after the directive.
public String id
public Directive directive
public String sourceDocId

View File

@ -13,10 +13,14 @@ import org.pegdown.PegDownProcessor
import java.util.List
/**
* The LiterateMarkdownGenerator is an implementation of JLPBaseGenerator that
* uses [Markdown](http://daringfireball.net/projects/markdown/) to process the
* documentation into HTML output.
* @org jlp.jdb-labs.com/LiterateMarkdownGenerator
*/
public class LiterateMarkdownGenerator extends JLPBaseGenerator {
/// We will use the PegDown library for generating the Markdown output.
protected PegDownProcessor pegdown
public LiterateMarkdownGenerator(Processor processor) {
@ -25,6 +29,13 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
pegdown = new PegDownProcessor(
Extensions.TABLES | Extensions.DEFINITIONS) }
// ===================================
/** ### Parse phase implementation. ### */
// ===================================
/** Implement the parse phase for *Directive* nodes. We are interested
* specifically in saving the link anchor information from *org*
* directives. */
protected void parse(Directive directive) {
switch(directive.type) {
case DirectiveType.Org:
@ -39,14 +50,23 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
break // do nothing
} }
/** We are not doing any parsing for *CodeBlocks* or *DocTexts*. We have to
* implement them, as they are abstract on JLPBaseGenerator. */
protected void parse(CodeBlock codeBlock) {} // nothing to do
protected void parse(DocText docText) {} // nothing to do
// ==================================
/** ### Emit phase implementation. ### */
// ==================================
/** @api Emit a *SourceFile*.
* Each *SourceFile* becomes one ouput HTML file. This method is the entry
* point for a file to be emitted. */
protected String emit(SourceFile sourceFile) {
StringBuilder sb = new StringBuilder()
// Create the HTML file head
/// Create the HTML head and begin the body.
sb.append(
"""<!DOCTYPE html>
<html>
@ -64,10 +84,10 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
</tr></thead>
<tbody>""")
// Emit the document to Markdown
/// Emit all of the blocks in the body of the html file.
sourceFile.blocks.each { block -> sb.append(emit(block)) }
// Create the HTML file foot
/// Create the HTML footer.
sb.append(
""" </tbody>
</table>
@ -77,39 +97,47 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
return sb.toString() }
/** @api Emit a *Block*. */
protected String emit(Block block) {
StringBuilder sb = new StringBuilder()
// Look for an `@org` directive in the `Block`
/// Look for an `@org` directive in the `Block` (already found in the
/// parse phase)..
Directive orgDir = block.docBlock.directives.find {
it.type == DirectiveType.Org }
// Create the `tr` that will hold the `Block`
/// Create the `tr` that will hold the `Block`. If we found an `@org`
/// directive we will add the id here.
if (orgDir) { sb.append("\n<tr id='${orgDir.value}'>") }
else { sb.append("<tr>") }
// Create the `td` for the documentation.
/// Create the `td` for the documentation.
sb.append('\n<td class="docs">')
sb.append(emit(block.docBlock))
sb.append('</td>')
// Create the `td` for the `CodeBlock`
/// Create the `td` for the `CodeBlock`
sb.append('\n<td class="code">')
sb.append(emit(block.codeBlock))
sb.append('</td>')
// Close the table row.
/// Close the table row.
sb.append('</tr>') }
/** @api Emit a *DocBlock*. */
protected String emit(DocBlock docBlock) {
// Create a queue for the doc block elements, we are going to
// sort them by type and line number
/// Create a queue for the doc block elements, we are going to
/// sort them by type and line number
List emitQueue
// Later we will need a string builder to hold our result.
/// Later we will need a string builder to hold our result.
StringBuilder sb
// Add all the directives
/** Add all the directives. We are also assigning priorities here that
* we will use along with the line numbers to sort the elements. Our
* goal is to preserve the order of doc blocks and doc-block-like
* elements (examples, api documentation, etc.) while pushing orgs,
* authorship and other directives to the top. */
emitQueue = docBlock.directives.collect { directive ->
def queueItem = [lineNumber: directive.lineNumber, value: directive]
switch(directive.type) {
@ -121,90 +149,98 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
return queueItem }
// Add all the doc text blocks
/// Add all the doc text blocks.
emitQueue.addAll(docBlock.docTexts.collect { docText ->
[lineNumber: docText.lineNumber, priority: 50, value: docText] })
// Sort the emit queue by priority, then line number.
/// Sort the emit queue by priority, then line number.
emitQueue.sort(
{i1, i2 -> i1.priority != i2.priority ?
i1.priority - i2.priority :
i1.lineNumber - i2.lineNumber} as Comparator)
// Finally, we want to treat the whole block as one markdown chunk, so
// we will concatenate the values in the emit queue and then process
// the whole block once
/** Finally, we want to treat the whole block as one markdown chunk so
* we will concatenate the values in the emit queue and then send the
* whole block at once to the markdown processor. */
sb = new StringBuilder()
emitQueue.each { queueItem -> sb.append(emit(queueItem.value)) }
return processMarkdown(sb.toString())
}
/** @api Emit a *CodeBlock*. */
protected String emit(CodeBlock codeBlock) {
def codeLines
// Collect the lines into an array.
/// Collect the lines into an array.
codeLines = codeBlock.lines.collect { lineNumber, line ->
[lineNumber, line] }
// Sort by line number.
/// Sort by line number.
codeLines.sort({ i1, i2 -> i1[0] - i2[0] } as Comparator)
codeLines = codeLines.collect { arr -> arr[1] }
// write out the lines in a <pre><code> block
/// Write out the lines in a <pre><code> block
return "<pre><code>${codeLines.join('')}</code></pre>" }
/** @api Emit a *DocText*. */
protected String emit(DocText docText) { return docText.value }
/** @api Emit a *Directive*. */
protected String emit(Directive directive) {
switch(directive.type) {
// An `@api` directive is immediately processed and wrapped in a
// div (we need to process this now because Markdown does not
// process input inside HTML elements).
/** An `@api` directive is immediately processed and wrapped in a
* div (we need to process this now because Markdown does not
* process input inside HTML elements). */
case DirectiveType.Api:
return "<div class='api'>" +
processMarkdown(directive.value) + "</div>\n"
// An `@author` directive is turned into a definition list.
/// `@author` directive is turned into a definition list.
case DirectiveType.Author:
return "Author\n: ${directive.value}\n"
case DirectiveType.Copyright:
return "\n&copy; ${directive.value}\n"
// An `@example` directive is returned as is
/// An `@example` directive is returned as is.
case DirectiveType.Example:
return directive.value
// An `@org` directive is ignored.
case DirectiveType.Org: return "" }
}
/// An `@org` directive is ignored here. We already emitted the id
/// when we started the block.
case DirectiveType.Org: return "" } }
/** This is a helper method to process a block of text as Markdown. We need
* to do some additional processing to deal with `jlp://` org links that
* may be present. */
protected String processMarkdown(String markdown) {
// convert to HTML from Markdown
/// Convert to HTML from Markdown
String html = pegdown.markdownToHtml(markdown)
// replace internal `jlp://` links with actual links based on`@org`
// references
html = html.replaceAll(/jlp:\/\/([^\s"]+)/) { wholeMatch, linkId ->
/// Replace internal `jlp://` links with actual links based on`@org`
/// references.
html = html.replaceAll(/href=['"]jlp:\/\/([^\s"]+)['"]/) { wholeMatch, linkId ->
// Get the org data stored for this org id.
/// Get the org data we found in the parse phase for this org id.
def link = processor.linkAnchors[linkId]
String newLink
if (!link) {
// We do not have any reference to this id.
/* TODO: log error */
newLink = "broken_link(${linkId})" }
newLink = "href=\"broken_link(${linkId})\"" }
// This link points to a location in this document.
/// This link points to a location in this document.
else if (processor.currentDocId == link.sourceDocId) {
newLink = "#$linkId" }
newLink = "href=\"#${linkId}\"" }
// The link should point to a different document.
/// The link should point to a different document.
else {
TargetDoc thisDoc = processor.currentDoc
TargetDoc linkDoc = processor.docs[link.sourceDocId]
@ -213,13 +249,12 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
thisDoc.sourceFile.parentFile,
linkDoc.sourceFile)
// The target document may not be in the same directory
// as us, backtrack to the (relative) top of our directory
// structure.
newLink = pathToLinkedDoc + ".html#${linkId}" }
/** The target document may not be in the same directory
* as us, backtrack to the (relative) top of our directory
* structure. */
newLink = 'href="' + pathToLinkedDoc + ".html#${linkId}\"" }
return newLink }
return html;
}
return html; }
}

View File

@ -1,7 +1,14 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.*
/**
* @org jlp.jdb-labs.com/MarkdownParser
*/
public class MarkdownParser implements JLPParser {
public SourceFile parse(String input) {

View File

@ -6,6 +6,8 @@ package com.jdblabs.jlp
import org.parboiled.BaseParser
import org.parboiled.Parboiled
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* Processor processes one batch of input files to create a set of output files.
@ -49,6 +51,8 @@ public class Processor {
protected Map<String, JLPParser> parsers = [:]
protected Map<String, JLPBaseGenerator> generators = [:]
private Logger log = LoggerFactory.getLogger(getClass())
/// ### Public Methods.
/// @org jlp.jdb-labs.com/Processor/public-methods
@ -97,14 +101,16 @@ public class Processor {
/// the parser for that file type and parse the file into an abstract
/// syntax tree (AST).
processDocs {
log.trace("Parsing '{}'.", currentDocId)
def parser = getParser(sourceTypeForFile(currentDoc.sourceFile))
// TODO: error detection
currentDoc.sourceAST = parser.parse(currentDoc.sourceFile.text) }
/// * Run our generator parse phase (see
/// jlp://com.jdb-labs.jlp.JLPBaseGenerator/phases for an explanation
/// of the generator phases).
/// [`JLPBaseGenerator`](jlp://com.jdb-labs.jlp.JLPBaseGenerator/phases)
/// for an explanation of the generator phases).
processDocs {
log.trace("Second-pass parsing for '{}'.", currentDocId)
def generator = getGenerator(sourceTypeForFile(currentDoc.sourceFile))
// TODO: error detection
generator.parse(currentDoc.sourceAST) }
@ -112,6 +118,7 @@ public class Processor {
/// * Second pass by the generators, the emit phase.
processDocs {
log.trace("Emitting documentation for '{}'.", currentDocId)
def generator = getGenerator(sourceTypeForFile(currentDoc.sourceFile))
currentDoc.output = generator.emit(currentDoc.sourceAST) }
@ -125,6 +132,9 @@ public class Processor {
File outputFile = new File(outputRoot, relativePath + ".html")
File outputDir = outputFile.parentFile
log.trace("Saving output for '{}' to '{}'",
currentDocId, outputFile)
/// Create the directory for this file if it does not exist.
if (!outputDir.exists()) { outputDir.mkdirs() }

View File

@ -1,10 +1,22 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.SourceFile
/**
* @api TargetDoc is a data class to hold information about the output file.
* @org jlp.jdb-labs.com/TargetDoc
*/
public class TargetDoc {
/// The result of parsing the input file.
public SourceFile sourceAST
/// The original source file.
public File sourceFile
public String output
}

View File

@ -1,7 +1,16 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
/**
* This defines the interface for nodes of the source file parse tree.
* @org jlp.jdb-labs.com/ast/ASTNode
*/
public class ASTNode {
/// This is the line number the element began on.
protected int lineNumber
public ASTNode(int lineNum) { this.lineNumber = lineNum }

View File

@ -1,5 +1,13 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
/**
* ASTNode for *Block*s.
* @org jlp.jdb-labs.com/ast/Block
*/
public class Block extends ASTNode {
public final DocBlock docBlock
public final CodeBlock codeBlock

View File

@ -1,7 +1,16 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
* @org jlp.jdb-labs.com/ast/CodeBlock
*/
package com.jdblabs.jlp.ast
import java.util.Map
/**
* @api ASTNode for *CodeBlock*s.
* @org jlp.jdb-labs.com/ast/CodeBlock
*/
public class CodeBlock extends ASTNode {
public Map<Integer, String> lines = [:]

View File

@ -1,7 +1,47 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
/**
* @api ASTNode for *Directive*s.
* A documentation directive allows the author to add metadata and processing
* instructions.
* @org jlp.jdb-labs.com/ast/Directive
*/
public class Directive extends ASTNode {
/**
* There are currently five types of directive currently available:
*
* API
* : The *API* directive allows the author to seperate the parts of the
* documentation that should be included in javadoc-style API
* documentation, as opposed to full literate code-style documentation.
*
* Author
* : The *Author* directive is used to specify the author of a set of
* documentation. It can be used to denote the author of an entire file
* when used in the first documentation block or just the author of a
* specific method when used in the documentation block before that
* method.
*
* Copyright
* : Similar to *Author*, this directive allows you to mark the copyright
* information for a set of documentation.
*
* Example
* : Used to mark an example in the documentation. The full doc block
* following the directive will be included inline as an example,
* typically typeset in a monospace font.
*
* Org
* : Used to create a link anchor in the documentation. The `jlp` protocol
* in a URL allows the author to link back to a link anchor. Refer to
* the [LinkAnchor](jlp://jlp.jdb-labs.com/LinkAnchor) documentation for
* more information about link anchors.
*/
public static enum DirectiveType {
Api, Author, Copyright, Example, Org;

View File

@ -1,8 +1,16 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
import java.util.ArrayList
import java.util.List
/**
* @api ASTNode for *DocBlock*s.
* @org jlp.jdb-labs.com/ast/DocBlock
*/
public class DocBlock extends ASTNode {
public List<Directive> directives = new ArrayList<Directive>()

View File

@ -1,5 +1,13 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
/**
* @api ASTNode for *DocText*s.
* @org jlp.jdb-labs.com/ast/DocText
*/
public class DocText extends ASTNode {
public String value

View File

@ -6,11 +6,24 @@ package com.jdblabs.jlp.ast
/**
* The top-level AST element. This represents a source file.
* @org jlp.jdb-labs.com/ast/SourceFile
*/
* @org jlp.jdb-labs.com/ast/SourceFile */
public class SourceFile {
/** The source file gets split into two parts during the initial parsing by
* the PEG grammer: a list of documentation blocks and a list of code
* blocks. Both are stored in the `Blocks` but the code block may get
* processed again if there is a language-specific parser involved for code
* awareness. In this case, we will keep a copy of the result of that
* parsing here as well. */
/// A list of the blocks in this `SourceFile`.
public List<ASTNode> blocks = []
/// The result from parsing the code in this source file. Currently there
/// are no language-specific parsers and this is always `null`.
public def codeAST
/// The id for this source file, currently set to the path name for the
/// input file relative to the input root.
public String id
}