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

Including directive to require file remotely #234

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"protagonist": "^1.2.2",
"serve-static": "^1.10.0",
"socket.io": "^1.3.7",
"yargs": "^3.31.0"
"yargs": "^3.31.0",
"sync-request": "^3.0.0"
},
"devDependencies": {
"async": "^1.5.0",
Expand Down
5 changes: 4 additions & 1 deletion src/bin.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parser = require('yargs')
.options('v', alias: 'version', describe: 'Display version number', default: false)
.options('c', alias: 'compile', describe: 'Compile the blueprint file', default: false)
.options('n', alias: 'include-path', describe: 'Base directory for relative includes')
.options('H', alias: 'include-host', describe: 'Base host for relative includes')
.options('verbose', describe: 'Show verbose information and stack traces', default: false)
.epilog('See https://github.com/danielgtaylor/aglio#readme for more information')

Expand Down Expand Up @@ -146,7 +147,9 @@ 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)
paths = aglio.collectPathsSync fs.readFileSync(argv.i, 'utf-8'), {
includePath: path.dirname(argv.i),
includeHost: argv.includeHost }

watcher = chokidar.watch [argv.i].concat(paths)
watcher.on "change", (path) ->
Expand Down
58 changes: 58 additions & 0 deletions src/include_directive.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
fs = require 'fs'
path = require 'path'
request = require 'sync-request'

INCLUDE_REGEX = /( *)<!-- include\((.*?)\)\s*(filesystem\((.*)\))?\s*-->/gmi

# Replace the include directive with the contents of the included
# file in the input.
replaceText = (options, match, spaces, filename, filesystem, type) ->
if type == 'host'
content = readWebContent("#{options.includeHost}#{filename}", spaces)
else
fullPath = path.join options.includePath, filename
lines = fs.readFileSync(fullPath, 'utf-8').replace(/\r\n?/g, '\n').split('\n')
content = spaces + lines.join "\n#{spaces}"
options.includePath = path.dirname(fullPath)
# The content can itself include other files, so check those
# as well! Beware of circular includes!

this.replace content, options

# Request web content
readWebContent = (path, spaces) ->
spaces ?= ' '
try
response = request('GET', path)
if response.statusCode == 200
spaces + response.getBody()
else
''
catch error
console.error "Invalid HTTP page #{path}"
''

# 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.
exports.replace = (input, options) ->
input.replace INCLUDE_REGEX, replaceText.bind(this, options)

# Get a list of all paths from included files. This *excludes* the
# input path itself.
exports.collectPathsSync = (input, options) ->
paths = []
input.replace INCLUDE_REGEX, (match, spaces, filename, filesystem, type) ->
if type == 'host'
fullPath = "#{options.includeHost}#{filename}"
paths.push fullPath
content = readWebContent(fullPath)
paths = paths.concat exports.collectPathsSync(content, options)

else
fullPath = path.join(options.includePath, filename)
paths.push fullPath

content = fs.readFileSync fullPath, 'utf-8'
paths = paths.concat exports.collectPathsSync(content, path.dirname(fullPath))
paths
39 changes: 8 additions & 31 deletions src/main.coffee
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
fs = require 'fs'
path = require 'path'
protagonist = require 'protagonist'
includeDirective = require './include_directive'

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

# Legacy template names
Expand All @@ -20,34 +20,9 @@ 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) ->
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

exports.collectPathsSync = (input, options) ->
includeDirective.collectPathsSync(input, options)

# Get the theme module for a given theme name
exports.getTheme = (name) ->
Expand All @@ -56,6 +31,7 @@ exports.getTheme = (name) ->

# Render an API Blueprint string using a given template
exports.render = (input, options, done) ->

# Support a template name as the options argument
if typeof options is 'string' or options instanceof String
options =
Expand All @@ -64,6 +40,7 @@ exports.render = (input, options, done) ->
# Defaults
options.filterInput ?= true
options.includePath ?= process.cwd()
options.includeHost ?= 'http://localhost'
options.theme ?= 'default'

# For backward compatibility
Expand All @@ -80,7 +57,7 @@ exports.render = (input, options, done) ->
options.theme = 'olio'

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

# 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 @@ -148,7 +125,7 @@ exports.renderFile = (inputFile, outputFile, options, done) ->
# Compile markdown from/to files
exports.compileFile = (inputFile, outputFile, done) ->
compile = (input) ->
compiled = includeDirective path.dirname(inputFile), input
compiled = includeDirective.replace input, includePath: path.dirname(inputFile)

if outputFile isnt '-'
fs.writeFile outputFile, compiled, (err) ->
Expand Down
21 changes: 19 additions & 2 deletions test/basic.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,29 @@ describe 'API Blueprint Renderer', ->
More content...
'''

paths = aglio.collectPathsSync input, '.'
paths = aglio.collectPathsSync input, { includePath: '.' }

fs.readFileSync.restore()

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

it 'Should get a list of included files with host', ->

input = '''
host: http://localhost
# Title
<!-- include(/docs/test1.apib) filesystem(host) -->
Some content...
<!-- include(/test2.apib) filesystem(host) -->
'''
paths = aglio.collectPathsSync input, { includeHost: 'http://localhost' }

assert.equal paths.length, 2
assert 'http://localhost/docs/test1.apib' in paths
assert 'http://localhost/test2.apib' in paths

it 'Should render blank string', (done) ->
aglio.render '', template: 'default', locals: {foo: 1}, (err, html) ->
if err then return done(err)
Expand Down Expand Up @@ -293,7 +308,9 @@ describe 'Executable', ->
console.error.restore()
assert err

bin.run i: path.join(root, 'example.apib'), s: true, p: 3000, h: 'localhost', (err) ->
file = path.join(root, 'example.apib')

bin.run i: file, s: true, p: 3000, h: 'localhost', (err) ->
assert.equal err, null
http.createServer.restore()

Expand Down