Refactored the overall process, fixed #0005: link behaviour.

* Refactored the overall process flow. Instead of ``JLPMain`` handling the
  process, it now reads the command line options and defers to ``Processor`` to
  handle the actual process. The ``Processor`` instance is responsible for
  processing one batch of input files and holds all the state that is common to
  this process.
* ``JLPBaseGenerator`` and generators based on it are now only responsible for
  handling one file, generating output from a source AST. As a consequence
  state that is common to the overall process is no longer stored in the
  generator but is stored on the ``Processor`` instance, which is exposed to the
  generators.
* Generators can now be instantiated directly (instead of having just a public
  static method) and are no longer one-time use. Now the life of a generator is
  expected to be the same as the life of the ``Processor``.
* Fixed inter-doc link behaviour.
* Created some data classes to replace the ad-hoc maps used to store state in
  the generator (now in the ``Processor``)
This commit is contained in:
Jonathan Bernard 2011-09-09 14:28:48 -05:00
parent e6d515fc96
commit d31d17d1e2
8 changed files with 267 additions and 107 deletions

25
doc/issues/0005ts3.rst Normal file
View File

@ -0,0 +1,25 @@
Internal links are not smart enough about cross-file linking.
=============================================================
There are two main problems with internal linking as it works now:
1. The links are resolved relative to the directory of the file being processed
when they should be resolved relative to the root output directory.
For example, consider ``@org id1`` defined in ``dir/file1.src``, then
referenced in ``dir/file2.src`` in this manner: ``[link](jlp://id1``. The
url written in ``dir/file2.html`` is ``dir/file1.html#id1``. However, when
the page is viewed, this url is interpreted relative to the directory of the
current page. So if the docs live at ``file:///docs`` then the url will
resolve to ``file:///docs/dir/dir/file1.src#id1`` instead of
``file:///docs/dir/file1.html#id1``.
2. The links do substitute the ``html`` suffix to the file names in place of the
files' suffixes. In the above example note that the actual urls end in
``.src`` like the original input files, but the expected url ends in
``.html``.
========= ==========
Created: 2011-09-08
Resolved: 2011-09-09
========= ==========

View File

@ -1,6 +1,6 @@
#Thu, 08 Sep 2011 12:27:26 -0500 #Fri, 09 Sep 2011 14:26:03 -0500
name=jlp name=jlp
version=0.2 version=0.3
build.number=1 build.number=1
lib.local=true lib.local=true
release.dir=release release.dir=release

View File

@ -6,36 +6,18 @@ import java.util.Map
public abstract class JLPBaseGenerator { public abstract class JLPBaseGenerator {
protected Map docState protected Processor processor
protected JLPBaseGenerator() { protected JLPBaseGenerator(Processor processor) {
docState = [orgs: [:], // stores `@org` references this.processor = processor }
codeTrees: [:], // stores code ASTs for
currentDocId: false ] } // store the current docid
protected Map<String, String> generate(Map<String, SourceFile> sources) { protected String generate(SourceFile source) {
Map result = [:]
// run the parse phase // run the parse phase
sources.each { sourceId, sourceAST -> parse(source)
// set up the current generator state for this source
docState.currentDocId = sourceId
docState.codeTrees[sourceId] = sourceAST.codeAST
parse(sourceAST) }
// run the emit phase // run the emit phase
sources.each { sourceId, sourceAST -> return emit(source) }
// set up the current generator state for this source
docState.currentDocId = sourceId
// generate the doc for this source
result[sourceId] = emit(sourceAST) }
// return our results
return result }
protected void parse(SourceFile sourceFile) { protected void parse(SourceFile sourceFile) {
sourceFile.blocks.each { block -> parse(block) } } sourceFile.blocks.each { block -> parse(block) } }

View File

@ -7,12 +7,8 @@ import org.parboiled.parserunners.ReportingParseRunner
public class JLPMain { public class JLPMain {
private JLPPegParser parser
public static void main(String[] args) { public static void main(String[] args) {
JLPMain inst = new JLPMain()
// create command-line parser // create command-line parser
CliBuilder cli = new CliBuilder( CliBuilder cli = new CliBuilder(
usage: 'jlp [options] <src-file> <src-file> ...') usage: 'jlp [options] <src-file> <src-file> ...')
@ -20,7 +16,8 @@ public class JLPMain {
// define options // define options
cli.h('Print this help information.', longOpt: 'help', required: false) cli.h('Print this help information.', longOpt: 'help', required: false)
cli.o("Output directory (defaults to 'jlp-docs').", cli.o("Output directory (defaults to 'jlp-docs').",
longOpt: 'output-dir', required: false) longOpt: 'output-dir', args: 1, argName: "output-dir",
required: false)
cli._(longOpt: 'relative-path-root', args: 1, required: false, cli._(longOpt: 'relative-path-root', args: 1, required: false,
'Resolve all relative paths against this root.') 'Resolve all relative paths against this root.')
@ -56,67 +53,21 @@ public class JLPMain {
// get files passed in // get files passed in
def filenames = opts.getArgs() def filenames = opts.getArgs()
def inputFiles = (filenames.collect { filename ->
// parse input // create a File object
Map parsedFiles = filenames.inject([:]) { acc, filename -> File file = new File(filename)
// if this is a relative path, resolve it against our path root
if (!file.isAbsolute()) { file = new File(pathRoot, filename) }
// warn the user about files that do not exist
if (!file.exists()) {
System.err.println
"'${file.canonicalPath}' does not exist: ignored." }
// create the File object return file }).findAll { it.exists() }
File file = new File(filename)
// if this is a relative path, resolve it against our root path Processor.process(outputDir, css, inputFiles)
if (!file.isAbsolute()) { file = new File(pathRoot, filename) }
// parse the file, store the result
acc[filename] = inst.parse(file)
return acc }
// generate output
Map htmlDocs = LiterateMarkdownGenerator.generateDocuments(parsedFiles)
// write output files
htmlDocs.each { filename, html ->
// split the path into parts
def fileParts = filename.split(/[\.\/]/)
// default the subdirectory to the output directory
File subDir = outputDir
// if the input file was in a subdirectory, we want to mirror that
// structure here.
if (fileParts.length > 2) {
// find the relative subdirectory of this file
subDir = new File(outputDir, fileParts[0..-3].join('/'))
// create that directory if needed
if (!subDir.exists()) subDir.mkdirs()
}
// recreate the output filename
def outputFilename = fileParts[-2] + ".html"
// write the HTML to the file
new File(subDir, outputFilename).withWriter { fileOut ->
// write the CSS if it is not present
File cssFile = new File(subDir, "jlp.css")
if (!cssFile.exists()) cssFile.withWriter { cssOut ->
cssOut.println css }
// write the file
fileOut.println html } }
} }
public JLPMain() {
parser = Parboiled.createParser(JLPPegParser.class)
}
public SourceFile parse(File inputFile) {
def parseRunner = new ReportingParseRunner(parser.SourceFile())
// parse the file
return parseRunner.run(inputFile.text).resultValue
}
} }

View File

@ -0,0 +1,11 @@
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.Directive
public class LinkAnchor {
public String id
public Directive directive
public String sourceDocId
}

View File

@ -12,25 +12,21 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
protected PegDownProcessor pegdown protected PegDownProcessor pegdown
protected LiterateMarkdownGenerator() { public LiterateMarkdownGenerator(Processor processor) {
super() super(processor)
pegdown = new PegDownProcessor( pegdown = new PegDownProcessor(
Extensions.TABLES | Extensions.DEFINITIONS) } Extensions.TABLES | Extensions.DEFINITIONS) }
protected static Map<String, String> generateDocuments(
Map<String, SourceFile> sources) {
LiterateMarkdownGenerator inst = new LiterateMarkdownGenerator()
return inst.generate(sources) }
protected void parse(Directive directive) { protected void parse(Directive directive) {
switch(directive.type) { switch(directive.type) {
case DirectiveType.Org: case DirectiveType.Org:
def orgMap = [:] LinkAnchor anchor = new LinkAnchor(
orgMap.id = directive.value id: directive.value,
orgMap.directive = directive directive: directive,
orgMap.sourceDocId = docState.currentDocId sourceDocId: processor.currentDocId)
docState.orgs[directive.value] = orgMap
processor.linkAnchors[anchor.id] = anchor
break; break;
default: default:
break // do nothing break // do nothing
@ -48,7 +44,7 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
"""<!DOCTYPE html> """<!DOCTYPE html>
<html> <html>
<head> <head>
<title>${docState.currentDocId}</title> <title>${processor.currentDocId}</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" media="all" href="jlp.css"/> <link rel="stylesheet" media="all" href="jlp.css"/>
</head> </head>
@ -56,7 +52,7 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
<div id="container"> <div id="container">
<table cellpadding="0" cellspacing="0"> <table cellpadding="0" cellspacing="0">
<thead><tr> <thead><tr>
<th class="docs"><h1>${docState.currentDocId}</h1></th> <th class="docs"><h1>${processor.currentDocId}</h1></th>
<th class="code"/> <th class="code"/>
</tr></thead> </tr></thead>
<tbody>""") <tbody>""")
@ -187,14 +183,33 @@ public class LiterateMarkdownGenerator extends JLPBaseGenerator {
// replace internal `jlp://` links with actual links based on`@org` // replace internal `jlp://` links with actual links based on`@org`
// references // references
html = html.replaceAll(/jlp:\/\/([^\s"]+)/) { wholeMatch, linkId -> html = html.replaceAll(/jlp:\/\/([^\s"]+)/) { wholeMatch, linkId ->
def link = docState.orgs[linkId]
// Get the org data stored for this org id.
def link = processor.linkAnchors[linkId]
String newLink String newLink
if (!link) { if (!link) {
// We do not have any reference to this id.
/* TODO: log error */ /* TODO: log error */
newLink = "broken_link(${linkId})" } newLink = "broken_link(${linkId})" }
else if (docState.currentDocId == link.sourceDocId) {
// This link points to a location in this document.
else if (processor.currentDocId == link.sourceDocId) {
newLink = "#$linkId" } newLink = "#$linkId" }
else { newLink = "${link.sourceDocId}#${linkId}" }
// The link should point to a different document.
else {
thisDoc = processor.currentDoc
linkDoc = processor.docs[link.sourceDocId]
pathToLinkedDoc = processor.getRelativePath(
thisDoc.sourceFile.parentFile,
thatDoc.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}" }
return newLink } return newLink }

View File

@ -0,0 +1,166 @@
package com.jdblabs.jlp
import org.parboiled.BaseParser
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
/**
* 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.
*/
public class Processor {
public Map<String, LinkAnchor> linkAnchors = [:]
public Map<String, TargetDoc> docs = [:]
public String currentDocId = null
public File inputRoot
public File outputRoot
public String css
// shortcut for docs[currentDocId]
public TargetDoc currentDoc
protected Map<Class, BaseParser> parsers = [:]
protected Map<Class, JLPBaseGenerator> generators = [:]
public static void process(File outputDir, String css,
List<File> inputFiles) {
// find the closest common parent folder to all of the files
File inputDir = inputFiles.inject(inputFiles[0]) { commonRoot, file ->
getCommonParent(commonRoot, file) }
// create our processor instance
Processor inst = new Processor(
inputRoot: inputDir,
outputRoot: outputDir,
css: css)
// run the process
inst.process(inputFiles)
}
protected void process(inputFiles) {
// Remember that the data for the processing run was initialized by the
// constructor.
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.
processDocs {
// TODO: add logic to configure or autodetect the correct parser for
// each file
def parser = getParser(JLPPegParser)
def parseRunner = new ReportingParseRunner(parser.SourceFile())
currentDoc.sourceAST = parseRunner.run(
currentDoc.sourceFile.text).resultValue }
// generate output
processDocs {
// TODO: add logic to configure or autodetect the correct generator
// for each file
def generator = getGenerator(LiterateMarkdownGenerator)
currentDoc.output = generator.generate(currentDoc.sourceAST) }
// Write the output to the output directory
processDocs {
// create the path to 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
if (!outputDir.exists()) {
outputDir.mkdirs() }
// 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
(new File(outputRoot, relativePath)).withWriter {
it.print currentDoc.sourceFile.text }
// Write the output to the file.
outputFile.withWriter { it.println currentDoc.output } } }
protected def processDocs(Closure c) {
docs.each { docId, doc ->
currentDocId = docId
currentDoc = doc
return c() } }
/**
* Assuming our current directory is `root`, get the relative path to
* `file`.
*/
public static String getRelativeFilepath(File root, File file) {
// make sure our root is a directory
if (!root.isDirectory()) root= root.parentFile
def rootPath = root.canonicalPath.split('/')
def filePath = file.canonicalPath.split('/')
def relativePath = []
// find the point of divergence in the two paths
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
(i..<rootPath.length).each { relativePath << ".." }
// add the path from our common parent directory to our file
(i..<filePath.length).each { j -> relativePath << filePath[j] }
return relativePath.join('/') }
/**
* Find the common parent directory to the given files.
*/
public static File getCommonParent(File file1, File file2) {
def path1 = file1.canonicalPath.split('/')
def path2 = file2.canonicalPath.split('/')
def newPath = []
// build new commonPath based on matching paths so far
int i = 0
while (i < Math.min(path1.length, path2.length) &&
path1[i] == path2[i]) {
newPath << path2[i]
i++ }
return new File(newPath.join('/')) }
protected getGenerator(Class generatorClass) {
if (generators[generatorClass] == null) {
def constructor = generatorClass.getConstructor(Processor)
generators[generatorClass] = constructor.newInstance(this)
}
return generators[generatorClass] }
protected getParser(Class parserClass) {
if (parsers[parserClass] == null) {
parsers[parserClass] = Parboiled.createParser(parserClass) }
return parsers[parserClass] }
}

View File

@ -0,0 +1,10 @@
package com.jdblabs.jlp
import com.jdblabs.jlp.ast.SourceFile
public class TargetDoc {
public SourceFile sourceAST
public File sourceFile
public String output
}