Skip to content

Commit

Permalink
support opening and closing delimiters
Browse files Browse the repository at this point in the history
For optimal Transcribe compatibility, conflicts with Markdown syntax
must be avoided. Markdown uses GREATER-THAN SIGN (>) at the beginning
of each line of a <blockquote>. This conflicts with that character's
use to signify doctest input, as in the following example:

    //# K :: a -> b -> a
    //.
    //. This is a paragraph.
    //.
    //. > This is a block quotation.
    //. > It should not be evaluated!
    //.
    //. ```javascript
    //. > K (true) (42)
    //. true
    //. ```

This commit adds --opening-delimiter and --closing-delimiter options to
the command-line interface, enabling the use of Markdown's <blockquote>
syntax in source files with delimited doctest blocks.

The following options would resolve the conflict in the example above:

    $ doctest --prefix . \
    .         --opening-delimiter '```javascript' \
    .         --closing-delimiter '```' \
    .         -- path/to/file.js
  • Loading branch information
davidchambers committed Mar 9, 2019
1 parent 06a21e6 commit b298ce9
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 72 deletions.
4 changes: 4 additions & 0 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ program
'pass options directly to the "node" binary')
.option (' --prefix <prefix>',
'specify Transcribe-style prefix (e.g. ".")')
.option (' --opening-delimiter <delimiter>',
'specify line preceding doctest block (e.g. "```javascript")')
.option (' --closing-delimiter <delimiter>',
'specify line following doctest block (e.g. "```")')
.option ('-p, --print',
'output the rewritten source without running tests')
.option ('-s, --silent',
Expand Down
166 changes: 98 additions & 68 deletions lib/doctest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (/^#!.*/, '')
Expand All @@ -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) {
Expand All @@ -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'; }, '');
Expand Down Expand Up @@ -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) {
Expand All @@ -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 } }

Expand All @@ -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}}
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]])])
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -190,7 +190,7 @@ testCommand ('bin/doctest --module commonjs lib/doctest.js', {
status: 0,
stdout: unlines ([
'running doctests in lib/doctest.js...',
'...'
'......'
]),
stderr: ''
});
Expand Down
4 changes: 4 additions & 0 deletions test/transcribe/index.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<blockquote>` element. If the `--opening-delimiter`
#. > and `--closing-delimiter` options are set to <code>```coffee</code> and
#. > <code>```</code> respectively, these lines will not be evaluated.
#.
#. ```coffee
#. > map(Math.sqrt)([1, 4, 9])
#. [1, 2, 3]
Expand Down
4 changes: 4 additions & 0 deletions test/transcribe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<blockquote>` element. If the `--opening-delimiter`
//. > and `--closing-delimiter` options are set to <code>```javascript</code>
//. > and <code>```</code> respectively, these lines will not be evaluated.
//.
//. ```javascript
//. > map(Math.sqrt)([1, 4, 9])
//. [1, 2, 3]
Expand Down
2 changes: 1 addition & 1 deletion test/transcribe/results.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
true,
"[1, 2, 3]",
"[1, 2, 3]",
8
12
]
]
]

0 comments on commit b298ce9

Please sign in to comment.