commonmark
Advanced tools
Comparing version 0.15.0 to 0.16.0
19
bench.js
@@ -5,6 +5,8 @@ var Benchmark = require('benchmark').Benchmark; | ||
var sm = require('./lib/index.js'); | ||
// https://github.com/coreyti/showdown | ||
var showdown = require('../../showdown/src/showdown'); | ||
// https://github.com/chjj/marked | ||
var marked = require('../../marked/marked.min.js'); | ||
// npm install showdown | ||
var Showdown = require('showdown').converter; | ||
// npm install marked | ||
var marked = require('marked'); | ||
// npm install markdown-it | ||
var markdownit = require('markdown-it')('commonmark'); | ||
@@ -19,3 +21,3 @@ var benchfile = process.argv[2]; | ||
var renderer = new sm.HtmlRenderer(); | ||
renderer.renderBlock(doc); | ||
renderer.render(doc); | ||
}) | ||
@@ -25,3 +27,3 @@ | ||
"use strict"; | ||
var converter = new showdown.converter(); | ||
var converter = new Showdown(); | ||
converter.makeHtml(contents); | ||
@@ -35,2 +37,7 @@ }) | ||
.add('markdown-it markdown->html', function() { | ||
"use strict"; | ||
markdownit.render(contents); | ||
}) | ||
.on('cycle', function(event) { | ||
@@ -37,0 +44,0 @@ "use strict"; |
@@ -0,2 +1,8 @@ | ||
"use strict"; | ||
var Node = require('./node'); | ||
var unescapeString = require('./common').unescapeString; | ||
var C_GREATERTHAN = 62; | ||
var C_NEWLINE = 10; | ||
var C_SPACE = 32; | ||
@@ -6,21 +12,52 @@ 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) { | ||
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) { | ||
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; | ||
}; | ||
@@ -32,17 +69,18 @@ | ||
var res = s.slice(offset).match(re); | ||
if (res) { | ||
if (res === null) { | ||
return -1; | ||
} else { | ||
return offset + res.index; | ||
} else { | ||
return -1; | ||
} | ||
}; | ||
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'); | ||
// destructively trip final blank lines in an array of strings | ||
var stripFinalBlankLines = function(lns) { | ||
var i = lns.length - 1; | ||
while (!reNonSpace.test(lns[i])) { | ||
lns.pop(); | ||
i--; | ||
} | ||
}; | ||
var reHrule = /^(?:(?:\* *){3,}|(?:_ *){3,}|(?:- *){3,}) *$/; | ||
// DOC PARSER | ||
@@ -52,18 +90,2 @@ | ||
var makeBlock = function(tag, start_line, start_column) { | ||
return { t: tag, | ||
open: true, | ||
last_line_blank: false, | ||
start_line: start_line, | ||
start_column: start_column, | ||
end_line: start_line, | ||
children: [], | ||
parent: null, | ||
// string_content is formed by concatenating strings, in finalize: | ||
string_content: "", | ||
strings: [], | ||
inline_content: [] | ||
}; | ||
}; | ||
// Returns true if parent block can contain child block. | ||
@@ -73,4 +95,4 @@ var canContain = function(parent_type, child_type) { | ||
parent_type === 'BlockQuote' || | ||
parent_type === 'ListItem' || | ||
(parent_type === 'List' && child_type === 'ListItem') ); | ||
parent_type === 'Item' || | ||
(parent_type === 'List' && child_type === 'Item') ); | ||
}; | ||
@@ -88,10 +110,13 @@ | ||
var endsWithBlankLine = function(block) { | ||
if (block.last_line_blank) { | ||
return true; | ||
while (block) { | ||
if (block.last_line_blank) { | ||
return true; | ||
} | ||
if (block.t === 'List' || block.t === 'Item') { | ||
block = block.lastChild; | ||
} else { | ||
break; | ||
} | ||
} | ||
if ((block.t === 'List' || block.t === 'ListItem') && block.children.length > 0) { | ||
return endsWithBlankLine(block.children[block.children.length - 1]); | ||
} else { | ||
return false; | ||
} | ||
return false; | ||
}; | ||
@@ -103,3 +128,3 @@ | ||
// break of of all lists" feature.) | ||
var breakOutOfLists = function(block, line_number) { | ||
var breakOutOfLists = function(block) { | ||
var b = block; | ||
@@ -116,6 +141,6 @@ var last_list = null; | ||
while (block !== last_list) { | ||
this.finalize(block, line_number); | ||
this.finalize(block, this.lineNumber); | ||
block = block.parent; | ||
} | ||
this.finalize(last_list, line_number); | ||
this.finalize(last_list, this.lineNumber); | ||
this.tip = last_list.parent; | ||
@@ -138,11 +163,12 @@ } | ||
// and so on til we find a block that can accept children. | ||
var addChild = function(tag, line_number, offset) { | ||
var addChild = function(tag, offset) { | ||
while (!canContain(this.tip.t, tag)) { | ||
this.finalize(this.tip, line_number); | ||
this.finalize(this.tip, this.lineNumber - 1); | ||
} | ||
var column_number = offset + 1; // offset 0 = column 1 | ||
var newBlock = makeBlock(tag, line_number, column_number); | ||
this.tip.children.push(newBlock); | ||
newBlock.parent = this.tip; | ||
var newBlock = new Node(tag, [[this.lineNumber, column_number], [0, 0]]); | ||
newBlock.strings = []; | ||
newBlock.string_content = null; | ||
this.tip.appendChild(newBlock); | ||
this.tip = newBlock; | ||
@@ -154,11 +180,17 @@ return newBlock; | ||
// start, delimiter, bullet character, padding) or null. | ||
var parseListMarker = function(ln, offset) { | ||
var parseListMarker = function(ln, offset, indent) { | ||
var rest = ln.slice(offset); | ||
var match; | ||
var spaces_after_marker; | ||
var data = {}; | ||
var data = { type: null, | ||
tight: true, | ||
bullet_char: null, | ||
start: null, | ||
delimiter: null, | ||
padding: null, | ||
marker_offset: indent }; | ||
if (rest.match(reHrule)) { | ||
return null; | ||
} | ||
if ((match = rest.match(/^[*+-]( +|$)/))) { | ||
if ((match = rest.match(reBulletListMarker))) { | ||
spaces_after_marker = match[1].length; | ||
@@ -168,3 +200,3 @@ data.type = 'Bullet'; | ||
} else if ((match = rest.match(/^(\d+)([.)])( +|$)/))) { | ||
} else if ((match = rest.match(reOrderedListMarker))) { | ||
spaces_after_marker = match[3].length; | ||
@@ -197,9 +229,17 @@ data.type = 'Ordered'; | ||
// Finalize and close any unmatched blocks. Returns true. | ||
var closeUnmatchedBlocks = function() { | ||
// finalize any blocks not matched | ||
while (this.oldtip !== this.lastMatchedContainer) { | ||
this.finalize(this.oldtip, this.lineNumber - 1); | ||
this.oldtip = this.oldtip.parent; | ||
} | ||
return true; | ||
}; | ||
// Analyze a line of text and update the document appropriately. | ||
// We parse markdown text by calling this on each line of input, | ||
// then finalizing the document. | ||
var incorporateLine = function(ln, line_number) { | ||
var incorporateLine = function(ln) { | ||
var all_matched = true; | ||
var last_child; | ||
var first_nonspace; | ||
@@ -211,9 +251,14 @@ var offset = 0; | ||
var indent; | ||
var last_matched_container; | ||
var i; | ||
var CODE_INDENT = 4; | ||
var allClosed; | ||
var container = this.doc; | ||
var oldtip = this.tip; | ||
this.oldtip = this.tip; | ||
// replace NUL characters for security | ||
if (ln.indexOf('\u0000') !== -1) { | ||
ln = ln.replace(/\0/g, '\uFFFD'); | ||
} | ||
// Convert tabs to spaces: | ||
@@ -225,10 +270,9 @@ ln = detabLine(ln); | ||
// Set all_matched to false if not all containers match. | ||
while (container.children.length > 0) { | ||
last_child = container.children[container.children.length - 1]; | ||
if (!last_child.open) { | ||
while (container.lastChild) { | ||
if (!container.lastChild.open) { | ||
break; | ||
} | ||
container = last_child; | ||
container = container.lastChild; | ||
match = matchAt(/[^ ]/, ln, offset); | ||
match = matchAt(reNonSpace, ln, offset); | ||
if (match === -1) { | ||
@@ -255,3 +299,3 @@ first_nonspace = ln.length; | ||
case 'ListItem': | ||
case 'Item': | ||
if (indent >= container.list_data.marker_offset + | ||
@@ -319,22 +363,8 @@ container.list_data.padding) { | ||
last_matched_container = container; | ||
allClosed = (container === this.oldtip); | ||
this.lastMatchedContainer = container; | ||
// This function is used to finalize and close any unmatched | ||
// blocks. We aren't ready to do this now, because we might | ||
// have a lazy paragraph continuation, in which case we don't | ||
// 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) { | ||
mythis.finalize(oldtip, line_number); | ||
oldtip = oldtip.parent; | ||
} | ||
already_done = true; | ||
}; | ||
// Check to see if we've hit 2nd blank line; if so break out of list: | ||
if (blank && container.last_line_blank) { | ||
this.breakOutOfLists(container, line_number); | ||
this.breakOutOfLists(container); | ||
} | ||
@@ -348,8 +378,9 @@ | ||
// 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; | ||
break; | ||
} else { | ||
@@ -365,11 +396,16 @@ first_nonspace = match; | ||
offset += CODE_INDENT; | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('IndentedCode', line_number, offset); | ||
} else { // indent > 4 in a lazy paragraph continuation | ||
break; | ||
allClosed = allClosed || | ||
this.closeUnmatchedBlocks(); | ||
container = this.addChild('IndentedCode', offset); | ||
} | ||
break; | ||
} | ||
} else if (ln.charCodeAt(first_nonspace) === C_GREATERTHAN) { | ||
offset = first_nonspace; | ||
var cc = ln.charCodeAt(offset); | ||
if (cc === C_GREATERTHAN) { | ||
// blockquote | ||
offset = first_nonspace + 1; | ||
offset += 1; | ||
// optional following space | ||
@@ -379,10 +415,10 @@ if (ln.charCodeAt(offset) === C_SPACE) { | ||
} | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('BlockQuote', line_number, offset); | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container = this.addChild('BlockQuote', first_nonspace); | ||
} else if ((match = ln.slice(first_nonspace).match(/^#{1,6}(?: +|$)/))) { | ||
} else if ((match = ln.slice(offset).match(reATXHeaderMarker))) { | ||
// ATX header | ||
offset = first_nonspace + match[0].length; | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('Header', line_number, first_nonspace); | ||
offset += match[0].length; | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container = this.addChild('Header', first_nonspace); | ||
container.level = match[0].trim().length; // number of #s | ||
@@ -394,18 +430,18 @@ // remove trailing ###s: | ||
} else if ((match = ln.slice(first_nonspace).match(/^`{3,}(?!.*`)|^~{3,}(?!.*~)/))) { | ||
} else if ((match = ln.slice(offset).match(reCodeFence))) { | ||
// fenced code block | ||
var fence_length = match[0].length; | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('FencedCode', line_number, first_nonspace); | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container = this.addChild('FencedCode', first_nonspace); | ||
container.fence_length = fence_length; | ||
container.fence_char = match[0][0]; | ||
container.fence_offset = first_nonspace - offset; | ||
offset = first_nonspace + fence_length; | ||
container.fence_offset = indent; | ||
offset += fence_length; | ||
break; | ||
} else if (matchAt(reHtmlBlockOpen, ln, first_nonspace) !== -1) { | ||
} else if (matchAt(reHtmlBlockOpen, ln, offset) !== -1) { | ||
// html block | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('HtmlBlock', line_number, first_nonspace); | ||
// note, we don't adjust offset because the tag is part of the text | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container = this.addChild('HtmlBlock', offset); | ||
offset -= indent; // back up so spaces are part of block | ||
break; | ||
@@ -415,21 +451,21 @@ | ||
container.strings.length === 1 && | ||
((match = ln.slice(first_nonspace).match(/^(?:=+|-+) *$/)))) { | ||
((match = ln.slice(offset).match(reSetextHeaderLine)))) { | ||
// setext header line | ||
closeUnmatchedBlocks(this); | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container.t = 'Header'; // convert Paragraph to SetextHeader | ||
container.level = match[0][0] === '=' ? 1 : 2; | ||
offset = ln.length; | ||
break; | ||
} else if (matchAt(reHrule, ln, first_nonspace) !== -1) { | ||
} else if (matchAt(reHrule, ln, offset) !== -1) { | ||
// hrule | ||
closeUnmatchedBlocks(this); | ||
container = this.addChild('HorizontalRule', line_number, first_nonspace); | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
container = this.addChild('HorizontalRule', first_nonspace); | ||
offset = ln.length - 1; | ||
break; | ||
} else if ((data = parseListMarker(ln, first_nonspace))) { | ||
} else if ((data = parseListMarker(ln, offset, indent))) { | ||
// list item | ||
closeUnmatchedBlocks(this); | ||
data.marker_offset = indent; | ||
offset = first_nonspace + data.padding; | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
offset += data.padding; | ||
@@ -439,3 +475,3 @@ // add the list if needed | ||
!(listsMatch(container.list_data, data))) { | ||
container = this.addChild('List', line_number, first_nonspace); | ||
container = this.addChild('List', first_nonspace); | ||
container.list_data = data; | ||
@@ -445,3 +481,3 @@ } | ||
// add the list item | ||
container = this.addChild('ListItem', line_number, first_nonspace); | ||
container = this.addChild('Item', first_nonspace); | ||
container.list_data = data; | ||
@@ -454,6 +490,2 @@ | ||
if (acceptsLines(container.t)) { | ||
// if it's a line container, it can't contain other containers | ||
break; | ||
} | ||
} | ||
@@ -464,3 +496,3 @@ | ||
match = matchAt(/[^ ]/, ln, offset); | ||
match = matchAt(reNonSpace, ln, offset); | ||
if (match === -1) { | ||
@@ -476,4 +508,3 @@ first_nonspace = ln.length; | ||
// First check for a lazy paragraph continuation: | ||
if (this.tip !== last_matched_container && | ||
!blank && | ||
if (!allClosed && !blank && | ||
this.tip.t === 'Paragraph' && | ||
@@ -489,3 +520,3 @@ this.tip.strings.length > 0) { | ||
// finalize any blocks not matched | ||
closeUnmatchedBlocks(this); | ||
allClosed = allClosed || this.closeUnmatchedBlocks(); | ||
@@ -500,5 +531,5 @@ // Block quote lines are never blank as they start with > | ||
container.t === 'FencedCode' || | ||
(container.t === 'ListItem' && | ||
container.children.length === 0 && | ||
container.start_line === line_number)); | ||
(container.t === 'Item' && | ||
!container.firstChild && | ||
container.sourcepos[0][0] === this.lineNumber)); | ||
@@ -521,6 +552,6 @@ var cont = container; | ||
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); | ||
this.finalize(container, this.lineNumber); | ||
} else { | ||
@@ -541,15 +572,10 @@ this.addLine(ln, offset); | ||
break; | ||
} else if (container.t !== 'HorizontalRule' && | ||
container.t !== 'Header') { | ||
} else { | ||
// create paragraph container for line | ||
container = this.addChild('Paragraph', line_number, first_nonspace); | ||
container = this.addChild('Paragraph', this.lineNumber, first_nonspace); | ||
this.addLine(ln, first_nonspace); | ||
} else { | ||
console.log("Line " + line_number.toString() + | ||
" with container type " + container.t + | ||
" did not match any condition."); | ||
} | ||
} | ||
} | ||
this.lastLineLength = ln.length - 1; // -1 for newline | ||
}; | ||
@@ -562,3 +588,3 @@ | ||
// parent of the closed block. | ||
var finalize = function(block, line_number) { | ||
var finalize = function(block, lineNumber) { | ||
var pos; | ||
@@ -570,12 +596,7 @@ // don't do anything if the block is already closed | ||
block.open = false; | ||
if (line_number > block.start_line) { | ||
block.end_line = line_number - 1; | ||
} else { | ||
block.end_line = line_number; | ||
} | ||
block.sourcepos[1] = [lineNumber, this.lastLineLength + 1]; | ||
switch (block.t) { | ||
case 'Paragraph': | ||
block.string_content = block.strings.join('\n').replace(/^ {2,}/m, ''); | ||
// delete block.strings; | ||
block.string_content = block.strings.join('\n'); | ||
@@ -595,8 +616,12 @@ // try parsing the beginning as link reference definitions: | ||
case 'Header': | ||
case 'HtmlBlock': | ||
block.string_content = block.strings.join('\n'); | ||
break; | ||
case 'HtmlBlock': | ||
block.literal = block.strings.join('\n'); | ||
break; | ||
case 'IndentedCode': | ||
block.string_content = block.strings.join('\n').replace(/(\n *)*$/, '\n'); | ||
stripFinalBlankLines(block.strings); | ||
block.literal = block.strings.join('\n') + '\n'; | ||
block.t = 'CodeBlock'; | ||
@@ -609,5 +634,5 @@ break; | ||
if (block.strings.length === 1) { | ||
block.string_content = ''; | ||
block.literal = ''; | ||
} else { | ||
block.string_content = block.strings.slice(1).join('\n') + '\n'; | ||
block.literal = block.strings.slice(1).join('\n') + '\n'; | ||
} | ||
@@ -618,12 +643,9 @@ block.t = 'CodeBlock'; | ||
case 'List': | ||
block.tight = true; // tight by default | ||
block.list_data.tight = true; // tight by default | ||
var numitems = block.children.length; | ||
var i = 0; | ||
while (i < numitems) { | ||
var item = block.children[i]; | ||
var item = block.firstChild; | ||
while (item) { | ||
// check for non-final list item ending with blank line: | ||
var last_item = i === numitems - 1; | ||
if (endsWithBlankLine(item) && !last_item) { | ||
block.tight = false; | ||
if (endsWithBlankLine(item) && item.next) { | ||
block.list_data.tight = false; | ||
break; | ||
@@ -633,14 +655,11 @@ } | ||
// spaces between any of them: | ||
var numsubitems = item.children.length; | ||
var j = 0; | ||
while (j < numsubitems) { | ||
var subitem = item.children[j]; | ||
var last_subitem = j === numsubitems - 1; | ||
if (endsWithBlankLine(subitem) && !(last_item && last_subitem)) { | ||
block.tight = false; | ||
var subitem = item.firstChild; | ||
while (subitem) { | ||
if (endsWithBlankLine(subitem) && (item.next || subitem.next)) { | ||
block.list_data.tight = false; | ||
break; | ||
} | ||
j++; | ||
subitem = subitem.next; | ||
} | ||
i++; | ||
item = item.next; | ||
} | ||
@@ -659,57 +678,46 @@ break; | ||
var processInlines = function(block) { | ||
var newblock = {}; | ||
newblock.t = block.t; | ||
newblock.start_line = block.start_line; | ||
newblock.start_column = block.start_column; | ||
newblock.end_line = block.end_line; | ||
switch(block.t) { | ||
case 'Paragraph': | ||
newblock.inline_content = | ||
this.inlineParser.parse(block.string_content.trim(), this.refmap); | ||
break; | ||
case 'Header': | ||
newblock.inline_content = | ||
this.inlineParser.parse(block.string_content.trim(), this.refmap); | ||
newblock.level = block.level; | ||
break; | ||
case 'List': | ||
newblock.list_data = block.list_data; | ||
newblock.tight = block.tight; | ||
break; | ||
case 'CodeBlock': | ||
newblock.string_content = block.string_content; | ||
newblock.info = block.info; | ||
break; | ||
case 'HtmlBlock': | ||
newblock.string_content = block.string_content; | ||
break; | ||
default: | ||
break; | ||
} | ||
if (block.children) { | ||
var newchildren = []; | ||
for (var i = 0; i < block.children.length; i++) { | ||
newchildren.push(this.processInlines(block.children[i])); | ||
var node, event; | ||
var walker = block.walker(); | ||
while ((event = walker.next())) { | ||
node = event.node; | ||
if (!event.entering && (node.t === 'Paragraph' || | ||
node.t === 'Header')) { | ||
this.inlineParser.parse(node, this.refmap); | ||
} | ||
newblock.children = newchildren; | ||
} | ||
return newblock; | ||
}; | ||
var Document = function() { | ||
var doc = new Node('Document', [[1, 1], [0, 0]]); | ||
doc.string_content = null; | ||
doc.strings = []; | ||
return doc; | ||
}; | ||
// The main parsing function. Returns a parsed document AST. | ||
var parse = function(input) { | ||
this.doc = makeBlock('Document', 1, 1); | ||
this.doc = new 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); | ||
this.lineNumber += 1; | ||
this.incorporateLine(lines[i]); | ||
} | ||
while (this.tip) { | ||
this.finalize(this.tip, len - 1); | ||
this.finalize(this.tip, len); | ||
} | ||
return this.processInlines(this.doc); | ||
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; | ||
}; | ||
@@ -719,7 +727,11 @@ | ||
// The DocParser object. | ||
function DocParser(){ | ||
function DocParser(options){ | ||
return { | ||
doc: makeBlock('Document', 1, 1), | ||
doc: new Document(), | ||
tip: this.doc, | ||
oldtip: this.doc, | ||
lineNumber: 0, | ||
lastMatchedContainer: this.doc, | ||
refmap: {}, | ||
lastLineLength: 0, | ||
inlineParser: new InlineParser(), | ||
@@ -732,3 +744,5 @@ breakOutOfLists: breakOutOfLists, | ||
processInlines: processInlines, | ||
parse: parse | ||
closeUnmatchedBlocks: closeUnmatchedBlocks, | ||
parse: parse, | ||
options: options || {} | ||
}; | ||
@@ -735,0 +749,0 @@ } |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
// derived from https://github.com/mathiasbynens/String.fromCodePoint | ||
@@ -5,3 +7,2 @@ /*! http://mths.be/fromcodepoint v0.2.1 by @mathias */ | ||
module.exports = function (_) { | ||
"use strict"; | ||
try { | ||
@@ -22,3 +23,2 @@ return String.fromCodePoint(_); | ||
var fromCodePoint = function() { | ||
"use strict"; | ||
var MAX_SIZE = 0x4000; | ||
@@ -25,0 +25,0 @@ var codeUnits = []; |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
var fromCodePoint = require('./from-code-point'); | ||
@@ -2131,3 +2133,2 @@ | ||
var entityToChar = function(m) { | ||
"use strict"; | ||
var isNumeric = /^&#/.test(m); | ||
@@ -2134,0 +2135,0 @@ var isHex = /^&#[Xx]/.test(m); |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
// commonmark.js - CommomMark in JavaScript | ||
@@ -12,12 +14,5 @@ // Copyright (C) 2014 John MacFarlane | ||
"use strict"; | ||
var util = require('util'); | ||
var renderAST = function(tree) { | ||
return util.inspect(tree, {depth: null}); | ||
}; | ||
module.exports.Node = require('./node'); | ||
module.exports.DocParser = require('./blocks'); | ||
module.exports.HtmlRenderer = require('./html-renderer'); | ||
module.exports.ASTRenderer = renderAST; | ||
module.exports.HtmlRenderer = require('./html'); | ||
module.exports.XmlRenderer = require('./xml'); |
@@ -0,1 +1,7 @@ | ||
"use strict"; | ||
var Node = require('./node'); | ||
var common = require('./common'); | ||
var normalizeURI = common.normalizeURI; | ||
var unescapeString = common.unescapeString; | ||
var fromCodePoint = require('./from-code-point.js'); | ||
@@ -7,3 +13,2 @@ var entityToChar = require('./html5-entities.js').entityToChar; | ||
var C_NEWLINE = 10; | ||
var C_SPACE = 32; | ||
var C_ASTERISK = 42; | ||
@@ -37,6 +42,6 @@ var C_UNDERSCORE = 95; | ||
var CLOSETAG = "</" + TAGNAME + "\\s*[>]"; | ||
var HTMLCOMMENT = "<!--([^-]+|[-][^-]+)*-->"; | ||
var HTMLCOMMENT = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"; | ||
var PROCESSINGINSTRUCTION = "[<][?].*?[?][>]"; | ||
var DECLARATION = "<![A-Z]+" + "\\s+[^>]*>"; | ||
var CDATA = "<!\\[CDATA\\[([^\\]]+|\\][^\\]]|\\]\\][^>])*\\]\\]>"; | ||
var CDATA = "<!\\[CDATA\\[[\\s\\S]*?\]\\]>"; | ||
var HTMLTAG = "(?:" + OPENTAG + "|" + CLOSETAG + "|" + HTMLCOMMENT + "|" + | ||
@@ -65,19 +70,29 @@ PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")"; | ||
var reAllEscapedChar = new RegExp('\\\\(' + ESCAPABLE + ')', 'g'); | ||
var reEntityHere = new RegExp('^' + ENTITY, 'i'); | ||
var reEntity = new RegExp(ENTITY, 'gi'); | ||
var reTicks = new RegExp('`+'); | ||
// Matches a character with a special meaning in markdown, | ||
// or a string of non-special characters. Note: we match | ||
// clumps of _ or * or `, because they need to be handled in groups. | ||
var reMain = /^(?:[_*`\n]+|[\[\]\\!<&*_]|(?: *[^\n `\[\]\\!<&*_]+)+|[ \n]+)/m; | ||
var reTicksHere = new RegExp('^`+'); | ||
// Replace entities and backslash escapes with literal characters. | ||
var unescapeString = function(s) { | ||
return s.replace(reAllEscapedChar, '$1') | ||
.replace(reEntity, entityToChar); | ||
}; | ||
var reEmailAutolink = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/; | ||
var reAutolink = /^<(?:coap|doi|javascript|aaa|aaas|about|acap|cap|cid|crid|data|dav|dict|dns|file|ftp|geo|go|gopher|h323|http|https|iax|icap|im|imap|info|ipp|iris|iris.beep|iris.xpc|iris.xpcs|iris.lwz|ldap|mailto|mid|msrp|msrps|mtqp|mupdate|news|nfs|ni|nih|nntp|opaquelocktoken|pop|pres|rtsp|service|session|shttp|sieve|sip|sips|sms|snmp|soap.beep|soap.beeps|tag|tel|telnet|tftp|thismessage|tn3270|tip|tv|urn|vemmi|ws|wss|xcon|xcon-userid|xmlrpc.beep|xmlrpc.beeps|xmpp|z39.50r|z39.50s|adiumxtra|afp|afs|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|chrome|chrome-extension|com-eventbrite-attendee|content|cvs|dlna-playsingle|dlna-playcontainer|dtn|dvb|ed2k|facetime|feed|finger|fish|gg|git|gizmoproject|gtalk|hcp|icon|ipn|irc|irc6|ircs|itms|jar|jms|keyparc|lastfm|ldaps|magnet|maps|market|message|mms|ms-help|msnim|mumble|mvn|notes|oid|palm|paparazzi|platform|proxy|psyc|query|res|resource|rmi|rsync|rtmp|secondlife|sftp|sgn|skype|smb|soldat|spotify|ssh|steam|svn|teamspeak|things|udp|unreal|ut2004|ventrilo|view-source|webcal|wtai|wyciwyg|xfire|xri|ymsgr):[^<>\x00-\x20]*>/i; | ||
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; | ||
// Normalize reference label: collapse internal whitespace | ||
@@ -91,2 +106,8 @@ // to single space, remove leading/trailing whitespace, case fold. | ||
var text = function(s) { | ||
var node = new Node('Text'); | ||
node.literal = s; | ||
return node; | ||
}; | ||
// INLINE PARSER | ||
@@ -122,3 +143,3 @@ | ||
var spnl = function() { | ||
this.match(/^ *(?:\n *)?/); | ||
this.match(reSpnl); | ||
return 1; | ||
@@ -131,6 +152,6 @@ }; | ||
// Attempt to parse backticks, returning either a backtick code span or a | ||
// Attempt to parse backticks, adding either a backtick code span or a | ||
// literal sequence of backticks. | ||
var parseBackticks = function(inlines) { | ||
var ticks = this.match(/^`+/); | ||
var parseBackticks = function(block) { | ||
var ticks = this.match(reTicksHere); | ||
if (!ticks) { | ||
@@ -142,8 +163,10 @@ return 0; | ||
var matched; | ||
while (!foundCode && (matched = this.match(/`+/m))) { | ||
var node; | ||
while (!foundCode && (matched = this.match(reTicks))) { | ||
if (matched === ticks) { | ||
inlines.push({ t: 'Code', c: this.subject.slice(afterOpenTicks, | ||
this.pos - ticks.length) | ||
.replace(/[ \n]+/g, ' ') | ||
.trim() }); | ||
node = new Node('Code'); | ||
node.literal = this.subject.slice(afterOpenTicks, | ||
this.pos - ticks.length) | ||
.trim().replace(reWhitespace, ' '); | ||
block.appendChild(node); | ||
return true; | ||
@@ -154,3 +177,3 @@ } | ||
this.pos = afterOpenTicks; | ||
inlines.push({ t: 'Text', c: ticks }); | ||
block.appendChild(text(ticks)); | ||
return true; | ||
@@ -161,16 +184,18 @@ }; | ||
// character, a hard line break (if the backslash is followed by a newline), | ||
// or a literal backslash to the 'inlines' list. | ||
var parseBackslash = function(inlines) { | ||
// or a literal backslash to the block's children. | ||
var parseBackslash = function(block) { | ||
var subj = this.subject, | ||
pos = this.pos; | ||
var node; | ||
if (subj.charCodeAt(pos) === C_BACKSLASH) { | ||
if (subj.charAt(pos + 1) === '\n') { | ||
this.pos = this.pos + 2; | ||
inlines.push({ t: 'Hardbreak' }); | ||
node = new Node('Hardbreak'); | ||
block.appendChild(node); | ||
} else if (reEscapable.test(subj.charAt(pos + 1))) { | ||
this.pos = this.pos + 2; | ||
inlines.push({ t: 'Text', c: subj.charAt(pos + 1) }); | ||
block.appendChild(text(subj.charAt(pos + 1))); | ||
} else { | ||
this.pos++; | ||
inlines.push({t: 'Text', c: '\\'}); | ||
block.appendChild(text('\\')); | ||
} | ||
@@ -184,18 +209,21 @@ return true; | ||
// Attempt to parse an autolink (URL or email in pointy brackets). | ||
var parseAutolink = function(inlines) { | ||
var parseAutolink = function(block) { | ||
var m; | ||
var dest; | ||
if ((m = this.match(/^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/))) { // email autolink | ||
var node; | ||
if ((m = this.match(reEmailAutolink))) { | ||
dest = m.slice(1, -1); | ||
inlines.push( | ||
{t: 'Link', | ||
label: [{ t: 'Text', c: dest }], | ||
destination: 'mailto:' + encodeURI(unescape(dest)) }); | ||
node = new Node('Link'); | ||
node.destination = normalizeURI('mailto:' + dest); | ||
node.title = ''; | ||
node.appendChild(text(dest)); | ||
block.appendChild(node); | ||
return true; | ||
} else if ((m = this.match(/^<(?:coap|doi|javascript|aaa|aaas|about|acap|cap|cid|crid|data|dav|dict|dns|file|ftp|geo|go|gopher|h323|http|https|iax|icap|im|imap|info|ipp|iris|iris.beep|iris.xpc|iris.xpcs|iris.lwz|ldap|mailto|mid|msrp|msrps|mtqp|mupdate|news|nfs|ni|nih|nntp|opaquelocktoken|pop|pres|rtsp|service|session|shttp|sieve|sip|sips|sms|snmp|soap.beep|soap.beeps|tag|tel|telnet|tftp|thismessage|tn3270|tip|tv|urn|vemmi|ws|wss|xcon|xcon-userid|xmlrpc.beep|xmlrpc.beeps|xmpp|z39.50r|z39.50s|adiumxtra|afp|afs|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|chrome|chrome-extension|com-eventbrite-attendee|content|cvs|dlna-playsingle|dlna-playcontainer|dtn|dvb|ed2k|facetime|feed|finger|fish|gg|git|gizmoproject|gtalk|hcp|icon|ipn|irc|irc6|ircs|itms|jar|jms|keyparc|lastfm|ldaps|magnet|maps|market|message|mms|ms-help|msnim|mumble|mvn|notes|oid|palm|paparazzi|platform|proxy|psyc|query|res|resource|rmi|rsync|rtmp|secondlife|sftp|sgn|skype|smb|soldat|spotify|ssh|steam|svn|teamspeak|things|udp|unreal|ut2004|ventrilo|view-source|webcal|wtai|wyciwyg|xfire|xri|ymsgr):[^<>\x00-\x20]*>/i))) { | ||
} else if ((m = this.match(reAutolink))) { | ||
dest = m.slice(1, -1); | ||
inlines.push({ | ||
t: 'Link', | ||
label: [{ t: 'Text', c: dest }], | ||
destination: encodeURI(unescape(dest)) }); | ||
node = new Node('Link'); | ||
node.destination = normalizeURI(dest); | ||
node.title = ''; | ||
node.appendChild(text(dest)); | ||
block.appendChild(node); | ||
return true; | ||
@@ -208,6 +236,9 @@ } else { | ||
// Attempt to parse a raw HTML tag. | ||
var parseHtmlTag = function(inlines) { | ||
var parseHtmlTag = function(block) { | ||
var m = this.match(reHtmlTag); | ||
var node; | ||
if (m) { | ||
inlines.push({ t: 'Html', c: m }); | ||
node = new Node('Html'); | ||
node.literal = m; | ||
block.appendChild(node); | ||
return true; | ||
@@ -243,13 +274,13 @@ } else { | ||
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)); | ||
} | ||
@@ -262,17 +293,4 @@ this.pos = startpos; | ||
var Emph = function(ils) { | ||
return {t: 'Emph', c: ils}; | ||
}; | ||
var Strong = function(ils) { | ||
return {t: 'Strong', c: ils}; | ||
}; | ||
var Str = function(s) { | ||
return {t: 'Text', c: s}; | ||
}; | ||
// Attempt to parse emphasis or strong emphasis. | ||
var parseEmphasis = function(cc, inlines) { | ||
var parseEmphasis = function(cc, block) { | ||
var res = this.scanDelims(cc); | ||
@@ -287,3 +305,4 @@ var numdelims = res.numdelims; | ||
this.pos += numdelims; | ||
inlines.push(Str(this.subject.slice(startpos, this.pos))); | ||
var node = text(this.subject.slice(startpos, this.pos)); | ||
block.appendChild(node); | ||
@@ -293,3 +312,3 @@ // Add entry to stack for this opener | ||
numdelims: numdelims, | ||
pos: inlines.length - 1, | ||
node: node, | ||
previous: this.delimiters, | ||
@@ -320,17 +339,3 @@ next: null, | ||
var removeGaps = function(inlines) { | ||
// remove gaps from inlines | ||
var i, j; | ||
j = 0; | ||
for (i = 0 ; i < inlines.length; i++) { | ||
if (inlines[i] !== null) { | ||
inlines[j] = inlines[i]; | ||
j++; | ||
} | ||
} | ||
inlines.splice(j); | ||
}; | ||
var processEmphasis = function(inlines, stack_bottom) { | ||
"use strict"; | ||
var processEmphasis = function(block, stack_bottom) { | ||
var opener, closer; | ||
@@ -340,5 +345,3 @@ var opener_inl, closer_inl; | ||
var use_delims; | ||
var contents; | ||
var emph; | ||
var i; | ||
var tmp, next; | ||
@@ -370,4 +373,4 @@ // find first closer above stack_bottom: | ||
opener_inl = inlines[opener.pos]; | ||
closer_inl = inlines[closer.pos]; | ||
opener_inl = opener.node; | ||
closer_inl = closer.node; | ||
@@ -377,17 +380,22 @@ // remove used delimiters from stack elts and inlines | ||
closer.numdelims -= use_delims; | ||
opener_inl.c = opener_inl.c.slice(0, opener_inl.c.length - use_delims); | ||
closer_inl.c = closer_inl.c.slice(0, closer_inl.c.length - use_delims); | ||
opener_inl.literal = | ||
opener_inl.literal.slice(0, | ||
opener_inl.literal.length - use_delims); | ||
closer_inl.literal = | ||
closer_inl.literal.slice(0, | ||
closer_inl.literal.length - use_delims); | ||
// build contents for new emph element | ||
contents = inlines.slice(opener.pos + 1, closer.pos); | ||
removeGaps(contents); | ||
var emph = new Node(use_delims === 1 ? 'Emph' : 'Strong'); | ||
emph = use_delims === 1 ? Emph(contents) : Strong(contents); | ||
// insert into list of inlines | ||
inlines[opener.pos + 1] = emph; | ||
for (i = opener.pos + 2; i < closer.pos; i++) { | ||
inlines[i] = null; | ||
tmp = opener_inl.next; | ||
while (tmp && tmp !== closer_inl) { | ||
next = tmp.next; | ||
tmp.unlink(); | ||
emph.appendChild(tmp); | ||
tmp = next; | ||
} | ||
opener_inl.insertAfter(emph); | ||
// remove elts btw opener and closer in delimiters stack | ||
@@ -403,3 +411,3 @@ tempstack = closer.previous; | ||
if (opener.numdelims === 0) { | ||
inlines[opener.pos] = null; | ||
opener_inl.unlink(); | ||
this.removeDelimiter(opener); | ||
@@ -409,3 +417,3 @@ } | ||
if (closer.numdelims === 0) { | ||
inlines[closer.pos] = null; | ||
closer_inl.unlink(); | ||
tempstack = closer.next; | ||
@@ -416,3 +424,2 @@ this.removeDelimiter(closer); | ||
} else { | ||
@@ -428,4 +435,2 @@ closer = closer.next; | ||
removeGaps(inlines); | ||
// remove all delimiters | ||
@@ -454,7 +459,7 @@ while (this.delimiters !== stack_bottom) { | ||
if (res) { // chop off surrounding <..>: | ||
return encodeURI(unescape(unescapeString(res.substr(1, res.length - 2)))); | ||
return normalizeURI(unescapeString(res.substr(1, res.length - 2))); | ||
} else { | ||
res = this.match(reLinkDestination); | ||
if (res !== null) { | ||
return encodeURI(unescape(unescapeString(res))); | ||
return normalizeURI(unescapeString(res)); | ||
} else { | ||
@@ -468,13 +473,13 @@ return null; | ||
var parseLinkLabel = function() { | ||
var m = this.match(/^\[(?:[^\\\[\]]|\\[\[\]]){0,1000}\]/); | ||
var m = this.match(reLinkLabel); | ||
return m === null ? 0 : m.length; | ||
}; | ||
// Add open bracket to delimiter stack and add a Str to inlines. | ||
var parseOpenBracket = function(inlines) { | ||
// Add open bracket to delimiter stack and add a text node to block's children. | ||
var parseOpenBracket = function(block) { | ||
var startpos = this.pos; | ||
this.pos += 1; | ||
inlines.push(Str("[")); | ||
var node = text('['); | ||
block.appendChild(node); | ||
@@ -484,3 +489,3 @@ // Add entry to stack for this opener | ||
numdelims: 1, | ||
pos: inlines.length - 1, | ||
node: node, | ||
previous: this.delimiters, | ||
@@ -501,5 +506,4 @@ next: null, | ||
// IF next character is [, and ! delimiter to delimiter stack and | ||
// add a Str to inlines. Otherwise just add a Str. | ||
var parseBang = function(inlines) { | ||
// add a text node to block's children. Otherwise just add a text node. | ||
var parseBang = function(block) { | ||
var startpos = this.pos; | ||
@@ -509,8 +513,10 @@ this.pos += 1; | ||
this.pos += 1; | ||
inlines.push(Str("![")); | ||
var node = text('!['); | ||
block.appendChild(node); | ||
// Add entry to stack for this opener | ||
this.delimiters = { cc: C_BANG, | ||
numdelims: 1, | ||
pos: inlines.length - 1, | ||
node: node, | ||
previous: this.delimiters, | ||
@@ -526,3 +532,3 @@ next: null, | ||
} else { | ||
inlines.push(Str("!")); | ||
block.appendChild(text('!')); | ||
} | ||
@@ -534,5 +540,5 @@ return true; | ||
// stack. Add either a link or image, or a plain [ character, | ||
// to the inlines stack. If there is a matching delimiter, | ||
// to block's children. If there is a matching delimiter, | ||
// remove it from the delimiter stack. | ||
var parseCloseBracket = function(inlines) { | ||
var parseCloseBracket = function(block) { | ||
var startpos; | ||
@@ -543,4 +549,2 @@ var is_image; | ||
var matched = false; | ||
var link_text; | ||
var i; | ||
var reflabel; | ||
@@ -564,3 +568,3 @@ var opener; | ||
// no matched opener, just return a literal | ||
inlines.push(Str("]")); | ||
block.appendChild(text(']')); | ||
return true; | ||
@@ -571,3 +575,3 @@ } | ||
// no matched opener, just return a literal | ||
inlines.push(Str("]")); | ||
block.appendChild(text(']')); | ||
// take opener off emphasis stack | ||
@@ -580,11 +584,2 @@ this.removeDelimiter(opener); | ||
is_image = opener.cc === C_BANG; | ||
// instead of copying a slice, we null out the | ||
// parts of inlines that don't correspond to link_text; | ||
// later, we'll collapse them. This is awkward, and could | ||
// be simplified if we made inlines a linked list rather than | ||
// an array: | ||
link_text = inlines.slice(0); | ||
for (i = 0; i < opener.pos + 1; i++) { | ||
link_text[i] = null; | ||
} | ||
@@ -600,6 +595,7 @@ // Check to see if we have a link/image | ||
// make sure there's a space before the title: | ||
(/^\s/.test(this.subject.charAt(this.pos - 1)) && | ||
(title = this.parseLinkTitle() || '') || true) && | ||
(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; | ||
@@ -635,10 +631,19 @@ } | ||
if (matched) { | ||
this.processEmphasis(link_text, opener.previous); | ||
var node = new Node(is_image ? 'Image' : 'Link'); | ||
node.destination = dest; | ||
node.title = title || ''; | ||
// remove the part of inlines that became link_text. | ||
// see note above on why we need to do this instead of splice: | ||
for (i = opener.pos; i < inlines.length; i++) { | ||
inlines[i] = null; | ||
var tmp, next; | ||
tmp = opener.node.next; | ||
while (tmp) { | ||
next = tmp.next; | ||
tmp.unlink(); | ||
node.appendChild(tmp); | ||
tmp = next; | ||
} | ||
block.appendChild(node); | ||
this.processEmphasis(node, opener.previous); | ||
opener.node.unlink(); | ||
// processEmphasis will remove this and later delimiters. | ||
@@ -657,6 +662,2 @@ // Now, for a link, we also deactivate earlier link openers. | ||
inlines.push({t: is_image ? 'Image' : 'Link', | ||
destination: dest, | ||
title: title, | ||
label: link_text}); | ||
return true; | ||
@@ -668,3 +669,3 @@ | ||
this.pos = startpos; | ||
inlines.push(Str("]")); | ||
block.appendChild(text(']')); | ||
return true; | ||
@@ -676,6 +677,6 @@ } | ||
// Attempt to parse an entity, return Entity object if successful. | ||
var parseEntity = function(inlines) { | ||
var parseEntity = function(block) { | ||
var m; | ||
if ((m = this.match(reEntityHere))) { | ||
inlines.push({ t: 'Text', c: entityToChar(m) }); | ||
block.appendChild(text(entityToChar(m))); | ||
return true; | ||
@@ -688,7 +689,7 @@ } else { | ||
// Parse a run of ordinary characters, or a single character with | ||
// a special meaning in markdown, as a plain string, adding to inlines. | ||
var parseString = function(inlines) { | ||
// a special meaning in markdown, as a plain string. | ||
var parseString = function(block) { | ||
var m; | ||
if ((m = this.match(reMain))) { | ||
inlines.push({ t: 'Text', c: m }); | ||
block.appendChild(text(m)); | ||
return true; | ||
@@ -702,13 +703,17 @@ } else { | ||
// line break; otherwise a soft line break. | ||
var parseNewline = function(inlines) { | ||
var m = this.match(/^ *\n/); | ||
if (m) { | ||
if (m.length > 2) { | ||
inlines.push({ t: 'Hardbreak' }); | ||
} else if (m.length > 0) { | ||
inlines.push({ t: 'Softbreak' }); | ||
var parseNewline = function(block) { | ||
this.pos += 1; // assume we're at a \n | ||
// check previous node for trailing spaces | ||
var lastc = block.lastChild; | ||
if (lastc && lastc.t === 'Text') { | ||
var sps = reFinalSpace.exec(lastc.literal)[0].length; | ||
if (sps > 0) { | ||
lastc.literal = lastc.literal.replace(reFinalSpace, ''); | ||
} | ||
return true; | ||
block.appendChild(new Node(sps >= 2 ? 'Hardbreak' : 'Softbreak')); | ||
} else { | ||
block.appendChild(new Node('Softbreak')); | ||
} | ||
return false; | ||
this.match(reInitialSpace); // gobble leading spaces in next line | ||
return true; | ||
}; | ||
@@ -720,3 +725,2 @@ | ||
this.pos = 0; | ||
this.label_nest_level = 0; | ||
var rawlabel; | ||
@@ -777,6 +781,6 @@ var dest; | ||
// Parse the next inline element in subject, advancing subject position. | ||
// On success, add the result to the inlines list, and return true. | ||
// On success, add the result to block's children and return true. | ||
// On failure, return false. | ||
var parseInline = function(inlines) { | ||
"use strict"; | ||
var parseInline = function(block) { | ||
var res; | ||
var c = this.peek(); | ||
@@ -786,35 +790,33 @@ if (c === -1) { | ||
} | ||
var res; | ||
switch(c) { | ||
case C_NEWLINE: | ||
case C_SPACE: | ||
res = this.parseNewline(inlines); | ||
res = this.parseNewline(block); | ||
break; | ||
case C_BACKSLASH: | ||
res = this.parseBackslash(inlines); | ||
res = this.parseBackslash(block); | ||
break; | ||
case C_BACKTICK: | ||
res = this.parseBackticks(inlines); | ||
res = this.parseBackticks(block); | ||
break; | ||
case C_ASTERISK: | ||
case C_UNDERSCORE: | ||
res = this.parseEmphasis(c, inlines); | ||
res = this.parseEmphasis(c, block); | ||
break; | ||
case C_OPEN_BRACKET: | ||
res = this.parseOpenBracket(inlines); | ||
res = this.parseOpenBracket(block); | ||
break; | ||
case C_BANG: | ||
res = this.parseBang(inlines); | ||
res = this.parseBang(block); | ||
break; | ||
case C_CLOSE_BRACKET: | ||
res = this.parseCloseBracket(inlines); | ||
res = this.parseCloseBracket(block); | ||
break; | ||
case C_LESSTHAN: | ||
res = this.parseAutolink(inlines) || this.parseHtmlTag(inlines); | ||
res = this.parseAutolink(block) || this.parseHtmlTag(block); | ||
break; | ||
case C_AMPERSAND: | ||
res = this.parseEntity(inlines); | ||
res = this.parseEntity(block); | ||
break; | ||
default: | ||
res = this.parseString(inlines); | ||
res = this.parseString(block); | ||
break; | ||
@@ -824,3 +826,5 @@ } | ||
this.pos += 1; | ||
inlines.push({t: 'Text', c: fromCodePoint(c)}); | ||
var textnode = new Node('Text'); | ||
textnode.literal = fromCodePoint(c); | ||
block.appendChild(textnode); | ||
} | ||
@@ -831,13 +835,12 @@ | ||
// Parse s as a list of inlines, using refmap to resolve references. | ||
var parseInlines = function(s, refmap) { | ||
this.subject = s; | ||
// Parse string_content in block into inline children, | ||
// using refmap to resolve references. | ||
var parseInlines = function(block, refmap) { | ||
this.subject = block.string_content.trim(); | ||
this.pos = 0; | ||
this.refmap = refmap || {}; | ||
this.delimiters = null; | ||
var inlines = []; | ||
while (this.parseInline(inlines)) { | ||
while (this.parseInline(block)) { | ||
} | ||
this.processEmphasis(inlines, null); | ||
return inlines; | ||
this.processEmphasis(block, null); | ||
}; | ||
@@ -847,6 +850,4 @@ | ||
function InlineParser(){ | ||
"use strict"; | ||
return { | ||
subject: '', | ||
label_nest_level: 0, // used by parseLinkLabel method | ||
delimiters: null, // used by parseEmphasis method | ||
@@ -858,3 +859,2 @@ pos: 0, | ||
spnl: spnl, | ||
unescapeString: unescapeString, | ||
parseBackticks: parseBackticks, | ||
@@ -884,2 +884,1 @@ parseBackslash: parseBackslash, | ||
module.exports = InlineParser; | ||
{ "name": "commonmark", | ||
"description": "a strongly specified, highly compatible variant of Markdown", | ||
"version": "0.15.0", | ||
"version": "0.16.0", | ||
"homepage": "http://commonmark.org", | ||
@@ -5,0 +5,0 @@ "keywords": |
203
test.js
@@ -6,78 +6,163 @@ #!/usr/bin/env node | ||
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 repeat = function(pattern, count) { | ||
if (count < 1) { | ||
return ''; | ||
} | ||
var result = ''; | ||
while (count > 1) { | ||
if (count & 1) { | ||
result += pattern; | ||
} | ||
count >>= 1; | ||
pattern += pattern; | ||
} | ||
return result + pattern; | ||
}; | ||
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('[0m') | ||
}; | ||
var writer = new commonmark.HtmlRenderer(); | ||
var reader = new commonmark.DocParser(); | ||
var passed = 0; | ||
var failed = 0; | ||
var results = { | ||
passed: 0, | ||
failed: 0 | ||
}; | ||
var showSpaces = function(s) { | ||
var t = s; | ||
return t.replace(/\t/g, '→') | ||
.replace(/ /g, '␣'); | ||
var t = s; | ||
return t.replace(/\t/g, '→') | ||
.replace(/ /g, '␣'); | ||
}; | ||
var pathologicalTest = function(testcase, results) { | ||
cursor.write(testcase.name + ' '); | ||
console.time(' elapsed time'); | ||
var actual = writer.render(reader.parse(testcase.input)); | ||
if (actual === testcase.expected) { | ||
cursor.green().write('✓\n').reset(); | ||
results.passed += 1; | ||
} else { | ||
cursor.red().write('✘\n'); | ||
cursor.cyan(); | ||
cursor.write('=== markdown ===============\n'); | ||
cursor.write(showSpaces(testcase.input)); | ||
cursor.write('=== expected ===============\n'); | ||
cursor.write(showSpaces(testcase.expected)); | ||
cursor.write('=== got ====================\n'); | ||
cursor.write(showSpaces(actual)); | ||
cursor.write('\n'); | ||
cursor.reset(); | ||
results.failed += 1; | ||
} | ||
console.timeEnd(' elapsed time'); | ||
}; | ||
fs.readFile('spec.txt', 'utf8', function(err, data) { | ||
if (err) { | ||
return console.log(err); | ||
} | ||
var i; | ||
var examples = []; | ||
var current_section = ""; | ||
var example_number = 0; | ||
var tests = data | ||
.replace(/\r\n?/g, "\n") // Normalize newlines for platform independence | ||
.replace(/^<!-- END TESTS -->(.|[\n])*/m, ''); | ||
if (err) { | ||
return console.log(err); | ||
} | ||
var i; | ||
var examples = []; | ||
var current_section = ""; | ||
var example_number = 0; | ||
var tests = data | ||
.replace(/\r\n?/g, "\n") // Normalize newlines for platform independence | ||
.replace(/^<!-- END TESTS -->(.|[\n])*/m, ''); | ||
tests.replace(/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$|^#{1,6} *(.*)$/gm, | ||
function(_, markdownSubmatch, htmlSubmatch, sectionSubmatch){ | ||
if (sectionSubmatch) { | ||
current_section = sectionSubmatch; | ||
} else { | ||
example_number++; | ||
examples.push({markdown: markdownSubmatch, | ||
html: htmlSubmatch, | ||
section: current_section, | ||
number: example_number}); | ||
} | ||
}); | ||
tests.replace(/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$|^#{1,6} *(.*)$/gm, | ||
function(_, markdownSubmatch, htmlSubmatch, sectionSubmatch){ | ||
if (sectionSubmatch) { | ||
current_section = sectionSubmatch; | ||
} else { | ||
example_number++; | ||
examples.push({markdown: markdownSubmatch, | ||
html: htmlSubmatch, | ||
section: current_section, | ||
number: example_number}); | ||
} | ||
}); | ||
current_section = ""; | ||
current_section = ""; | ||
console.time("Elapsed time"); | ||
cursor.write('Spec tests:\n\n'); | ||
console.time("Elapsed time"); | ||
for (i = 0; i < examples.length; i++) { | ||
var example = examples[i]; | ||
if (example.section !== current_section) { | ||
if (current_section !== '') { | ||
cursor.write('\n'); | ||
} | ||
current_section = example.section; | ||
cursor.reset().write(current_section).reset().write(' '); | ||
for (i = 0; i < examples.length; i++) { | ||
var example = examples[i]; | ||
if (example.section !== current_section) { | ||
if (current_section !== '') { | ||
cursor.write('\n'); | ||
} | ||
current_section = example.section; | ||
cursor.reset().write(current_section).reset().write(' '); | ||
} | ||
var actual = writer.render(reader.parse(example.markdown.replace(/→/g, '\t'))); | ||
if (actual === example.html) { | ||
results.passed++; | ||
cursor.green().write('✓').reset(); | ||
} else { | ||
results.failed++; | ||
cursor.write('\n'); | ||
cursor.red().write('✘ Example ' + example.number + '\n'); | ||
cursor.cyan(); | ||
cursor.write('=== markdown ===============\n'); | ||
cursor.write(showSpaces(example.markdown)); | ||
cursor.write('=== expected ===============\n'); | ||
cursor.write(showSpaces(example.html)); | ||
cursor.write('=== got ====================\n'); | ||
cursor.write(showSpaces(actual)); | ||
cursor.reset(); | ||
} | ||
} | ||
var actual = writer.renderBlock(reader.parse(example.markdown.replace(/→/g, '\t'))); | ||
if (actual === example.html) { | ||
passed++; | ||
cursor.green().write('✓').reset(); | ||
} else { | ||
failed++; | ||
cursor.write('\n'); | ||
cursor.write('\n'); | ||
console.timeEnd("Elapsed time"); | ||
cursor.red().write('✘ Example ' + example.number + '\n'); | ||
cursor.cyan(); | ||
cursor.write('=== markdown ===============\n'); | ||
cursor.write(showSpaces(example.markdown)); | ||
cursor.write('=== expected ===============\n'); | ||
cursor.write(showSpaces(example.html)); | ||
cursor.write('=== got ====================\n'); | ||
cursor.write(showSpaces(actual)); | ||
cursor.reset(); | ||
// pathological cases | ||
cursor.write('\nPathological cases:\n'); | ||
var cases = [ | ||
{ name: 'U+0000 in input', | ||
input: 'abc\u0000xyz\u0000\n', | ||
expected: '<p>abc\ufffdxyz\ufffd</p>\n' }, | ||
{ name: 'nested strong emph 10000 deep', | ||
input: repeat('*a **a ', 10000) + 'b' + repeat(' a** a*', 10000), | ||
expected: '<p>' + repeat('<em>a <strong>a ', 10000) + 'b' + | ||
repeat(' a</strong> a</em>', 10000) + '</p>\n' }, | ||
{ name: 'nested brackets 10000 deep', | ||
input: repeat('[', 10000) + 'a' + repeat(']', 10000), | ||
expected: '<p>' + repeat('[', 10000) + 'a' + repeat(']', 10000) + | ||
'</p>\n' }, | ||
{ name: 'nested block quote 10000 deep', | ||
input: repeat('> ', 10000) + 'a\n', | ||
expected: repeat('<blockquote>\n', 10000) + '<p>a</p>\n' + | ||
repeat('</blockquote>\n', 10000) } | ||
]; | ||
for (i = 0; i < cases.length; i++) { | ||
pathologicalTest(cases[i], results); | ||
} | ||
} | ||
cursor.write('\n' + passed.toString() + ' tests passed, ' + | ||
failed.toString() + ' failed.\n'); | ||
cursor.write('\n'); | ||
console.timeEnd("Elapsed time"); | ||
cursor.write(results.passed.toString() + ' tests passed, ' + | ||
results.failed.toString() + ' failed.\n'); | ||
}); |
Sorry, the diff of this file is not supported yet
120149
19
4388