Skip to content

Commit

Permalink
Breaking: use Hercule for transclusion
Browse files Browse the repository at this point in the history
Internally Hercule tokenises files into content and link tokens. Only the link portion (excluding the identifying surrounding syntax) is parsed. In the simple case this is simply a file path.In this regard, Aglio syntax permits a strict subset of Hercule's with differing surrounding markers.

Hercule also has built-in circular dependency detection, to prevent the it hanging when provided with bad input.

- Remove: INCLUDE regular expression.
- New: add tokenising regular expression, link match function to extend Hercule's tokeniser with support for existing Aglio syntax.
- Update: compileFile, render to use hercule.transcludeString.
- Beaking: collectPathsSync removed in favour of collectPaths (async).
Hercule is written using streams, which are asynchronous. Without extension of Hercule's sync API (childProcess.spawnSync) which is significantly less performant, collectPathsSync becomes collectPaths (async).
- Docs: updated collectPaths to reflect async.
  • Loading branch information
James Ramsay committed Mar 13, 2016
1 parent ccd8c2a commit 62d1e95
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 82 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,16 @@ aglio.render(blueprint, options, function (err, html, warnings) {
### Reference
The following methods are available from the `aglio` library:

#### aglio.collectPathsSync (blueprint, includePath)
#### aglio.collectPaths (blueprint, includePath, callback)
Get a list of paths that are included in the blueprint. This list can be watched for changes to do things like live reload. The blueprint's own path is not included.

```javascript
var blueprint = '# GET /foo\n<-- include(example.json -->\n';
var watchPaths = aglio.collectPathsSync(blueprint, process.cwd())
aglio.collectPaths(blueprint, process.cwd(), function (err, watchPaths) {
if (err) return console.log(err);

console.log(watchPaths);
});
```

#### aglio.render (blueprint, options, callback)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"aglio-theme-olio": "^1.6.2",
"chokidar": "^1.4.1",
"cli-color": "^1.1.0",
"hercule": "^3.0.0-alpha.1",
"pretty-error": "^1.2.0",
"protagonist": "^1.2.2",
"serve-static": "^1.10.0",
Expand Down
14 changes: 7 additions & 7 deletions src/bin.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ exports.run = (argv=parser.argv, done=->) ->
socket.on 'request-refresh', ->
sendHtml socket

paths = aglio.collectPathsSync fs.readFileSync(argv.i, 'utf-8'), path.dirname(argv.i)
aglio.collectPaths fs.readFileSync(argv.i, 'utf-8'), path.dirname(argv.i), (err, paths) ->
watcher = chokidar.watch [argv.i].concat(paths)
watcher.on "change", (path) ->
console.log "Updated " + path
_html = null
sendHtml io

watcher = chokidar.watch [argv.i].concat(paths)
watcher.on "change", (path) ->
console.log "Updated " + path
_html = null
sendHtml io
done()

done()
else
# Render or Compile API Blueprint, requires input/output files
if not argv.i or not argv.o
Expand Down
128 changes: 55 additions & 73 deletions src/main.coffee
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
fs = require 'fs'
path = require 'path'
protagonist = require 'protagonist'
hercule = require 'hercule'


linkRegexp = new RegExp(/(^[\t ]*)?(\:\[.*?\]\((.*?)\))|()( *<!-- include\((.*)\) -->)/gm)
linkMatch = (match) -> _.get(match, '[3]') || _.get(match, '[6]')

INCLUDE = /( *)<!-- include\((.*)\) -->/gmi
ROOT = path.dirname __dirname

# Legacy template names
Expand All @@ -20,34 +24,13 @@ errMsg = (message, err) ->
err.message = "#{message}: #{err.message}"
return err

# Replace the include directive with the contents of the included
# file in the input.
includeReplace = (includePath, match, spaces, filename) ->
fullPath = path.join includePath, filename
lines = fs.readFileSync(fullPath, 'utf-8').replace(/\r\n?/g, '\n').split('\n')
content = spaces + lines.join "\n#{spaces}"

# The content can itself include other files, so check those
# as well! Beware of circular includes!
includeDirective path.dirname(fullPath), content

# Handle the include directive, which inserts the contents of one
# file into another. We find the directive using a regular expression
# and replace it using the method above.
includeDirective = (includePath, input) ->
input.replace INCLUDE, includeReplace.bind(this, includePath)

# Get a list of all paths from included files. This *excludes* the
# input path itself.
exports.collectPathsSync = (input, includePath) ->
exports.collectPaths = (input, includePath, cb) ->
paths = []
input.replace INCLUDE, (match, spaces, filename) ->
fullPath = path.join(includePath, filename)
paths.push fullPath

content = fs.readFileSync fullPath, 'utf-8'
paths = paths.concat exports.collectPathsSync(content, path.dirname(fullPath))
paths
hercule.transcludeString input, {relativePath: includePath, linkRegexp, linkMatch}, null, paths, (err, output) ->
if err then return cb(err)
return cb(null, paths)

# Get the theme module for a given theme name
exports.getTheme = (name) ->
Expand Down Expand Up @@ -80,45 +63,44 @@ exports.render = (input, options, done) ->
options.theme = 'olio'

# Handle custom directive(s)
input = includeDirective options.includePath, input

# Protagonist does not support \r ot \t in the input, so
# try to intelligently massage the input so that it works.
# This is required to process files created on Windows.
filteredInput = if not options.filterInput then input else
input
.replace(/\r\n?/g, '\n')
.replace(/\t/g, ' ')

benchmark.start 'parse'
protagonist.parse filteredInput, type: 'ast', (err, res) ->
benchmark.end 'parse'
if err
err.input = input
return done(errMsg 'Error parsing input', err)

try
theme = exports.getTheme options.theme
catch err
return done(errMsg 'Error getting theme', err)

# Setup default options if needed
for option in theme.getConfig().options or []
# Convert `foo-bar` into `themeFooBar`
words = (f[0].toUpperCase() + f.slice(1) for f in option.name.split('-'))
name = "theme#{words.join('')}"
options[name] ?= option.default

benchmark.start 'render-total'
theme.render res.ast, options, (err, html) ->
benchmark.end 'render-total'
if err then return done(err)

# Add filtered input to warnings since we have no
# error to return
res.warnings.input = filteredInput

done null, html, res.warnings
hercule.transcludeString input, {relativePath: options.includePath, linkRegexp, linkMatch}, (err, input) ->
# Protagonist does not support \r ot \t in the input, so
# try to intelligently massage the input so that it works.
# This is required to process files created on Windows.
filteredInput = if not options.filterInput then input else
input
.replace(/\r\n?/g, '\n')
.replace(/\t/g, ' ')

benchmark.start 'parse'
protagonist.parse filteredInput, type: 'ast', (err, res) ->
benchmark.end 'parse'
if err
err.input = input
return done(errMsg 'Error parsing input', err)

try
theme = exports.getTheme options.theme
catch err
return done(errMsg 'Error getting theme', err)

# Setup default options if needed
for option in theme.getConfig().options or []
# Convert `foo-bar` into `themeFooBar`
words = (f[0].toUpperCase() + f.slice(1) for f in option.name.split('-'))
name = "theme#{words.join('')}"
options[name] ?= option.default

benchmark.start 'render-total'
theme.render res.ast, options, (err, html) ->
benchmark.end 'render-total'
if err then return done(err)

# Add filtered input to warnings since we have no
# error to return
res.warnings.input = filteredInput

done null, html, res.warnings

# Render from/to files
exports.renderFile = (inputFile, outputFile, options, done) ->
Expand Down Expand Up @@ -148,14 +130,14 @@ exports.renderFile = (inputFile, outputFile, options, done) ->
# Compile markdown from/to files
exports.compileFile = (inputFile, outputFile, done) ->
compile = (input) ->
compiled = includeDirective path.dirname(inputFile), input

if outputFile isnt '-'
fs.writeFile outputFile, compiled, (err) ->
done err
else
console.log compiled
done null
hercule.transcludeString input, (err, compiled) ->
if err then return done(errMsg 'Error compiling output', err)
if outputFile isnt '-'
fs.writeFile outputFile, compiled, (err) ->
done err
else
console.log compiled
done null

if inputFile isnt '-'
fs.readFile inputFile, encoding: 'utf-8', (err, input) ->
Expand Down

0 comments on commit 62d1e95

Please sign in to comment.