Vitaly Puzrin
10 years ago
15 changed files with 902 additions and 4 deletions
@ -0,0 +1,12 @@ |
|||
'use strict'; |
|||
|
|||
var fs = require('fs'); |
|||
var Remarkable = require('../'); |
|||
|
|||
var md = new Remarkable(); |
|||
|
|||
var data = fs.readFileSync(__dirname +'/samples/lorem1.txt', 'utf8'); |
|||
|
|||
for (var i=0; i<20000; i++) { |
|||
md.render(data); |
|||
} |
@ -0,0 +1,89 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
|
|||
'use strict'; |
|||
|
|||
|
|||
var fs = require('fs'); |
|||
var argparse = require('argparse'); |
|||
|
|||
var Remarkable = require('..'); |
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
|
|||
var cli = new argparse.ArgumentParser({ |
|||
prog: 'js-yaml', |
|||
version: require('../package.json').version, |
|||
addHelp: true |
|||
}); |
|||
|
|||
cli.addArgument(['file'], { |
|||
help: 'File to read', |
|||
nargs: '?', |
|||
defaultValue: '-' |
|||
}); |
|||
|
|||
cli.addArgument(['-t', '--trace'], { |
|||
help: 'Show stack trace on error', |
|||
action: 'storeTrue' |
|||
}); |
|||
|
|||
var options = cli.parseArgs(); |
|||
|
|||
|
|||
function readFile(filename, encoding, callback) { |
|||
if (options.file === '-') { |
|||
// read from stdin
|
|||
|
|||
var chunks = []; |
|||
|
|||
process.stdin.on('data', function(chunk) { |
|||
chunks.push(chunk); |
|||
}); |
|||
|
|||
process.stdin.on('end', function() { |
|||
return callback(null, Buffer.concat(chunks).toString(encoding)); |
|||
}); |
|||
} else { |
|||
fs.readFile(filename, encoding, callback); |
|||
} |
|||
} |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
|
|||
readFile(options.file, 'utf8', function (error, input) { |
|||
var output, md; |
|||
|
|||
if (error) { |
|||
if ('ENOENT' === error.code) { |
|||
console.error('File not found: ' + options.file); |
|||
process.exit(2); |
|||
} |
|||
|
|||
console.error( |
|||
options.trace && error.stack || |
|||
error.message || |
|||
String(error)); |
|||
|
|||
process.exit(1); |
|||
} |
|||
|
|||
md = new Remarkable(); |
|||
|
|||
try { |
|||
output = md.render(input); |
|||
|
|||
} catch (error) { |
|||
console.error( |
|||
options.trace && error.stack || |
|||
error.message || |
|||
String(error)); |
|||
|
|||
process.exit(1); |
|||
} |
|||
|
|||
process.stdout.write(output); |
|||
|
|||
process.exit(0); |
|||
}); |
@ -1,15 +1,123 @@ |
|||
'use strict'; |
|||
|
|||
|
|||
var Renderer = require('./lib/renderer'); |
|||
var LexerBlock = require('./lib/lexer_block'); |
|||
var LexerInline = require('./lib/lexer_inline'); |
|||
|
|||
|
|||
// Parser state class
|
|||
//
|
|||
function State(src, lexerBlock, lexerInline, renderer, options) { |
|||
var ch, s, start, pos, len; |
|||
|
|||
// TODO: Temporary solution. Check if more effective possible,
|
|||
// withous str change
|
|||
//
|
|||
// - replace tabs with spaces
|
|||
// - remove `\r` to simplify newlines check (???)
|
|||
|
|||
this.src = src |
|||
.replace(/\t/g, ' ') |
|||
.replace(/\r/g, '') |
|||
.replace(/\u00a0/g, ' ') |
|||
.replace(/\u2424/g, '\n'); |
|||
|
|||
// Shortcuts to simplify nested calls
|
|||
this.lexerBlock = lexerBlock; |
|||
this.lexerInline = lexerInline; |
|||
this.renderer = renderer; |
|||
|
|||
// TODO: (?) set directly for faster access.
|
|||
this.options = options; |
|||
|
|||
//
|
|||
// Internal state vartiables
|
|||
//
|
|||
|
|||
this.tokens = []; |
|||
|
|||
this.bMarks = []; // lines begin/end markers for fast jumps
|
|||
this.eMarks = []; //
|
|||
|
|||
// Generate markers.
|
|||
s = this.src; |
|||
for(start = pos = 0, len = s.length; pos < len; pos++) { |
|||
ch = s.charCodeAt(pos); |
|||
|
|||
if (ch === 0x0D || ch === 0x0A) { |
|||
this.bMarks.push(start); |
|||
this.eMarks.push(pos); |
|||
start = pos + 1; |
|||
} |
|||
if (ch === 0x0D && pos < len && s.charCodeAt(pos) === 0x0A) { |
|||
pos++; |
|||
start++; |
|||
} |
|||
} |
|||
if (ch !== 0x0D || ch !== 0x0A) { |
|||
this.bMarks.push(start); |
|||
this.eMarks.push(len); |
|||
} |
|||
|
|||
// inline lexer variables
|
|||
this.pos = 0; // char index in src
|
|||
|
|||
// block lexer variables
|
|||
this.blkLevel = 0; |
|||
this.blkIndent = 0; |
|||
this.line = 0; // line index in src
|
|||
this.lineMax = this.bMarks.length; |
|||
|
|||
// renderer
|
|||
this.result = ''; |
|||
|
|||
} |
|||
|
|||
|
|||
// Main class
|
|||
//
|
|||
function Remarkable(options) { |
|||
this.options = {}; |
|||
this.state = null; |
|||
|
|||
this.lexerInline = new LexerInline(); |
|||
this.lexerBlock = new LexerBlock(); |
|||
this.renderer = new Renderer(); |
|||
|
|||
if (options) { this.set(options); } |
|||
} |
|||
|
|||
Remarkable.prototype.set = function (options) { |
|||
|
|||
Remarkable.prototype.set = function (options) { |
|||
Object.keys(options).forEach(function (key) { |
|||
this.options[key] = options[key]; |
|||
}, this); |
|||
}; |
|||
|
|||
|
|||
Remarkable.prototype.render = function (src) { |
|||
return ''; |
|||
|
|||
if (!src) { return ''; } |
|||
|
|||
var state = new State( |
|||
src, |
|||
this.lexerBlock, |
|||
this.lexerInline, |
|||
this.renderer, |
|||
this.options |
|||
); |
|||
|
|||
// TODO: skip leading empty lines
|
|||
|
|||
state.lexerBlock.tokenize(state, state.line, state.lineMax); |
|||
|
|||
// TODO: ??? eat empty paragraphs from tail
|
|||
|
|||
//console.log(state.tokens)
|
|||
|
|||
return this.renderer.render(state); |
|||
}; |
|||
|
|||
|
|||
module.exports = Remarkable; |
|||
|
@ -0,0 +1,301 @@ |
|||
// Block lexer
|
|||
|
|||
|
|||
'use strict'; |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
// Helpers
|
|||
|
|||
|
|||
// Check if character is white space
|
|||
function isWhiteSpace(ch) { |
|||
// TODO: check other spaces and tabs
|
|||
return ch === 0x20; |
|||
} |
|||
|
|||
// Check if line from `pos` is empty or contains spaces only
|
|||
function isEmpty(state, line) { |
|||
var ch, pos = state.bMarks[line], max = state.src.length; |
|||
|
|||
while (pos < max) { |
|||
ch = state.src.charCodeAt(pos++); |
|||
|
|||
if (ch === 0x0A || ch === 0x0D) { return true; } |
|||
|
|||
if (!isWhiteSpace(ch)) { return false; } |
|||
} |
|||
|
|||
return true; // EOL reached
|
|||
} |
|||
|
|||
// Return absolute position of char with default indent an given line,
|
|||
// or -1 if no requested indent
|
|||
function getIndent(state, line, indent) { |
|||
var ch, pos, max; |
|||
|
|||
if (line >= state.lineMax) { return -1; } |
|||
|
|||
pos = state.bMarks[line]; |
|||
max = state.eMarks[line]; |
|||
|
|||
while (pos < max && indent > 0) { |
|||
ch = state.src.charCodeAt(pos++); |
|||
if (ch === 0x09) { indent -= 4; continue; } |
|||
if (isWhiteSpace(ch)) { indent--; continue; } |
|||
return -1; |
|||
} |
|||
|
|||
if (indent > 0) { return -1; } |
|||
|
|||
return pos; |
|||
} |
|||
|
|||
// Skip empty lines, starting from `state.line`
|
|||
function skipEmptyLines(state, from) { |
|||
while (from < state.lineMax) { |
|||
if (!isEmpty(state, from)) { |
|||
state.line = from; |
|||
return; |
|||
} |
|||
from++; |
|||
} |
|||
state.line = from; |
|||
} |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
// Lexer rules
|
|||
|
|||
var rules = []; |
|||
|
|||
|
|||
// code
|
|||
rules.push(function code(state, startLine, endLine) { |
|||
var nextLine, last; |
|||
|
|||
if (getIndent(state, startLine, 4) === -1) { return false; } |
|||
|
|||
last = nextLine = startLine + 1; |
|||
|
|||
while (nextLine < endLine) { |
|||
if (isEmpty(state, nextLine)) { |
|||
nextLine++; |
|||
if (state.options.pedantic) { |
|||
last = nextLine; |
|||
} |
|||
continue; |
|||
} |
|||
if (getIndent(state, nextLine, 4) !== -1) { |
|||
nextLine++; |
|||
last = nextLine; |
|||
continue; |
|||
} |
|||
break; |
|||
} |
|||
|
|||
state.tokens.push({ |
|||
type: 'code', |
|||
startLine: startLine, |
|||
endLine: last |
|||
}); |
|||
|
|||
state.line = nextLine; |
|||
return true; |
|||
}); |
|||
|
|||
|
|||
// Horizontal rule
|
|||
rules.push(function hr(state, startLine, endLine) { |
|||
var ch, marker, |
|||
pos = state.bMarks[startLine], |
|||
space_max = pos + 3, |
|||
max = state.eMarks[startLine]; |
|||
|
|||
ch = state.src.charCodeAt(pos); |
|||
|
|||
// quick test first char
|
|||
if (!isWhiteSpace(ch) && |
|||
ch !== 0x2A/* * */ && |
|||
ch !== 0x2D/* - */ && |
|||
ch !== 0x5F/* _ */) { |
|||
return false; |
|||
} |
|||
|
|||
// skip up to 3 leading spaces
|
|||
while (isWhiteSpace(ch) && pos < max && pos < space_max) { |
|||
pos++; |
|||
ch = state.src.charCodeAt(pos); |
|||
} |
|||
|
|||
// Check hr marker
|
|||
if (ch !== 0x2A/* * */ && |
|||
ch !== 0x2D/* - */ && |
|||
ch !== 0x5F/* _ */) { |
|||
return false; |
|||
} |
|||
|
|||
// remember marker type
|
|||
marker = ch; |
|||
|
|||
if (pos + 2 < max && |
|||
state.src.charCodeAt(pos + 1) === marker && |
|||
state.src.charCodeAt(pos + 2) === marker) { |
|||
// Style 1: ***, ---, ___
|
|||
pos += 3; |
|||
} else if (pos + 4 < max && |
|||
isWhiteSpace(state.src.charCodeAt(pos + 1)) && |
|||
state.src.charCodeAt(pos + 2) === marker && |
|||
isWhiteSpace(state.src.charCodeAt(pos + 3)) && |
|||
state.src.charCodeAt(pos + 4) === marker) { |
|||
// Style 2: * * *, - - -, _ _ _
|
|||
pos += 5; |
|||
} else { |
|||
return false; |
|||
} |
|||
|
|||
// check that line tail has spaces only
|
|||
while(pos < max) { |
|||
ch = state.src.charCodeAt(pos++); |
|||
if (isWhiteSpace(ch)) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
state.tokens.push({ type: 'hr' }); |
|||
|
|||
skipEmptyLines(state, ++startLine); |
|||
return true; |
|||
}); |
|||
|
|||
|
|||
// Paragraph
|
|||
rules.push(function paragraph(state, startLine, endLine) { |
|||
var nextLine = startLine + 1; |
|||
|
|||
// jump line-by-line until empty one or EOF
|
|||
while (nextLine < endLine && !isEmpty(state, nextLine)) { |
|||
nextLine++; |
|||
} |
|||
|
|||
state.tokens.push({ type: 'paragraph_open' }); |
|||
state.lexerInline.tokenize( |
|||
state, |
|||
state.bMarks[startLine], |
|||
state.eMarks[nextLine - 1] |
|||
); |
|||
state.tokens.push({ type: 'paragraph_close' }); |
|||
|
|||
skipEmptyLines(state, nextLine); |
|||
return true; |
|||
}); |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
// Lexer class
|
|||
|
|||
|
|||
function findByName(self, name) { |
|||
for (var i = 0; i < self.rules.length; i++) { |
|||
if (self.rules[i].name === name) { |
|||
return i; |
|||
} |
|||
} |
|||
return -1; |
|||
} |
|||
|
|||
|
|||
// Block Lexer class
|
|||
//
|
|||
function LexerBlock() { |
|||
this.rules = []; |
|||
|
|||
for (var i = 0; i < rules.length; i++) { |
|||
this.after(null, rules[i]); |
|||
} |
|||
} |
|||
|
|||
|
|||
// Replace/delete lexer function
|
|||
//
|
|||
LexerBlock.prototype.at = function (name, fn) { |
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
if (fn) { |
|||
this.rules[index] = fn; |
|||
} else { |
|||
this.rules = this.rules.slice(0, index).concat(this.rules.slice(index + 1)); |
|||
} |
|||
}; |
|||
|
|||
|
|||
// Add function to lexer chain before one with given name.
|
|||
// Or add to start, if name not defined
|
|||
//
|
|||
LexerBlock.prototype.before = function (name, fn) { |
|||
if (!name) { |
|||
this.rules.unshift(fn); |
|||
return; |
|||
} |
|||
|
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
this.rules.splice(index, 0, fn); |
|||
}; |
|||
|
|||
|
|||
// Add function to lexer chain after one with given name.
|
|||
// Or add to end, if name not defined
|
|||
//
|
|||
LexerBlock.prototype.after = function (name, fn) { |
|||
if (!name) { |
|||
this.rules.push(fn); |
|||
return; |
|||
} |
|||
|
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
this.rules.splice(index + 1, 0, fn); |
|||
}; |
|||
|
|||
|
|||
// Generate tokens for input range
|
|||
//
|
|||
LexerBlock.prototype.tokenize = function (state, startLine, endLine) { |
|||
var ok, i, |
|||
rules = this.rules, |
|||
len = this.rules.length, |
|||
line = startLine; |
|||
|
|||
while (line < endLine) { |
|||
|
|||
// Try all possible rules.
|
|||
// On success, rule should:
|
|||
//
|
|||
// - update `state.pos`
|
|||
// - update `state.tokens`
|
|||
// - return true
|
|||
|
|||
for (i = 0; i < len; i++) { |
|||
ok = rules[i](state, line, endLine); |
|||
if (ok) { break; } |
|||
} |
|||
|
|||
if (ok) { |
|||
line = state.line; |
|||
continue; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
|
|||
module.exports = LexerBlock; |
@ -0,0 +1,134 @@ |
|||
// Inline lexer
|
|||
|
|||
'use strict'; |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
// Lexer rules
|
|||
|
|||
var rules = []; |
|||
|
|||
|
|||
// Pure text
|
|||
rules.push(function text(state, begin, end) { |
|||
state.tokens.push({ |
|||
type: 'text', |
|||
begin: begin, |
|||
end: end |
|||
}); |
|||
|
|||
state.pos = end; |
|||
return true; |
|||
}); |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////
|
|||
// Lexer class
|
|||
|
|||
|
|||
function findByName(self, name) { |
|||
for (var i = 0; i < self.rules.length; i++) { |
|||
if (self.rules[i].name === name) { |
|||
return i; |
|||
} |
|||
} |
|||
return -1; |
|||
} |
|||
|
|||
|
|||
// Block Lexer class
|
|||
//
|
|||
function LexerInline() { |
|||
this.rules = []; |
|||
|
|||
for (var i = 0; i < rules.length; i++) { |
|||
this.after(null, rules[i]); |
|||
} |
|||
} |
|||
|
|||
|
|||
// Replace/delete lexer function
|
|||
//
|
|||
LexerInline.prototype.at = function (name, fn) { |
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
if (fn) { |
|||
this.rules[index] = fn; |
|||
} else { |
|||
this.rules = this.rules.slice(0, index).concat(this.rules.slice(index + 1)); |
|||
} |
|||
}; |
|||
|
|||
|
|||
// Add function to lexer chain before one with given name.
|
|||
// Or add to start, if name not defined
|
|||
//
|
|||
LexerInline.prototype.before = function (name, fn) { |
|||
if (!name) { |
|||
this.rules.unshift(fn); |
|||
return; |
|||
} |
|||
|
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
this.rules.splice(index, 0, fn); |
|||
}; |
|||
|
|||
|
|||
// Add function to lexer chain after one with given name.
|
|||
// Or add to end, if name not defined
|
|||
//
|
|||
LexerInline.prototype.after = function (name, fn) { |
|||
if (!name) { |
|||
this.rules.push(fn); |
|||
return; |
|||
} |
|||
|
|||
var index = findByName(name); |
|||
if (index === -1) { |
|||
throw new Error('Lexer rule not found: ' + name); |
|||
} |
|||
|
|||
this.rules.splice(index + 1, 0, fn); |
|||
}; |
|||
|
|||
|
|||
// Generate tokens for input range
|
|||
//
|
|||
LexerInline.prototype.tokenize = function (state, begin, end) { |
|||
var ok, i, |
|||
rules = this.rules, |
|||
len = this.rules.length, |
|||
pos = begin; |
|||
|
|||
while (pos < end) { |
|||
|
|||
// Try all possible rules.
|
|||
// On success, rule should:
|
|||
//
|
|||
// - update `state.pos`
|
|||
// - update `state.tokens`
|
|||
// - return true
|
|||
|
|||
for (i = 0; i < len; i++) { |
|||
ok = rules[i](state, pos, end); |
|||
if (ok) { break; } |
|||
} |
|||
|
|||
if (ok) { |
|||
pos = state.pos; |
|||
continue; |
|||
} |
|||
} |
|||
|
|||
state.pos = end; |
|||
}; |
|||
|
|||
|
|||
module.exports = LexerInline; |
@ -0,0 +1,65 @@ |
|||
'use strict'; |
|||
|
|||
|
|||
function escapeHTML(str) { |
|||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
|||
} |
|||
|
|||
function joinLines(state, begin, end) { |
|||
return state.src.slice( |
|||
state.bMarks[begin], |
|||
end < state.lineMax ? state.bMarks[end] : state.src.length |
|||
); |
|||
} |
|||
|
|||
var rules = {}; |
|||
|
|||
|
|||
rules.code = function (state, token) { |
|||
// TODO: check if we need variable indent cut
|
|||
var lines = joinLines(state, token.startLine, token.endLine).replace(/^ {4}/gm, ''); |
|||
|
|||
state.result += '<pre><code>' + escapeHTML(lines) + '</code></pre>\n'; |
|||
}; |
|||
|
|||
rules.hr = function (state, token) { |
|||
state.result += '<hr>\n'; |
|||
}; |
|||
|
|||
|
|||
rules.paragraph_open = function (state, token) { |
|||
state.result += '<p>'; |
|||
}; |
|||
rules.paragraph_close = function (state, token) { |
|||
state.result += '</p>\n'; |
|||
}; |
|||
|
|||
|
|||
rules.text = function (state, token) { |
|||
state.result += escapeHTML(state.src.slice(token.begin, token.end)); |
|||
}; |
|||
|
|||
|
|||
// TODO: Stub. Do extendable.
|
|||
function Renderer() { |
|||
} |
|||
|
|||
Renderer.prototype.render = function (state) { |
|||
var i, len, rule, |
|||
tokens = state.tokens; |
|||
|
|||
for (i = 0, len = tokens.length; i < len; i++) { |
|||
rule = rules[tokens[i].type]; |
|||
|
|||
// TODO: temporary check
|
|||
if (!rule) { |
|||
throw Error('Renderer error: unknown token ' + tokens[i].type); |
|||
} |
|||
|
|||
rule(state, tokens[i]); |
|||
} |
|||
|
|||
return state.result; |
|||
}; |
|||
|
|||
module.exports = Renderer; |
@ -0,0 +1,16 @@ |
|||
/*global describe*/ |
|||
'use strict'; |
|||
|
|||
|
|||
var path = require('path'); |
|||
|
|||
|
|||
var utils = require('./utils'); |
|||
var Remarked = require('../'); |
|||
|
|||
|
|||
describe('Default', function () { |
|||
var md = new Remarked(); |
|||
|
|||
utils.addTests(path.join(__dirname, 'fixtures/defaults'), md); |
|||
}); |
@ -0,0 +1,12 @@ |
|||
<pre><code>code block on the first line |
|||
</code></pre> |
|||
<p>Regular text.</p> |
|||
<pre><code>code block indented by spaces |
|||
</code></pre> |
|||
<p>Regular text.</p> |
|||
<pre><code>the lines in this block |
|||
all contain trailing spaces |
|||
</code></pre> |
|||
<p>Regular Text.</p> |
|||
<pre><code>code block on the last line |
|||
</code></pre> |
@ -0,0 +1,14 @@ |
|||
code block on the first line |
|||
|
|||
Regular text. |
|||
|
|||
code block indented by spaces |
|||
|
|||
Regular text. |
|||
|
|||
the lines in this block |
|||
all contain trailing spaces |
|||
|
|||
Regular Text. |
|||
|
|||
code block on the last line |
@ -0,0 +1,39 @@ |
|||
<p>Dashes:</p> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>--- |
|||
</code></pre> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>- - - |
|||
</code></pre> |
|||
<p>Asterisks:</p> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>*** |
|||
</code></pre> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>* * * |
|||
</code></pre> |
|||
<p>Underscores:</p> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>___ |
|||
</code></pre> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<hr> |
|||
<pre><code>_ _ _ |
|||
</code></pre> |
@ -0,0 +1,67 @@ |
|||
Dashes: |
|||
|
|||
--- |
|||
|
|||
--- |
|||
|
|||
--- |
|||
|
|||
--- |
|||
|
|||
--- |
|||
|
|||
- - - |
|||
|
|||
- - - |
|||
|
|||
- - - |
|||
|
|||
- - - |
|||
|
|||
- - - |
|||
|
|||
|
|||
Asterisks: |
|||
|
|||
*** |
|||
|
|||
*** |
|||
|
|||
*** |
|||
|
|||
*** |
|||
|
|||
*** |
|||
|
|||
* * * |
|||
|
|||
* * * |
|||
|
|||
* * * |
|||
|
|||
* * * |
|||
|
|||
* * * |
|||
|
|||
|
|||
Underscores: |
|||
|
|||
___ |
|||
|
|||
___ |
|||
|
|||
___ |
|||
|
|||
___ |
|||
|
|||
___ |
|||
|
|||
_ _ _ |
|||
|
|||
_ _ _ |
|||
|
|||
_ _ _ |
|||
|
|||
_ _ _ |
|||
|
|||
_ _ _ |
@ -0,0 +1,38 @@ |
|||
/*global describe, it*/ |
|||
'use strict'; |
|||
|
|||
|
|||
var fs = require('fs'); |
|||
var path = require('path'); |
|||
var assert = require('assert'); |
|||
|
|||
|
|||
function addTests(dir, md) { |
|||
var files = fs.readdirSync(dir); |
|||
|
|||
files.forEach(function (name) { |
|||
var filePath = path.join(dir, name); |
|||
var stat = fs.statSync(filePath); |
|||
|
|||
if (stat.isDirectory()) { |
|||
describe(name, function () { |
|||
addTests(filePath, md); |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
if (stat.isFile) { |
|||
if (path.extname(filePath) !== '.md') { return; } |
|||
|
|||
var mustBe = fs.readFileSync(path.join(dir, path.basename(name, '.md') + '.html'), 'utf8'); |
|||
var src = fs.readFileSync(filePath, 'utf8'); |
|||
|
|||
it(name, function () { |
|||
assert.strictEqual(mustBe, md.render(src)); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
|
|||
module.exports.addTests = addTests; |
Loading…
Reference in new issue