Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: support for Hercule transclusion w/ backwards compatibility (WIP) #241

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion example.apib
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Some non-standard Markdown extensions are also supported, such as this informati
These extensions may change in the future as the [CommonMark specification](http://spec.commonmark.org/) defines a [standard extension syntax](https://github.com/jgm/CommonMark/wiki/Proposed-Extensions).
:::

<!-- include(example-include.md) -->
:[Hercule Syntax Include](example-include.md)

# Data Structures

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.3",
"chokidar": "^1.4.1",
"cli-color": "^1.1.0",
"hercule": "^3.0.1",
"pretty-error": "^1.2.0",
"drafter": "^1.0.0",
"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
129 changes: 55 additions & 74 deletions src/main.coffee
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
fs = require 'fs'
path = require 'path'
drafter = require 'drafter'
hercule = require 'hercule'

linkRegExp = new RegExp(/((^[\t ]*)?(?:(\:\[.*?]\((.*?)\))|(<!-- include\((.*?)\) -->)))/gm)
linkMatch = (match) -> match[4] || match[6]

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

# Legacy template names
Expand All @@ -20,34 +23,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, done) ->
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}, (err, output, paths) ->
if err then return done(err)
return done(null, paths[1..])

# Get the theme module for a given theme name
exports.getTheme = (name) ->
Expand Down Expand Up @@ -79,46 +61,45 @@ exports.render = (input, options, done) ->
options.themeVariables = variables
options.theme = 'olio'

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

# Drafter 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'
drafter.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) ->

# Drafter 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'
drafter.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 +129,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, { linkRegExp, linkMatch }, (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
29 changes: 20 additions & 9 deletions test/basic.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ describe 'API Blueprint Renderer', ->

assert.ok theme

it 'Should get a list of included files', ->
sinon.stub fs, 'readFileSync', -> 'I am a test file'
it 'Should get a list of included files', (done) ->
aglio.collectPaths blueprint, '.', (err, paths) ->
assert.equal err, null
assert.equal paths.length, 2
assert 'example-include.md' in paths
assert 'example-schema.json' in paths
done()

it 'Should return error on bad links', (done) ->
input = '''
# Title
<!-- include(test1.apib) -->
Expand All @@ -28,13 +34,9 @@ describe 'API Blueprint Renderer', ->
More content...
'''

paths = aglio.collectPathsSync input, '.'

fs.readFileSync.restore()

assert.equal paths.length, 2
assert 'test1.apib' in paths
assert 'test2.apib' in paths
aglio.collectPaths input, '.', (err, paths) ->
assert.ok err.message
done()

it 'Should render blank string', (done) ->
aglio.render '', template: 'default', locals: {foo: 1}, (err, html) ->
Expand Down Expand Up @@ -130,6 +132,15 @@ describe 'API Blueprint Renderer', ->

done()

it 'Should handle compile errors', (done) ->
sinon.stub process.stdin, 'read', -> '# Hello\n<!-- include(i-dont-exist.md) -->\n'

setTimeout -> process.stdin.emit 'readable', 1

aglio.compileFile '-', 'example-compiled.apib', (err) ->
assert.ok err.message
done()

it 'Should compile to stdout', (done) ->
sinon.stub console, 'log'

Expand Down