summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--benchmarks.md13
-rw-r--r--dingus.html3
-rw-r--r--js/ansi/ansi.js405
-rw-r--r--js/ansi/newlines.js71
-rwxr-xr-xjs/bin/commonmark27
-rw-r--r--js/lib/blocks.js124
-rw-r--r--js/lib/html.js50
-rwxr-xr-xjs/lib/index.js14
-rw-r--r--js/lib/inlines.js46
-rw-r--r--js/lib/node.js24
-rwxr-xr-xjs/test.js20
11 files changed, 218 insertions, 579 deletions
diff --git a/benchmarks.md b/benchmarks.md
index 7e2167d..0fec12d 100644
--- a/benchmarks.md
+++ b/benchmarks.md
@@ -5,12 +5,14 @@ Some benchmarks, run on an ancient Thinkpad running Intel Core 2 Duo at 2GHz.
|Implementation | Time (sec)| Factor |
|-------------------|-----------:|--------:|
| Markdown.pl | 2921.24 | 14606.2 |
+| Python markdown | 55.32 | 276.6 |
| PHP markdown | 20.85 | 104.3 |
| kramdown | 20.83 | 104.1 |
| lunamark | 6.295 | 31.5 |
| cheapskate | 5.760 | 28.8 |
| peg-markdown | 5.450 | 27.3 |
-| **commonmark.js** | 2.335 | 11.6 |
+| parsedown | 3.490 | 17.4 |
+| **commonmark.js** | 2.070 | 10.3 |
| marked | 1.855 | 9.3 |
| discount | 1.705 | 8.5 |
| **cmark** | 0.280 | 1.4 |
@@ -38,7 +40,8 @@ They can be run using `make benchjs`:
|Implementation | ops/sec |
|-------------------|-------------|
-| showdown.js | 173 ±1.45% |
-| **commonmark.js** | 443 ±0.81% |
-| marked.js | 531 ±0.69% |
-| markdown-it | 684 ±1.15% |
+| showdown.js | 168 ±1.65% |
+| **commonmark.js** | 503 ±0.92% |
+| marked.js | 549 ±0.52% |
+| markdown-it | 687 ±1.02% |
+
diff --git a/dingus.html b/dingus.html
index 6b379eb..ee1622f 100644
--- a/dingus.html
+++ b/dingus.html
@@ -10,6 +10,7 @@
<script type="text/javascript">
var writer = new commonmark.HtmlRenderer();
+var astwriter = new commonmark.ASTRenderer();
var reader = new commonmark.DocParser();
function getQueryVariable(variable) {
@@ -57,7 +58,7 @@ $(document).ready(function() {
var renderTime = endTime - startTime;
$("#preview").html(result);
$("#html").text(result);
- $("#ast").text(commonmark.ASTRenderer(parsed));
+ $("#ast").text(astwriter.render(parsed));
$("#rendertime").text(renderTime);
};
var parseAndRender = function() {
diff --git a/js/ansi/ansi.js b/js/ansi/ansi.js
deleted file mode 100644
index 52fc8ec..0000000
--- a/js/ansi/ansi.js
+++ /dev/null
@@ -1,405 +0,0 @@
-
-/**
- * References:
- *
- * - http://en.wikipedia.org/wiki/ANSI_escape_code
- * - http://www.termsys.demon.co.uk/vtansi.htm
- *
- */
-
-/**
- * Module dependencies.
- */
-
-var emitNewlineEvents = require('./newlines')
- , prefix = '\x1b[' // For all escape codes
- , suffix = 'm' // Only for color codes
-
-/**
- * The ANSI escape sequences.
- */
-
-var codes = {
- up: 'A'
- , down: 'B'
- , forward: 'C'
- , back: 'D'
- , nextLine: 'E'
- , previousLine: 'F'
- , horizontalAbsolute: 'G'
- , eraseData: 'J'
- , eraseLine: 'K'
- , scrollUp: 'S'
- , scrollDown: 'T'
- , savePosition: 's'
- , restorePosition: 'u'
- , queryPosition: '6n'
- , hide: '?25l'
- , show: '?25h'
-}
-
-/**
- * Rendering ANSI codes.
- */
-
-var styles = {
- bold: 1
- , italic: 3
- , underline: 4
- , inverse: 7
-}
-
-/**
- * The negating ANSI code for the rendering modes.
- */
-
-var reset = {
- bold: 22
- , italic: 23
- , underline: 24
- , inverse: 27
-}
-
-/**
- * The standard, styleable ANSI colors.
- */
-
-var colors = {
- white: 37
- , black: 30
- , blue: 34
- , cyan: 36
- , green: 32
- , magenta: 35
- , red: 31
- , yellow: 33
- , grey: 90
- , brightBlack: 90
- , brightRed: 91
- , brightGreen: 92
- , brightYellow: 93
- , brightBlue: 94
- , brightMagenta: 95
- , brightCyan: 96
- , brightWhite: 97
-}
-
-
-/**
- * Creates a Cursor instance based off the given `writable stream` instance.
- */
-
-function ansi (stream, options) {
- if (stream._ansicursor) {
- return stream._ansicursor
- } else {
- return stream._ansicursor = new Cursor(stream, options)
- }
-}
-module.exports = exports = ansi
-
-/**
- * The `Cursor` class.
- */
-
-function Cursor (stream, options) {
- if (!(this instanceof Cursor)) {
- return new Cursor(stream, options)
- }
- if (typeof stream != 'object' || typeof stream.write != 'function') {
- throw new Error('a valid Stream instance must be passed in')
- }
-
- // the stream to use
- this.stream = stream
-
- // when 'enabled' is false then all the functions are no-ops except for write()
- this.enabled = options && options.enabled
- if (typeof this.enabled === 'undefined') {
- this.enabled = stream.isTTY
- }
- this.enabled = !!this.enabled
-
- // then `buffering` is true, then `write()` calls are buffered in
- // memory until `flush()` is invoked
- this.buffering = !!(options && options.buffering)
- this._buffer = []
-
- // controls the foreground and background colors
- this.fg = this.foreground = new Colorer(this, 0)
- this.bg = this.background = new Colorer(this, 10)
-
- // defaults
- this.Bold = false
- this.Italic = false
- this.Underline = false
- this.Inverse = false
-
- // keep track of the number of "newlines" that get encountered
- this.newlines = 0
- emitNewlineEvents(stream)
- stream.on('newline', function () {
- this.newlines++
- }.bind(this))
-}
-exports.Cursor = Cursor
-
-/**
- * Helper function that calls `write()` on the underlying Stream.
- * Returns `this` instead of the write() return value to keep
- * the chaining going.
- */
-
-Cursor.prototype.write = function (data) {
- if (this.buffering) {
- this._buffer.push(arguments)
- } else {
- this.stream.write.apply(this.stream, arguments)
- }
- return this
-}
-
-/**
- * Buffer `write()` calls into memory.
- *
- * @api public
- */
-
-Cursor.prototype.buffer = function () {
- this.buffering = true
- return this
-}
-
-/**
- * Write out the in-memory buffer.
- *
- * @api public
- */
-
-Cursor.prototype.flush = function () {
- this.buffering = false
- var str = this._buffer.map(function (args) {
- if (args.length != 1) throw new Error('unexpected args length! ' + args.length);
- return args[0];
- }).join('');
- this._buffer.splice(0); // empty
- this.write(str);
- return this
-}
-
-
-/**
- * The `Colorer` class manages both the background and foreground colors.
- */
-
-function Colorer (cursor, base) {
- this.current = null
- this.cursor = cursor
- this.base = base
-}
-exports.Colorer = Colorer
-
-/**
- * Write an ANSI color code, ensuring that the same code doesn't get rewritten.
- */
-
-Colorer.prototype._setColorCode = function setColorCode (code) {
- var c = String(code)
- if (this.current === c) return
- this.cursor.enabled && this.cursor.write(prefix + c + suffix)
- this.current = c
- return this
-}
-
-
-/**
- * Set up the positional ANSI codes.
- */
-
-Object.keys(codes).forEach(function (name) {
- var code = String(codes[name])
- Cursor.prototype[name] = function () {
- var c = code
- if (arguments.length > 0) {
- c = toArray(arguments).map(Math.round).join(';') + code
- }
- this.enabled && this.write(prefix + c)
- return this
- }
-})
-
-/**
- * Set up the functions for the rendering ANSI codes.
- */
-
-Object.keys(styles).forEach(function (style) {
- var name = style[0].toUpperCase() + style.substring(1)
- , c = styles[style]
- , r = reset[style]
-
- Cursor.prototype[style] = function () {
- if (this[name]) return
- this.enabled && this.write(prefix + c + suffix)
- this[name] = true
- return this
- }
-
- Cursor.prototype['reset' + name] = function () {
- if (!this[name]) return
- this.enabled && this.write(prefix + r + suffix)
- this[name] = false
- return this
- }
-})
-
-/**
- * Setup the functions for the standard colors.
- */
-
-Object.keys(colors).forEach(function (color) {
- var code = colors[color]
-
- Colorer.prototype[color] = function () {
- this._setColorCode(this.base + code)
- return this.cursor
- }
-
- Cursor.prototype[color] = function () {
- return this.foreground[color]()
- }
-})
-
-/**
- * Makes a beep sound!
- */
-
-Cursor.prototype.beep = function () {
- this.enabled && this.write('\x07')
- return this
-}
-
-/**
- * Moves cursor to specific position
- */
-
-Cursor.prototype.goto = function (x, y) {
- x = x | 0
- y = y | 0
- this.enabled && this.write(prefix + y + ';' + x + 'H')
- return this
-}
-
-/**
- * Resets the color.
- */
-
-Colorer.prototype.reset = function () {
- this._setColorCode(this.base + 39)
- return this.cursor
-}
-
-/**
- * Resets all ANSI formatting on the stream.
- */
-
-Cursor.prototype.reset = function () {
- this.enabled && this.write(prefix + '0' + suffix)
- this.Bold = false
- this.Italic = false
- this.Underline = false
- this.Inverse = false
- this.foreground.current = null
- this.background.current = null
- return this
-}
-
-/**
- * Sets the foreground color with the given RGB values.
- * The closest match out of the 216 colors is picked.
- */
-
-Colorer.prototype.rgb = function (r, g, b) {
- var base = this.base + 38
- , code = rgb(r, g, b)
- this._setColorCode(base + ';5;' + code)
- return this.cursor
-}
-
-/**
- * Same as `cursor.fg.rgb(r, g, b)`.
- */
-
-Cursor.prototype.rgb = function (r, g, b) {
- return this.foreground.rgb(r, g, b)
-}
-
-/**
- * Accepts CSS color codes for use with ANSI escape codes.
- * For example: `#FF000` would be bright red.
- */
-
-Colorer.prototype.hex = function (color) {
- return this.rgb.apply(this, hex(color))
-}
-
-/**
- * Same as `cursor.fg.hex(color)`.
- */
-
-Cursor.prototype.hex = function (color) {
- return this.foreground.hex(color)
-}
-
-
-// UTIL FUNCTIONS //
-
-/**
- * Translates a 255 RGB value to a 0-5 ANSI RGV value,
- * then returns the single ANSI color code to use.
- */
-
-function rgb (r, g, b) {
- var red = r / 255 * 5
- , green = g / 255 * 5
- , blue = b / 255 * 5
- return rgb5(red, green, blue)
-}
-
-/**
- * Turns rgb 0-5 values into a single ANSI color code to use.
- */
-
-function rgb5 (r, g, b) {
- var red = Math.round(r)
- , green = Math.round(g)
- , blue = Math.round(b)
- return 16 + (red*36) + (green*6) + blue
-}
-
-/**
- * Accepts a hex CSS color code string (# is optional) and
- * translates it into an Array of 3 RGB 0-255 values, which
- * can then be used with rgb().
- */
-
-function hex (color) {
- var c = color[0] === '#' ? color.substring(1) : color
- , r = c.substring(0, 2)
- , g = c.substring(2, 4)
- , b = c.substring(4, 6)
- return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)]
-}
-
-/**
- * Turns an array-like object into a real array.
- */
-
-function toArray (a) {
- var i = 0
- , l = a.length
- , rtn = []
- for (; i<l; i++) {
- rtn.push(a[i])
- }
- return rtn
-}
diff --git a/js/ansi/newlines.js b/js/ansi/newlines.js
deleted file mode 100644
index 4e37a0a..0000000
--- a/js/ansi/newlines.js
+++ /dev/null
@@ -1,71 +0,0 @@
-
-/**
- * Accepts any node Stream instance and hijacks its "write()" function,
- * so that it can count any newlines that get written to the output.
- *
- * When a '\n' byte is encountered, then a "newline" event will be emitted
- * on the stream, with no arguments. It is up to the listeners to determine
- * any necessary deltas required for their use-case.
- *
- * Ex:
- *
- * var cursor = ansi(process.stdout)
- * , ln = 0
- * process.stdout.on('newline', function () {
- * ln++
- * })
- */
-
-/**
- * Module dependencies.
- */
-
-var assert = require('assert')
-var NEWLINE = '\n'.charCodeAt(0)
-
-function emitNewlineEvents (stream) {
- if (stream._emittingNewlines) {
- // already emitting newline events
- return
- }
-
- var write = stream.write
-
- stream.write = function (data) {
- // first write the data
- var rtn = write.apply(stream, arguments)
-
- if (stream.listeners('newline').length > 0) {
- var len = data.length
- , i = 0
- // now try to calculate any deltas
- if (typeof data == 'string') {
- for (; i<len; i++) {
- processByte(stream, data.charCodeAt(i))
- }
- } else {
- // buffer
- for (; i<len; i++) {
- processByte(stream, data[i])
- }
- }
- }
-
- return rtn
- }
-
- stream._emittingNewlines = true
-}
-module.exports = emitNewlineEvents
-
-
-/**
- * Processes an individual byte being written to a stream
- */
-
-function processByte (stream, b) {
- assert.equal(typeof b, 'number')
- if (b === NEWLINE) {
- stream.emit('newline')
- }
-}
diff --git a/js/bin/commonmark b/js/bin/commonmark
index f0c427c..8f1210b 100755
--- a/js/bin/commonmark
+++ b/js/bin/commonmark
@@ -2,21 +2,21 @@
"use strict";
var fs = require('fs');
-var util = require('util');
var commonmark = require('../lib/index.js');
-var parser = new commonmark.DocParser();
-var renderer = new commonmark.HtmlRenderer();
var inps = [];
var file;
var files = [];
-var options = { sourcepos: false };
+var options = { sourcepos: false, time: false };
+var format = 'html';
var i;
for (i = 2; i < process.argv.length; i++) {
var arg = process.argv[i];
if (arg === '--ast') {
- renderer = { render: commonmark.ASTRenderer };
+ format = 'ast';
+ } else if (arg === '--time') {
+ options.time = true;
} else if (arg === '--sourcepos') {
options.sourcepos = true;
} else if (/^--/.test(arg)) {
@@ -27,6 +27,16 @@ for (i = 2; i < process.argv.length; i++) {
}
}
+var parser = new commonmark.DocParser(options);
+var renderer;
+
+if (format === 'html') {
+ renderer = new commonmark.HtmlRenderer(options);
+} else if (format === 'ast') {
+ renderer = new commonmark.ASTRenderer(options);
+ renderer.options.colors = true;
+}
+
if (files.length === 0) {
files = ['/dev/stdin'];
}
@@ -36,4 +46,9 @@ for (i = 0; i < files.length; i++) {
inps.push(fs.readFileSync(file, 'utf8'));
}
-process.stdout.write(renderer.render(parser.parse(inps.join('\n')), options));
+var inp = inps.join('\n');
+var doc = parser.parse(inp);
+
+var rendered = renderer.render(doc);
+
+if (!options.time) { process.stdout.write(rendered); } \ No newline at end of file
diff --git a/js/lib/blocks.js b/js/lib/blocks.js
index c6e5d75..bd00b1a 100644
--- a/js/lib/blocks.js
+++ b/js/lib/blocks.js
@@ -1,35 +1,66 @@
var Node = require('./node');
var C_GREATERTHAN = 62;
+var C_NEWLINE = 10;
var C_SPACE = 32;
var C_OPEN_BRACKET = 91;
var InlineParser = require('./inlines');
+
var unescapeString = new InlineParser().unescapeString;
+var BLOCKTAGNAME = '(?:article|header|aside|hgroup|iframe|blockquote|hr|body|li|map|button|object|canvas|ol|caption|output|col|p|colgroup|pre|dd|progress|div|section|dl|table|td|dt|tbody|embed|textarea|fieldset|tfoot|figcaption|th|figure|thead|footer|footer|tr|form|ul|h1|h2|h3|h4|h5|h6|video|script|style)';
+
+var HTMLBLOCKOPEN = "<(?:" + BLOCKTAGNAME + "[\\s/>]" + "|" +
+ "/" + BLOCKTAGNAME + "[\\s>]" + "|" + "[?!])";
+
+var reHtmlBlockOpen = new RegExp('^' + HTMLBLOCKOPEN, 'i');
+
+var reHrule = /^(?:(?:\* *){3,}|(?:_ *){3,}|(?:- *){3,}) *$/;
+
+var reMaybeSpecial = /^[ #`~*+_=<>0-9-]/;
+
+var reNonSpace = /[^ \t\n]/;
+
+var reBulletListMarker = /^[*+-]( +|$)/;
+
+var reOrderedListMarker = /^(\d+)([.)])( +|$)/;
+
+var reATXHeaderMarker = /^#{1,6}(?: +|$)/;
+
+var reCodeFence = /^`{3,}(?!.*`)|^~{3,}(?!.*~)/;
+
+var reClosingCodeFence = /^(?:`{3,}|~{3,})(?= *$)/;
+
+var reSetextHeaderLine = /^(?:=+|-+) *$/;
+
+var reLineEnding = /\r\n|\n|\r/;
+
// Returns true if string contains only space characters.
var isBlank = function(s) {
"use strict";
- return /^\s*$/.test(s);
+ return !(reNonSpace.test(s));
};
+var tabSpaces = [' ', ' ', ' ', ' '];
+
// Convert tabs to spaces on each line using a 4-space tab stop.
var detabLine = function(text) {
"use strict";
- if (text.indexOf('\u0000') !== -1) {
- // replace NUL for security
- text = text.replace(/\0/g, '\uFFFD');
- }
- if (text.indexOf('\t') === -1) {
- return text;
- } else {
- var lastStop = 0;
- return text.replace(/\t/g, function(match, offset) {
- var result = ' '.slice((offset - lastStop) % 4);
- lastStop = offset + 1;
- return result;
- });
+
+ var start = 0;
+ var offset;
+ var lastStop = 0;
+
+ while ((offset = text.indexOf('\t', start)) !== -1) {
+ var numspaces = (offset - lastStop) % 4;
+ var spaces = tabSpaces[numspaces];
+ text = text.slice(0, offset) + spaces + text.slice(offset + 1);
+ lastStop = offset + numspaces;
+ start = lastStop;
}
+
+ return text;
};
// Attempt to match a regex in string s at offset offset.
@@ -44,13 +75,15 @@ var matchAt = function(re, s, offset) {
}
};
-var BLOCKTAGNAME = '(?:article|header|aside|hgroup|iframe|blockquote|hr|body|li|map|button|object|canvas|ol|caption|output|col|p|colgroup|pre|dd|progress|div|section|dl|table|td|dt|tbody|embed|textarea|fieldset|tfoot|figcaption|th|figure|thead|footer|footer|tr|form|ul|h1|h2|h3|h4|h5|h6|video|script|style)';
-var HTMLBLOCKOPEN = "<(?:" + BLOCKTAGNAME + "[\\s/>]" + "|" +
- "/" + BLOCKTAGNAME + "[\\s>]" + "|" + "[?!])";
-var reHtmlBlockOpen = new RegExp('^' + HTMLBLOCKOPEN, 'i');
-
-var reHrule = /^(?:(?:\* *){3,}|(?:_ *){3,}|(?:- *){3,}) *$/;
-
+// destructively trip final blank lines in an array of strings
+var stripFinalBlankLines = function(lns) {
+ "use strict";
+ var i = lns.length - 1;
+ while (!reNonSpace.test(lns[i])) {
+ lns.pop();
+ i--;
+ }
+};
// DOC PARSER
@@ -160,12 +193,12 @@ var parseListMarker = function(ln, offset) {
if (rest.match(reHrule)) {
return null;
}
- if ((match = rest.match(/^[*+-]( +|$)/))) {
+ if ((match = rest.match(reBulletListMarker))) {
spaces_after_marker = match[1].length;
data.type = 'Bullet';
data.bullet_char = match[0][0];
- } else if ((match = rest.match(/^(\d+)([.)])( +|$)/))) {
+ } else if ((match = rest.match(reOrderedListMarker))) {
spaces_after_marker = match[3].length;
data.type = 'Ordered';
data.start = parseInt(match[1]);
@@ -214,6 +247,11 @@ var incorporateLine = function(ln, line_number) {
var container = this.doc;
var oldtip = this.tip;
+ // replace NUL characters for security
+ if (ln.indexOf('\u0000') !== -1) {
+ ln = ln.replace(/\0/g, '\uFFFD');
+ }
+
// Convert tabs to spaces:
ln = detabLine(ln);
@@ -226,7 +264,7 @@ var incorporateLine = function(ln, line_number) {
}
container = container.lastChild;
- match = matchAt(/[^ ]/, ln, offset);
+ match = matchAt(reNonSpace, ln, offset);
if (match === -1) {
first_nonspace = ln.length;
blank = true;
@@ -319,13 +357,11 @@ var incorporateLine = function(ln, line_number) {
// want to close unmatched blocks. So we store this closure for
// use later, when we have more information.
var closeUnmatchedBlocks = function(mythis) {
- var already_done = false;
// finalize any blocks not matched
- while (!already_done && oldtip !== last_matched_container) {
+ while (oldtip !== last_matched_container) {
mythis.finalize(oldtip, line_number - 1);
oldtip = oldtip.parent;
}
- already_done = true;
};
// Check to see if we've hit 2nd blank line; if so break out of list:
@@ -339,9 +375,9 @@ var incorporateLine = function(ln, line_number) {
container.t !== 'IndentedCode' &&
container.t !== 'HtmlBlock' &&
// this is a little performance optimization:
- matchAt(/^[ #`~*+_=<>0-9-]/, ln, offset) !== -1) {
+ matchAt(reMaybeSpecial, ln, offset) !== -1) {
- match = matchAt(/[^ ]/, ln, offset);
+ match = matchAt(reNonSpace, ln, offset);
if (match === -1) {
first_nonspace = ln.length;
blank = true;
@@ -371,7 +407,7 @@ var incorporateLine = function(ln, line_number) {
closeUnmatchedBlocks(this);
container = this.addChild('BlockQuote', line_number, offset);
- } else if ((match = ln.slice(first_nonspace).match(/^#{1,6}(?: +|$)/))) {
+ } else if ((match = ln.slice(first_nonspace).match(reATXHeaderMarker))) {
// ATX header
offset = first_nonspace + match[0].length;
closeUnmatchedBlocks(this);
@@ -382,7 +418,7 @@ var incorporateLine = function(ln, line_number) {
[ln.slice(offset).replace(/^ *#+ *$/, '').replace(/ +#+ *$/, '')];
break;
- } else if ((match = ln.slice(first_nonspace).match(/^`{3,}(?!.*`)|^~{3,}(?!.*~)/))) {
+ } else if ((match = ln.slice(first_nonspace).match(reCodeFence))) {
// fenced code block
var fence_length = match[0].length;
closeUnmatchedBlocks(this);
@@ -402,7 +438,7 @@ var incorporateLine = function(ln, line_number) {
} else if (container.t === 'Paragraph' &&
container.strings.length === 1 &&
- ((match = ln.slice(first_nonspace).match(/^(?:=+|-+) *$/)))) {
+ ((match = ln.slice(first_nonspace).match(reSetextHeaderLine)))) {
// setext header line
closeUnmatchedBlocks(this);
container.t = 'Header'; // convert Paragraph to SetextHeader
@@ -447,7 +483,7 @@ var incorporateLine = function(ln, line_number) {
// What remains at the offset is a text line. Add the text to the
// appropriate container.
- match = matchAt(/[^ ]/, ln, offset);
+ match = matchAt(reNonSpace, ln, offset);
if (match === -1) {
first_nonspace = ln.length;
blank = true;
@@ -500,7 +536,7 @@ var incorporateLine = function(ln, line_number) {
// check for closing code fence:
match = (indent <= 3 &&
ln.charAt(first_nonspace) === container.fence_char &&
- ln.slice(first_nonspace).match(/^(?:`{3,}|~{3,})(?= *$)/));
+ ln.slice(first_nonspace).match(reClosingCodeFence));
if (match && match[0].length >= container.fence_length) {
// don't add closing fence to container; instead, close it:
this.finalize(container, line_number);
@@ -569,7 +605,8 @@ var finalize = function(block, line_number) {
break;
case 'IndentedCode':
- block.literal = block.strings.join('\n').replace(/(\n *)*$/, '\n');
+ stripFinalBlankLines(block.strings);
+ block.literal = block.strings.join('\n') + '\n';
block.t = 'CodeBlock';
break;
@@ -644,21 +681,31 @@ var parse = function(input) {
this.doc = Document();
this.tip = this.doc;
this.refmap = {};
- var lines = input.replace(/\n$/, '').split(/\r\n|\n|\r/);
+ if (this.options.time) { console.time("preparing input"); }
+ var lines = input.split(reLineEnding);
var len = lines.length;
+ if (input.charCodeAt(input.length - 1) === C_NEWLINE) {
+ // ignore last blank line created by final newline
+ len -= 1;
+ }
+ if (this.options.time) { console.timeEnd("preparing input"); }
+ if (this.options.time) { console.time("block parsing"); }
for (var i = 0; i < len; i++) {
this.incorporateLine(lines[i], i + 1);
}
while (this.tip) {
this.finalize(this.tip, len);
}
+ if (this.options.time) { console.timeEnd("block parsing"); }
+ if (this.options.time) { console.time("inline parsing"); }
this.processInlines(this.doc);
+ if (this.options.time) { console.timeEnd("inline parsing"); }
return this.doc;
};
// The DocParser object.
-function DocParser(){
+function DocParser(options){
"use strict";
return {
doc: Document(),
@@ -672,7 +719,8 @@ function DocParser(){
incorporateLine: incorporateLine,
finalize: finalize,
processInlines: processInlines,
- parse: parse
+ parse: parse,
+ options: options || {}
};
}
diff --git a/js/lib/html.js b/js/lib/html.js
index 26c677b..847ed98 100644
--- a/js/lib/html.js
+++ b/js/lib/html.js
@@ -19,31 +19,38 @@ var tag = function(name, attrs, selfclosing) {
return result;
};
-var renderNodes = function(block, options) {
+var reHtmlTag = /\<[^>]*\>/;
+
+var renderNodes = function(block) {
var attrs;
var info_words;
var tagname;
var walker = block.walker();
var event, node, entering;
- var buffer = [];
+ var buffer = "";
+ var lastOut = "\n";
var disableTags = 0;
var grandparent;
var out = function(s) {
if (disableTags > 0) {
- buffer.push(s.replace(/\<[^>]*\>/g, ''));
+ buffer += s.replace(reHtmlTag, '');
} else {
- buffer.push(s);
+ buffer += s;
}
+ lastOut = s;
};
var esc = this.escape;
var cr = function() {
- if (buffer.length > 0 && buffer[buffer.length - 1] !== '\n') {
- out('\n');
+ if (lastOut !== '\n') {
+ buffer += '\n';
+ lastOut = '\n';
}
};
- options = options || {};
+ var options = this.options;
+
+ if (options.time) { console.time("rendering"); }
while ((event = walker.next())) {
entering = event.entering;
@@ -81,10 +88,6 @@ var renderNodes = function(block, options) {
out(tag(entering ? 'strong' : '/strong'));
break;
- case 'Emph':
- out(tag(entering ? 'strong' : '/strong'));
- break;
-
case 'Html':
out(node.literal);
break;
@@ -198,7 +201,7 @@ var renderNodes = function(block, options) {
}
cr();
out(tag('pre') + tag('code', attrs));
- out(this.escape(node.literal));
+ out(esc(node.literal));
out(tag('/code') + tag('/pre'));
cr();
break;
@@ -220,14 +223,15 @@ var renderNodes = function(block, options) {
break;
default:
- console.log("Unknown node type " + node.t);
+ throw("Unknown node type " + node.t);
}
}
- return buffer.join('');
+ if (options.time) { console.timeEnd("rendering"); }
+ return buffer;
};
-var sub = function(s) {
+var replaceUnsafeChar = function(s) {
switch (s) {
case '&':
return '&amp;';
@@ -242,23 +246,27 @@ var sub = function(s) {
}
};
+var reNeedsEscaping = /[&<>"]/;
// The HtmlRenderer object.
-function HtmlRenderer(){
+function HtmlRenderer(options){
return {
// default options:
- blocksep: '\n', // space between blocks
- innersep: '\n', // space between block container tag and contents
softbreak: '\n', // by default, soft breaks are rendered as newlines in HTML
// set to "<br />" to make them hard breaks
// set to " " if you want to ignore line wrapping in source
escape: function(s, preserve_entities) {
- if (preserve_entities) {
- return s.replace(/[&](?:[#](x[a-f0-9]{1,8}|[0-9]{1,8});|[a-z][a-z0-9]{1,31};)|[&<>"]/gi, sub);
+ if (reNeedsEscaping.test(s)) {
+ if (preserve_entities) {
+ return s.replace(/[&](?:[#](x[a-f0-9]{1,8}|[0-9]{1,8});|[a-z][a-z0-9]{1,31};)|[&<>"]/gi, replaceUnsafeChar);
+ } else {
+ return s.replace(/[&<>"]/g, replaceUnsafeChar);
+ }
} else {
- return s.replace(/[&<>"]/g, sub);
+ return s;
}
},
+ options: options || {},
render: renderNodes
};
}
diff --git a/js/lib/index.js b/js/lib/index.js
index d0532c6..22a2184 100755
--- a/js/lib/index.js
+++ b/js/lib/index.js
@@ -13,11 +13,15 @@
var util = require('util');
-var renderAST = function(tree) {
- return util.inspect(tree.toAST(), {depth: 20}) + '\n';
-};
-
module.exports.Node = require('./node');
module.exports.DocParser = require('./blocks');
module.exports.HtmlRenderer = require('./html');
-module.exports.ASTRenderer = renderAST;
+module.exports.ASTRenderer = function(options) {
+ return {
+ render: function(tree) {
+ return util.inspect(tree.toAST(), null, 20,
+ this.options.colors) + '\n';
+ },
+ options: options || {}
+ };
+}
diff --git a/js/lib/inlines.js b/js/lib/inlines.js
index 72c4448..4d49861 100644
--- a/js/lib/inlines.js
+++ b/js/lib/inlines.js
@@ -65,6 +65,8 @@ var reEntityHere = new RegExp('^' + ENTITY, 'i');
var reEntityOrEscapedChar = new RegExp('\\\\' + ESCAPABLE + '|' + ENTITY, 'gi');
+var reBackslashOrAmp = /[\\&]/;
+
var reTicks = new RegExp('`+');
var reTicksHere = new RegExp('^`+');
@@ -75,6 +77,18 @@ var reAutolink = /^<(?:coap|doi|javascript|aaa|aaas|about|acap|cap|cid|crid|data
var reSpnl = /^ *(?:\n *)?/;
+var reWhitespaceChar = /^\s/;
+
+var reWhitespace = /\s+/g;
+
+var reFinalSpace = / *$/;
+
+var reInitialSpace = /^ */;
+
+var reAsciiAlnum = /[a-z0-9]/i;
+
+var reLinkLabel = /^\[(?:[^\\\[\]]|\\[\[\]]){0,1000}\]/;
+
// Matches a string of non-special characters.
var reMain = /^[^\n`\[\]\\!<&*_]+/m;
@@ -90,7 +104,11 @@ var unescapeChar = function(s) {
// Replace entities and backslash escapes with literal characters.
var unescapeString = function(s) {
"use strict";
- return s.replace(reEntityOrEscapedChar, unescapeChar);
+ if (reBackslashOrAmp.test(s)) {
+ return s.replace(reEntityOrEscapedChar, unescapeChar);
+ } else {
+ return s;
+ }
};
// Normalize reference label: collapse internal whitespace
@@ -167,8 +185,7 @@ var parseBackticks = function(block) {
node = new Node('Code');
node.literal = this.subject.slice(afterOpenTicks,
this.pos - ticks.length)
- .replace(/[ \n]+/g, ' ')
- .trim();
+ .trim().replace(reWhitespace, ' ');
block.appendChild(node);
return true;
}
@@ -270,17 +287,17 @@ var scanDelims = function(cc) {
char_after = fromCodePoint(cc_after);
}
- var can_open = numdelims > 0 && !(/\s/.test(char_after)) &&
+ var can_open = numdelims > 0 && !(reWhitespaceChar.test(char_after)) &&
!(rePunctuation.test(char_after) &&
!(/\s/.test(char_before)) &&
!(rePunctuation.test(char_before)));
- var can_close = numdelims > 0 && !(/\s/.test(char_before)) &&
+ var can_close = numdelims > 0 && !(reWhitespaceChar.test(char_before)) &&
!(rePunctuation.test(char_before) &&
- !(/\s/.test(char_after)) &&
+ !(reWhitespaceChar.test(char_after)) &&
!(rePunctuation.test(char_after)));
if (cc === C_UNDERSCORE) {
- can_open = can_open && !((/[a-z0-9]/i).test(char_before));
- can_close = can_close && !((/[a-z0-9]/i).test(char_after));
+ can_open = can_open && !((reAsciiAlnum).test(char_before));
+ can_close = can_close && !((reAsciiAlnum).test(char_after));
}
this.pos = startpos;
return { numdelims: numdelims,
@@ -463,7 +480,7 @@ var parseLinkDestination = function() {
// Attempt to parse a link label, returning number of characters parsed.
var parseLinkLabel = function() {
"use strict";
- var m = this.match(/^\[(?:[^\\\[\]]|\\[\[\]]){0,1000}\]/);
+ var m = this.match(reLinkLabel);
return m === null ? 0 : m.length;
};
@@ -581,10 +598,11 @@ var parseCloseBracket = function(block) {
((dest = this.parseLinkDestination()) !== null) &&
this.spnl() &&
// make sure there's a space before the title:
- (/^\s/.test(this.subject.charAt(this.pos - 1)) &&
+ (reWhitespaceChar.test(this.subject.charAt(this.pos - 1)) &&
(title = this.parseLinkTitle() || '') || true) &&
this.spnl() &&
- this.match(/^\)/)) {
+ this.subject.charAt(this.pos) === ')') {
+ this.pos += 1;
matched = true;
}
} else {
@@ -691,15 +709,15 @@ var parseNewline = function(block) {
// check previous node for trailing spaces
var lastc = block.lastChild;
if (lastc && lastc.t === 'Text') {
- var sps = / *$/.exec(lastc.literal)[0].length;
+ var sps = reFinalSpace.exec(lastc.literal)[0].length;
if (sps > 0) {
- lastc.literal = lastc.literal.replace(/ *$/, '');
+ lastc.literal = lastc.literal.replace(reFinalSpace, '');
}
block.appendChild(new Node(sps >= 2 ? 'Hardbreak' : 'Softbreak'));
} else {
block.appendChild(new Node('Softbreak'));
}
- this.match(/^ */); // gobble leading spaces in next line
+ this.match(reInitialSpace); // gobble leading spaces in next line
return true;
};
diff --git a/js/lib/node.js b/js/lib/node.js
index 84fb122..9dc7c3f 100644
--- a/js/lib/node.js
+++ b/js/lib/node.js
@@ -14,18 +14,12 @@ function isContainer(node) {
t === 'Image');
}
-function NodeWalker(root) {
- this.current = root;
- this.root = root;
- this.entering = true;
-}
-
-NodeWalker.prototype.resumeAt = function(node, entering) {
+var resumeAt = function(node, entering) {
this.current = node;
this.entering = (entering === true);
};
-NodeWalker.prototype.next = function(){
+var next = function(){
var cur = this.current;
var entering = this.entering;
@@ -56,7 +50,15 @@ NodeWalker.prototype.next = function(){
return {entering: entering, node: cur};
};
-function Node(nodeType, sourcepos) {
+var NodeWalker = function(root) {
+ return { current: root,
+ root: root,
+ entering: true,
+ next: next,
+ resumeAt: resumeAt };
+};
+
+var Node = function(nodeType, sourcepos) {
this.t = nodeType;
this.parent = null;
this.firstChild = null;
@@ -77,7 +79,7 @@ function Node(nodeType, sourcepos) {
this.fence_length = undefined;
this.fence_offset = undefined;
this.level = undefined;
-}
+};
Node.prototype.isContainer = function() {
return isContainer(this);
@@ -154,7 +156,7 @@ Node.prototype.insertBefore = function(sibling) {
};
Node.prototype.walker = function() {
- var walker = new NodeWalker(this);
+ var walker = NodeWalker(this);
return walker;
};
diff --git a/js/test.js b/js/test.js
index 6cb7c98..2e8b5c3 100755
--- a/js/test.js
+++ b/js/test.js
@@ -3,8 +3,24 @@
var fs = require('fs');
var commonmark = require('./lib/index.js');
-var ansi = require('./ansi/ansi');
-var cursor = ansi(process.stdout);
+
+// Home made mini-version of the npm ansi module:
+var escSeq = function(s) {
+ return function (){
+ process.stdout.write('\u001b' + s);
+ return this;
+ };
+};
+var cursor = {
+ write: function (s) {
+ process.stdout.write(s);
+ return this;
+ },
+ green: escSeq('[0;32m'),
+ red: escSeq('[0;31m'),
+ cyan: escSeq('[0;36m'),
+ reset: escSeq('[0;30m'),
+};
var writer = new commonmark.HtmlRenderer();
var reader = new commonmark.DocParser();