diff --git a/lib/command.js b/lib/command.js index 1b0d9ff0..9f48985f 100644 --- a/lib/command.js +++ b/lib/command.js @@ -15,6 +15,10 @@ program 'pass options directly to the "node" binary') .option (' --prefix ', 'specify Transcribe-style prefix (e.g. ".")') +.option (' --opening-delimiter ', + 'specify line preceding doctest block (e.g. "```javascript")') +.option (' --closing-delimiter ', + 'specify line following doctest block (e.g. "```")') .option ('-p, --print', 'output the rewritten source without running tests') .option ('-s, --silent', diff --git a/lib/doctest.js b/lib/doctest.js index 1f9b674a..0a926718 100644 --- a/lib/doctest.js +++ b/lib/doctest.js @@ -49,7 +49,9 @@ module.exports = function(path, options) { var source = toModule ( rewriters[options.type == null ? inferType (path) : options.type] ( - options.prefix == null ? '' : options.prefix, + {prefix: options.prefix == null ? '' : options.prefix, + openingDelimiter: options.openingDelimiter, + closingDelimiter: options.closingDelimiter}, fs.readFileSync (path, 'utf8') .replace (/\r\n?/g, '\n') .replace (/^#!.*/, '') @@ -75,13 +77,6 @@ function indentN(n, s) { return s.replace (/^(?!$)/gm, (Array (n + 1)).join (' ')); } -// matchLine :: (String, String) -> Nullable (Array3 String String String) -function matchLine(prefix, s) { - return s.slice (0, prefix.length) === prefix ? - (s.slice (prefix.length)).match (/^\s*(>|[.]*)[ ]?(.*)$/) : - null; -} - // object :: Array (Array2 String Any) -> Object function object(pairs) { return pairs.reduce (function(object, pair) { @@ -102,6 +97,20 @@ function show(x) { _show (x); } +// stripLeading :: (Number, String, String) -> String +// +// > stripLeading (1, '.', 'xxx') +// 'xxx' +// > stripLeading (1, '.', '...xxx...') +// '..xxx...' +// > stripLeading (Infinity, '.', '...xxx...') +// 'xxx...' +function stripLeading(n, c, s) { + var idx = 0; + while (idx < n && s.charAt (idx) === c) idx += 1; + return s.slice (idx); +} + // unlines :: Array String -> String function unlines(lines) { return lines.reduce (function(s, line) { return s + line + '\n'; }, ''); @@ -144,9 +153,10 @@ function toModule(source, moduleType) { } } +var CLOSED = 'closed'; +var OPEN = 'open'; var INPUT = 'input'; var OUTPUT = 'output'; -var DEFAULT = 'default'; // normalizeTest :: { output :: { value :: String } } -> Undefined function normalizeTest($test) { @@ -160,6 +170,44 @@ function normalizeTest($test) { } } +function processLine( + options, // :: { prefix :: String + // , openingDelimiter :: Nullable String + // , closingDelimiter :: Nullable String } + accum, // :: { state :: State, tests :: Array Test } + line, // :: String + input, // :: Test -> Undefined + output, // :: Test -> Undefined + appendToInput, // :: Test -> Undefined + appendToOutput // :: Test -> Undefined +) { + var $test, value; + var prefix = options.prefix; + if (line.slice (0, prefix.length) === prefix) { + var trimmedLine = (line.slice (prefix.length)).replace (/^\s*/, ''); + if (accum.state === CLOSED) { + if (trimmedLine === options.openingDelimiter) accum.state = OPEN; + } else if (trimmedLine === options.closingDelimiter) { + accum.state = CLOSED; + } else if (trimmedLine.charAt (0) === '>') { + value = stripLeading (1, ' ', stripLeading (1, '>', trimmedLine)); + accum.tests.push ($test = {}); + $test[accum.state = INPUT] = {value: value}; + input ($test); + } else if (trimmedLine.charAt (0) === '.') { + value = stripLeading (1, ' ', stripLeading (Infinity, '.', trimmedLine)); + $test = accum.tests[accum.tests.length - 1]; + $test[accum.state].value += '\n' + value; + (accum.state === INPUT ? appendToInput : appendToOutput) ($test); + } else if (accum.state === INPUT) { + value = trimmedLine; + $test = accum.tests[accum.tests.length - 1]; + $test[accum.state = OUTPUT] = {value: value}; + output ($test); + } + } +} + // Location = { start :: { line :: Integer, column :: Integer } // , end :: { line :: Integer, column :: Integer } } @@ -173,7 +221,7 @@ function normalizeTest($test) { // // Returns the doctests present in the given esprima comment objects. // -// > transformComments ('', [{ +// > transformComments ({prefix: ''}, [{ // . type: 'Line', // . value: ' > 6 * 7', // . loc: {start: {line: 1, column: 0}, end: {line: 1, column: 10}} @@ -192,7 +240,7 @@ function normalizeTest($test) { // . value: '42', // . loc: {start: {line: 2, column: 0}, end: {line: 2, column: 5}}} // . }] -function transformComments(prefix, comments) { +function transformComments(options, comments) { var result = comments.reduce (function(accum, comment, commentIndex) { return (comment.value.split ('\n')).reduce (function(accum, line, idx) { var normalizedLine, start, end; @@ -204,34 +252,27 @@ function transformComments(prefix, comments) { start = comment.loc.start; end = comment.loc.end; } - - var match = matchLine (prefix, normalizedLine); - if (match != null) { - var $1 = match[1]; - var $2 = match[2]; - if ($1 === '>') { - accum.state = INPUT; - accum.tests.push (object ([ - ['commentIndex', commentIndex], - [INPUT, {value: $2, loc: {start: start, end: end}}] - ])); - } else if ($1 !== '' || accum.state === INPUT) { - var last = accum.tests[accum.tests.length - 1]; - last.commentIndex = commentIndex; - if ($1 !== '') { - last[accum.state].value += '\n' + $2; - last[accum.state].loc.end = end; - } else { - accum.state = OUTPUT; - last[accum.state] = {value: $2, loc: {start: start, end: end}}; - } - } else { - accum.state = DEFAULT; + processLine ( + options, + accum, + normalizedLine, + function($test) { + $test[INPUT].loc = {start: start, end: end}; + }, + function($test) { + $test.commentIndex = commentIndex; + $test[OUTPUT].loc = {start: start, end: end}; + }, + function($test) { + $test[INPUT].loc.end = end; + }, + function($test) { + $test[OUTPUT].loc.end = end; } - } + ); return accum; }, accum); - }, {state: DEFAULT, tests: []}); + }, {state: options.openingDelimiter == null ? OPEN : CLOSED, tests: []}); var $tests = result.tests; $tests.forEach (normalizeTest); @@ -308,7 +349,7 @@ function wrap$coffee(test) { ]).join ('\n'); } -function rewrite$js(prefix, input) { +function rewrite$js(options, input) { // 1. Locate block comments and line comments within the input text. // // 2. Create a list of comment chunks from the list of line comments @@ -346,8 +387,8 @@ function rewrite$js(prefix, input) { return comments; }, {Block: [], Line: []}); - var blockTests = transformComments (prefix, comments.Block); - var lineTests = transformComments (prefix, comments.Line); + var blockTests = transformComments (options, comments.Block); + var lineTests = transformComments (options, comments.Line); var chunks = lineTests .concat ([object ([[INPUT, bookend]])]) @@ -382,7 +423,7 @@ function rewrite$js(prefix, input) { .join (''); } -function rewrite$coffee(prefix, input) { +function rewrite$coffee(options, input) { var lines = input.match (/^.*(?=\n)/gm); var chunks = lines.reduce (function(accum, line, idx) { var isComment = /^[ \t]*#(?!##)/.test (line); @@ -402,35 +443,24 @@ function rewrite$coffee(prefix, input) { var testChunks = chunks.commentChunks.map (function(commentChunk) { var result = commentChunk.lines.reduce (function(accum, line, idx) { - var fullMatch = line.match (/^([ \t]*)#[ \t]*(.*)$/); - var indent = fullMatch[1]; - var match = matchLine (prefix, fullMatch[2]); - if (match != null) { - var $1 = match[1]; - var $2 = match[2]; - if ($1 === '>') { - accum.state = INPUT; - accum.tests.push (object ([ - ['indent', indent], - [INPUT, {value: $2}] - ])); - } else if ($1 !== '' || accum.state === INPUT) { - var last = accum.tests[accum.tests.length - 1]; - if ($1 !== '') { - last[accum.state].value += '\n' + $2; - } else { - accum.state = OUTPUT; - last[accum.state] = { - value: $2, - loc: {start: {line: commentChunk.loc.start.line + idx}} - }; - } - } else { - accum.state = DEFAULT; - } - } + var match = line.match (/^([ \t]*)#[ \t]*(.*)$/); + processLine ( + options, + accum, + match[2], + function($test) { + $test.indent = match[1]; + }, + function($test) { + $test[OUTPUT].loc = { + start: {line: commentChunk.loc.start.line + idx} + }; + }, + function() {}, + function() {} + ); return accum; - }, {state: DEFAULT, tests: []}); + }, {state: options.openingDelimiter == null ? OPEN : CLOSED, tests: []}); return result.tests.map (function($test) { normalizeTest ($test); diff --git a/test/index.js b/test/index.js index 1ace24df..3eae9081 100644 --- a/test/index.js +++ b/test/index.js @@ -80,8 +80,8 @@ testModule ('test/line-endings/LF.coffee', {silent: true}); testModule ('test/exceptions/index.js', {silent: true}); testModule ('test/statements/index.js', {silent: true}); testModule ('test/fantasy-land/index.js', {silent: true}); -testModule ('test/transcribe/index.js', {prefix: '.', silent: true}); -testModule ('test/transcribe/index.coffee', {prefix: '.', silent: true}); +testModule ('test/transcribe/index.js', {prefix: '.', openingDelimiter: '```javascript', closingDelimiter: '```', silent: true}); +testModule ('test/transcribe/index.coffee', {prefix: '.', openingDelimiter: '```coffee', closingDelimiter: '```', silent: true}); testModule ('test/amd/index.js', {module: 'amd', silent: true}); testModule ('test/commonjs/require/index.js', {module: 'commonjs', silent: true}); testModule ('test/commonjs/exports/index.js', {module: 'commonjs', silent: true}); @@ -190,7 +190,7 @@ testCommand ('bin/doctest --module commonjs lib/doctest.js', { status: 0, stdout: unlines ([ 'running doctests in lib/doctest.js...', - '...' + '......' ]), stderr: '' }); diff --git a/test/transcribe/index.coffee b/test/transcribe/index.coffee index 1c1c8a04..0d85cc9a 100644 --- a/test/transcribe/index.coffee +++ b/test/transcribe/index.coffee @@ -3,6 +3,10 @@ #. Transforms a list of elements of type `a` into a list of elements #. of type `b` using the provided function of type `a -> b`. #. +#. > This is a Markdown `
` element. If the `--opening-delimiter` +#. > and `--closing-delimiter` options are set to ```coffee and +#. > ``` respectively, these lines will not be evaluated. +#. #. ```coffee #. > map(Math.sqrt)([1, 4, 9]) #. [1, 2, 3] diff --git a/test/transcribe/index.js b/test/transcribe/index.js index ee0e34ac..3eac5723 100644 --- a/test/transcribe/index.js +++ b/test/transcribe/index.js @@ -3,6 +3,10 @@ //. Transforms a list of elements of type `a` into a list of elements //. of type `b` using the provided function of type `a -> b`. //. +//. > This is a Markdown `
` element. If the `--opening-delimiter` +//. > and `--closing-delimiter` options are set to ```javascript +//. > and ``` respectively, these lines will not be evaluated. +//. //. ```javascript //. > map(Math.sqrt)([1, 4, 9]) //. [1, 2, 3] diff --git a/test/transcribe/results.json b/test/transcribe/results.json index 05111371..bf1f6a8e 100644 --- a/test/transcribe/results.json +++ b/test/transcribe/results.json @@ -5,7 +5,7 @@ true, "[1, 2, 3]", "[1, 2, 3]", - 8 + 12 ] ] ]