Started documenting JLP with JLP.

This commit is contained in:
Jonathan Bernard
2011-12-27 12:02:45 -06:00
parent 1f9b6cc66d
commit f5c7ac64e3
12 changed files with 293 additions and 71 deletions

View File

@ -1,9 +1,16 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.*
import java.util.List
import java.util.Map
/**
* @org jlp.jdb-labs.com/JLPBaseGenerator
*/
public abstract class JLPBaseGenerator {
protected Processor processor

View File

@ -1,3 +1,7 @@
/**
* @author Jonathan Bernard
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.ASTNode
@ -5,11 +9,18 @@ import com.jdblabs.jlp.ast.SourceFile
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
/**
* @api JLPMain is the entrypoint for the system. It is responsible for parsing
* the command-line options and invoking the Processor.
* @org jlp.jdb-labs.com/JLPMain
*/
public class JLPMain {
public static void main(String[] args) {
// create command-line parser
/// #### Define command-line options.
/// We are using the Groovy wrapper around the Apache Commons CLI
/// library.
CliBuilder cli = new CliBuilder(
usage: 'jlp [options] <src-file> <src-file> ...')
@ -23,78 +34,91 @@ public class JLPMain {
cli._(longOpt: 'relative-path-root', args: 1, required: false,
'Resolve all relative paths against this root.')
// parse options
/// #### Parse the options.
def opts = cli.parse(args)
// display help if requested
/// Display help if requested.
if (opts.h) {
cli.usage()
return }
// get the relative path root (or set to current directory if not given)
/// Get the relative path root (or set to current directory if it was
/// not given)
def pathRoot = new File(opts."relative-path-root" ?: ".")
// fail if our root is non-existant
/// 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.
if (!pathRoot.exists() || !pathRoot.isDirectory()) {
System.err.println "'${pathRoot.path}' is not a valid directory."
System.exit(1) }
// get the output directory and create it if necessary
/// Get the output directory, either from the command line or by
/// default.
def outputDir = opts.o ? new File(opts.o) : new File("jlp-docs")
// resolve the output directory against our relative root
/// Resolve the output directory against our relative root
if (!outputDir.isAbsolute()) {
outputDir = new File(pathRoot, outputDir.path) }
// create the output directory if it does not exist
/// Create the output directory if it does not exist.
if (!outputDir.exists()) outputDir.mkdirs()
// get the CSS theme to use. We will start by assuming the default will
// be used.
/// Get the CSS theme to use. We will start by assuming the default will
/// be used.
def css = JLPMain.class.getResourceAsStream("/jlp.css")
// If the CSS file was specified on the command-line, let's look for it.
/// If the CSS file was specified on the command-line, let's look for it.
if (opts.'css-file') {
def cssFile = new File(opts.'css-file')
// resolve against our relative root
/// Resolve the file against our relative root.
if (!cssFile.isAbsolute()) {
cssFile = new File(pathRoot, cssFile.path) }
// Finally, make sure the file actually exists.
/// Finally, make sure the CSS file actually exists.
if (cssFile.exists()) { css = cssFile }
/// If it does not, we are going to warn the user and keep the
/// default.
else {
println "WARN: Could not fine the custom CSS file: '" +
"${cssFile.canonicalPath}'."
println " Using the default CSS." }}
// Extract the text from our css source (either an InputStream or a
// File)
/// Extract the text from our css source (either an InputStream or a
/// File)
css = css.text
// get files passed in
/// #### Create the input file list.
/// We will start with the filenames passed as arguments on the command
/// line.
def filenames = opts.getArgs()
def inputFiles = []
filenames.each { filename ->
// create a File object
File file = new File(filename)
// if this is a relative path, resolve it against our path root
/// For each filename we try to resolve it to an actual file
/// relative to our root.
File file = new File(filename)
if (!file.isAbsolute()) { file = new File(pathRoot, filename) }
// if this file does not exist, warn the user and skip it
/// If this file does not exist, warn the user and skip it.
if (!file.exists()) {
System.err.println(
"'${file.canonicalPath}' does not exist: ignored.")
return }
// if this file is a directory, add all the files in it (recurse
// into sub-directories and add their contents as well).
/// 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.
if (file.isDirectory()) { file.eachFileRecurse {
if (it.isFile()) { inputFiles << it }}}
/// Not a directory, just add the file.
else { inputFiles << file } }
/// #### Process the files.
Processor.process(outputDir, css, inputFiles)
}

View File

@ -1,3 +1,7 @@
/**
* @author Jonathan Bernard (jdb@jdb-labs.com)
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.*
@ -8,6 +12,9 @@ import org.pegdown.PegDownProcessor
import java.util.List
/**
* @org jlp.jdb-labs.com/LiterateMarkdownGenerator
*/
public class LiterateMarkdownGenerator extends JLPBaseGenerator {
protected PegDownProcessor pegdown

View File

@ -1,3 +1,7 @@
/**
* @author Jonathan Bernard
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp
import org.parboiled.BaseParser
@ -7,106 +11,142 @@ import org.parboiled.Parboiled
* Processor processes one batch of input files to create a set of output files.
* It holds the intermediate state needed by the generators and coordinates the
* work of the parsers and generators for each of the input files.
* @org jlp.jdb-labs.com/Processor
*/
public class Processor {
/// ### Public State
/// @org jlp.jdb-labs.com/Processor/public-state
/// A map of all the link anchors defined in the documents.
public Map<String, LinkAnchor> linkAnchors = [:]
/// A map of all the documents being processed.
public Map<String, TargetDoc> docs = [:]
/// The id of the document currently being processed.
public String currentDocId = null
/// The root of the input path.
public File inputRoot
/// The root of the output path.
public File outputRoot
/// The CSS that will be used for the resulting HTML documents. Note that
/// this is the CSS file contents, not the name of a CSS file.
public String css
// shortcut for docs[currentDocId]
/// A shortcut for `docs[currentDocId]`
public TargetDoc currentDoc
/// ### Non-public State
/// @org jlp.jdb-labs.com/Processor/non-public-state
/// Maps of all the parsers and generators by input file type. Parsers and
/// generators are both safe for re-use within a single thread, so we cache
/// them here.
protected Map<String, JLPParser> parsers = [:]
protected Map<String, JLPBaseGenerator> generators = [:]
/// ### Public Methods.
/// @org jlp.jdb-labs.com/Processor/public-methods
/**
* #### Processor.process
* @org jlp.jdb-labs.com/Processor/process
* @api Process the input files given, writing the resulting documentation
* to the directory named in `outputDir`, using the CSS given in `css`
*/
public static void process(File outputDir, String css,
List<File> inputFiles) {
// find the closest common parent folder to all of the files
/// Find the closest common parent folder to all of the files given.
/// This will be our input root for the parsing process.
File inputDir = inputFiles.inject(inputFiles[0]) { commonRoot, file ->
getCommonParent(commonRoot, file) }
// create our processor instance
/// Create an instance of this class with the options given.
Processor inst = new Processor(
inputRoot: inputDir,
outputRoot: outputDir,
css: css)
// run the process
inst.process(inputFiles)
}
/// Run the process.
inst.process(inputFiles) }
/// ### Non-Public implementation methods.
/// @org jlp.jdb-labs.com/Processor/non-public-methods
/**
* #### process
* @org jlp.jdb-labs.com/Processor/process2
*/
protected void process(inputFiles) {
// Remember that the data for the processing run was initialized by the
// constructor.
/// Remember that the data for the processing run was initialized by the
/// constructor.
/// * Create the processing context for each input file. We are using
/// the relative path of the file as the document id.
inputFiles.each { file ->
// set the current doc id
def docId = getRelativeFilepath(inputRoot, file)
// create the processing context for this file
docs[docId] = new TargetDoc(sourceFile: file) }
// Parse the input files.
/// * Run the parse phase on each of the files. For each file, we load
/// the parser for that file type and parse the file into an abstract
/// syntax tree (AST).
processDocs {
// TODO: add logic to configure or autodetect the correct parser for
// each file
def parser = getParser(sourceTypeForFile(currentDoc.sourceFile))
// TODO: error detection
currentDoc.sourceAST = parser.parse(currentDoc.sourceFile.text) }
// run our generator parse phase (first pass over the ASTs)
/// * Run our generator parse phase (see
/// jlp://com.jdb-labs.jlp.JLPBaseGenerator/phases for an explanation
/// of the generator phases).
processDocs {
// TODO: add logic to configure or autodetect the correct generator
// for each file
def generator =
getGenerator(sourceTypeForFile(currentDoc.sourceFile))
def generator = getGenerator(sourceTypeForFile(currentDoc.sourceFile))
// TODO: error detection
generator.parse(currentDoc.sourceAST) }
// Second pass by the generators, create output.
/// * Second pass by the generators, the emit phase.
processDocs {
// TODO: add logic to configure or autodetect the correct generator
// for each file
def generator =
getGenerator(sourceTypeForFile(currentDoc.sourceFile))
def generator = getGenerator(sourceTypeForFile(currentDoc.sourceFile))
currentDoc.output = generator.emit(currentDoc.sourceAST) }
// Write the output to the output directory
/// * Write the output to the output directory.
processDocs {
// create the path to the output file
/// Create the path and file object for the output file
String relativePath =
getRelativeFilepath(inputRoot, currentDoc.sourceFile)
File outputFile = new File(outputRoot, relativePath + ".html")
File outputDir = outputFile.parentFile
// create the directory if need be
/// Create the directory for this file if it does not exist.
if (!outputDir.exists()) { outputDir.mkdirs() }
// write the css file if it does not exist
/// Write the CSS file if it does not exist.
File cssFile = new File(outputDir, "jlp.css")
if (!cssFile.exists()) { cssFile.withWriter{ it.println css } }
// Copy the source file over
/// Copy the source file over.
// TODO: make this behavior customizable.
(new File(outputRoot, relativePath)).withWriter {
it.print currentDoc.sourceFile.text }
// Write the output to the file.
/// Write the output to the file.
outputFile.withWriter { it.println currentDoc.output } } }
/**
* #### processDocs
* A helper method to walk over every document the processor is aware of,
* setting up the `currentDocId` and `currentDoc` variables before calling
* the given closure.
* @org jlp.jdb-labs.com/Processor/processDocs
*/
protected def processDocs(Closure c) {
docs.each { docId, doc ->
currentDocId = docId
@ -115,33 +155,41 @@ public class Processor {
return c() } }
/**
* #### getRelativeFilepath
* Assuming our current directory is `root`, get the relative path to
* `file`.
* @org jlp.jdb-labs.com/Processor/getRelativeFilepath
*/
public static String getRelativeFilepath(File root, File file) {
// make sure our root is a directory
/// Make sure our root is a directory
if (!root.isDirectory()) root= root.parentFile
/// Split both paths into their individual parts.
def rootPath = root.canonicalPath.split('/')
def filePath = file.canonicalPath.split('/')
def relativePath = []
// find the point of divergence in the two paths
/// Find the point of divergence in the two paths by walking down their
/// parts until we find a pair that do not match.
int i = 0
while (i < Math.min(rootPath.length, filePath.length) &&
rootPath[i] == filePath[i]) { i++ }
// backtrack from our root to our common parent directory
/// Backtrack from our root to our newly-found common parent directory.
(i..<rootPath.length).each { relativePath << ".." }
// add the path from our common parent directory to our file
/// Add the remainder of the path from our common parent directory to
/// our file.
(i..<filePath.length).each { j -> relativePath << filePath[j] }
/// Reconstitute the parts into one string.
return relativePath.join('/') }
/**
* #### getCommonParent
* Find the common parent directory to the given files.
* @org jlp.jdb-labs.com/Processor/getCommonParent
*/
public static File getCommonParent(File file1, File file2) {
def path1 = file1.canonicalPath.split('/')
@ -158,13 +206,23 @@ public class Processor {
return new File(newPath.join('/')) }
/**
* #### sourceTypeForFile
* Lookup the source type for a given file. We do a lookup based on the file
* extension for file types we recognize.
* @org jlp.jdb-labs.com/Processor/sourceTypeForFile
*/
public static sourceTypeForFile(File sourceFile) {
/// First we need to find the file extension.
String extension
def nameParts = sourceFile.name.split(/\./)
/// If there is no extension, then this is a binary file.
if (nameParts.length == 1) { return 'binary' }
else { extension = nameParts[-1] }
/// Lookup the file type by extension
switch (extension) {
case 'c': case 'h': return 'c';
case 'c++': case 'h++': case 'cpp': case 'hpp': return 'c++';
@ -175,17 +233,33 @@ public class Processor {
case 'md': return 'markdown';
default: return 'unknown'; }}
/**
* #### getGenerator
* Get a generator for the given source file type.
* @org jlp.jdb-labs.com/Processor/getGenerator
*/
protected getGenerator(String sourceType) {
/// We lazily create the generators.
if (generators[sourceType] == null) {
switch(sourceType) {
/// So far, all languages are using the vanilla
///[`LiterateMarkdownGenerator`]
///(jlp://jlp.jdb-labs.com/LiterateMarkdownGenerator)
default:
generators[sourceType] =
new LiterateMarkdownGenerator(this) }}
return generators[sourceType] }
/**
* #### getParser
* Get a parser for the given source file type.
* @org jlp.jdb-labs.com/Processor/getParser
*/
protected getParser(String sourceType) {
/// We are lazily loading the parsers also.
if (parsers[sourceType] == null) {
/// We do have different parsers for different languages.
switch(sourceType) {
case 'erlang':
parsers[sourceType] = Parboiled.createParser(

View File

@ -1,5 +1,13 @@
/**
* @author Jonathan Bernard
* @copyright JDB Labs 2010-2011
*/
package com.jdblabs.jlp.ast
/**
* The top-level AST element. This represents a source file.
* @org jlp.jdb-labs.com/ast/SourceFile
*/
public class SourceFile {
public List<ASTNode> blocks = []
public def codeAST