diff --git a/package.json b/package.json index fb840352..ce1ce162 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/bin.coffee b/src/bin.coffee index 977f6111..8fc3c76f 100644 --- a/src/bin.coffee +++ b/src/bin.coffee @@ -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') @@ -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) -> diff --git a/src/include_directive.coffee b/src/include_directive.coffee new file mode 100644 index 00000000..194ee93e --- /dev/null +++ b/src/include_directive.coffee @@ -0,0 +1,58 @@ +fs = require 'fs' +path = require 'path' +request = require 'sync-request' + +INCLUDE_REGEX = /( *)/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 diff --git a/src/main.coffee b/src/main.coffee index da860cca..07e7e37a 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -1,8 +1,8 @@ fs = require 'fs' path = require 'path' protagonist = require 'protagonist' +includeDirective = require './include_directive' -INCLUDE = /( *)/gmi ROOT = path.dirname __dirname # Legacy template names @@ -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) -> @@ -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 = @@ -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 @@ -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. @@ -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) -> diff --git a/test/basic.coffee b/test/basic.coffee index 5a1a407b..6fc7908b 100644 --- a/test/basic.coffee +++ b/test/basic.coffee @@ -28,7 +28,7 @@ describe 'API Blueprint Renderer', -> More content... ''' - paths = aglio.collectPathsSync input, '.' + paths = aglio.collectPathsSync input, { includePath: '.' } fs.readFileSync.restore() @@ -36,6 +36,21 @@ describe 'API Blueprint Renderer', -> 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 + + Some content... + + ''' + 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) @@ -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()