Skip to content

Commit

Permalink
Add an include directive.
Browse files Browse the repository at this point in the history
This adds support for including files in your API Blueprint. For example:

```markdown
+ Response 200 (application/json)

    <!-- include(long-response.json) -->
```

This implements one part of #27 - the other part of adding links
to the menu is not included.
  • Loading branch information
danielgtaylor committed Dec 16, 2014
1 parent 95fb91d commit e558d2d
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 30 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased
* New logo
* Add support for [including files]
(https://github.com/danielgtaylor/aglio#including-files)

# 1.16.2 - 2014-11-18
* Update dependencies (chokidar, marked, protagonist, stylus)
Expand Down
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Features
* Asyncronous processing
* Multiple templates/themes
* Support for custom templates written in [Jade](http://jade-lang.com/)
* Include other documents in your blueprint
* Commandline executable `aglio -i api.md -o api.html`
* Live preview server `aglio -i api.md --server`
* Node.js library `require('aglio')`
Expand All @@ -28,6 +29,17 @@ Example output is generated from the [example API Blueprint](https://raw.github.
* Slate theme: [Single Page](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/slate.html) or [Multiple Pages](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/slate-multi.html) or [Collapsible](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/slate-collapsible.html)
* Cyborg theme: [Single Page](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/cyborg.html) or [Multiple Pages](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/cyborg-multi.html) or [Collapsible](http://htmlpreview.github.io/?https://raw.githubusercontent.com/danielgtaylor/aglio/blob/master/examples/cyborg-collapsible.html)

Including Files
---------------
It is possible to include other files in your blueprint by using a special include directive with a path to the included file relative to the current file's directory. Included files can be written in API Blueprint, Markdown or HTML (or JSON for response examples). Included files can include other files, so be careful of circular references.

```markdown
<!-- include(filename.md) -->
```

For tools that do not support this include directive it will just render out as an HTML comment. API Blueprint may support its own mechanism of including files in the future, and this syntax was chosen to not interfere with the [external documents proposal](https://github.com/apiaryio/api-blueprint/issues/20) while allowing `aglio` users to include documents today.


Installation & Usage
====================
There are two ways to use aglio: as an executable or as a library for Node.js.
Expand Down Expand Up @@ -133,15 +145,19 @@ aglio.getTemplates(function (err, names) {
});
```

#### aglio.collectPathsSync (blueprint, includePath)
Get a list of paths

#### aglio.render (blueprint, options, callback)
Render an API Blueprint string and pass the generated HTML to the callback. The `options` can either be an object of options or a simple template name or file path string. Available options are:

| Option | Type | Default | Description |
| ----------- | ------ | ------- | -------------------------------------------- |
| condenseNav | bool | `true` | Condense navigation links |
| filterInput | bool | `true` | Filter `\r` and `\t` from the input |
| locals | object | `{}` | Extra locals to pass to templates |
| template | string | |Template name or path to custom template file |
| Option | Type | Default | Description |
| ----------- | ------ | ------------- | -------------------------------------------- |
| condenseNav | bool | `true` | Condense navigation links |
| filterInput | bool | `true` | Filter `\r` and `\t` from the input |
| locals | object | `{}` | Extra locals to pass to templates |
| template | string | | Template name or path to custom template file |
| includePath | string | process.cwd() | Base directory for relative includes. |

```javascript
var blueprint = '...';
Expand Down
4 changes: 4 additions & 0 deletions example-include.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Included File
This is content that was included from another file! It's easy, simply use `include(filename)` in an HTML comment (`<!-- include... -->`).

Included files can include other files as well, allowing you to structure your API documentation as you see fit. Since Markdown supports inline HTML, the files you include can be *either* Markdown or HTML.
19 changes: 19 additions & 0 deletions example-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "array",
"maxItems": 50,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"image": {
"type": "string"
},
"joined": {
"type": "string",
"pattern": "\d{4}-\d{2}-\d{2}"
}
}
}
}
22 changes: 3 additions & 19 deletions example.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ And some code with no highlighting:
Foo bar baz
```

<!-- include(example-include.md) -->

# Group Notes
Group description (also with *Markdown*)

Expand Down Expand Up @@ -236,25 +238,7 @@ A list of users

+ Schema

{
"type": "array",
"maxItems": 50,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"image": {
"type": "string"
},
"joined": {
"type": "string",
"pattern": "\d{4}-\d{2}-\d{2}"
}
}
}
}
<!-- include(example-schema.json) -->

### Get users [GET]
Get a list of users. Example:
Expand Down
5 changes: 4 additions & 1 deletion src/bin.coffee
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
aglio = require './main'
chokidar = require 'chokidar'
clc = require 'cli-color'
fs = require 'fs'
http = require 'http'
chokidar = require 'chokidar'
path = require 'path'
parser = require('yargs')
.usage('Usage: $0 [options] -i infile [-o outfile -s]')
.example('$0 -i example.md -o output.html', 'Render to HTML')
Expand Down Expand Up @@ -47,6 +48,7 @@ exports.run = (argv=parser.argv, done=->) ->
filterInput: argv.f
condenseNav: argv.c
fullWidth: argv.w
includePath: path.dirname argv.i
locals:
livePreview: true

Expand Down Expand Up @@ -92,6 +94,7 @@ exports.run = (argv=parser.argv, done=->) ->
io.on "connection", () ->
console.log "Socket connected"

# TODO: Watch included files?
watcher = chokidar.watch(argv.i,
persistent: false
)
Expand Down
41 changes: 38 additions & 3 deletions src/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ moment = require 'moment'
path = require 'path'
protagonist = require 'protagonist'

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

# A function to create ID-safe slugs
slug = (value) ->
Expand All @@ -25,21 +26,50 @@ highlight = (code, lang) ->
else
hljs.highlightAuto(code).value

# 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)

# Setup marked with code highlighting and smartypants
marked.setOptions
highlight: highlight
smartypants: true

# Get a list of available internal templates
exports.getTemplates = (done) ->
fs.readdir path.join(root, 'templates'), (err, files) ->
fs.readdir path.join(ROOT, 'templates'), (err, files) ->
if err then return done(err)

# Return template names without the extension, and exclude items
# that start with an underscore, which allows component reuse
# among built-in templates.
done null, (f for f in files when f[0] isnt '_').map (item) -> item.replace /\.jade$/, ''

# Get a list of all paths from included files. This *excludes* the
# input path itself.
exports.collectPathsSync = (input, includePath) ->
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

# Render an API Blueprint string using a given template
exports.render = (input, options, done) ->
# Support a template name as the options argument
Expand All @@ -52,6 +82,10 @@ exports.render = (input, options, done) ->
options.filterInput ?= true
options.condenseNav ?= true
options.fullWidth ?= false
options.includePath ?= process.cwd()

# 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.
Expand Down Expand Up @@ -83,7 +117,7 @@ exports.render = (input, options, done) ->
if fs.existsSync options.template
templatePath = options.template
else
templatePath = path.join root, 'templates', "#{options.template}.jade"
templatePath = path.join ROOT, 'templates', "#{options.template}.jade"

jade.renderFile templatePath, locals, (err, html) ->
if err then return done(err)
Expand All @@ -108,6 +142,7 @@ exports.renderFile = (inputFile, outputFile, options, done) ->
done null, warnings

if inputFile isnt '-'
options.includePath ?= path.dirname inputFile
fs.readFile inputFile, encoding: 'utf-8', (err, input) ->
if err then return done(err)
render input.toString()
Expand Down
24 changes: 23 additions & 1 deletion test/basic.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe 'API Blueprint Renderer', ->
assert templates.length
done()

it 'Should get a list of templates', (done) ->
it 'Should handle template list error', (done) ->
sinon.stub fs, 'readdir', (name, callback) ->
callback 'error'

Expand All @@ -31,6 +31,25 @@ describe 'API Blueprint Renderer', ->

done()

it 'Should get a list of included files', ->
sinon.stub fs, 'readFileSync', -> 'I am a test file'

input = '''
# Title
<!-- include(test1.md) -->
Some content...
<!-- include(test2.md) -->
More content...
'''

paths = aglio.collectPathsSync input, '.'

fs.readFileSync.restore()

assert.equal paths.length, 2
assert 'test1.md' in paths
assert 'test2.md' in paths

it 'Should render blank string', (done) ->
aglio.render '', template: 'default', locals: {foo: 1}, (err, html) ->
if err then return done(err)
Expand All @@ -45,6 +64,9 @@ describe 'API Blueprint Renderer', ->

assert html

# Ensure include works
assert html.indexOf 'This is content that was included'

done()

it 'Should render mixed line endings and tabs properly', (done) ->
Expand Down

0 comments on commit e558d2d

Please sign in to comment.