From 0393a77aee4a99ec66854c2687f767576f0ae6da Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Sun, 7 Sep 2014 19:29:01 +0400 Subject: [PATCH] Added lists and basic support for tables --- lib/lexer_block.js | 1 + lib/lexer_block/list.js | 205 +++++++++++++++++++++++++++++++-------- lib/lexer_block/table.js | 82 ++++++++++++++++ lib/renderer.js | 40 ++++++++ 4 files changed, 288 insertions(+), 40 deletions(-) create mode 100644 lib/lexer_block/table.js diff --git a/lib/lexer_block.js b/lib/lexer_block.js index f2d0d6c..08c96da 100644 --- a/lib/lexer_block.js +++ b/lib/lexer_block.js @@ -14,6 +14,7 @@ rules.push(require('./lexer_block/list')); rules.push(require('./lexer_block/heading')); rules.push(require('./lexer_block/lheading')); rules.push(require('./lexer_block/blockquote')); +rules.push(require('./lexer_block/table')); rules.push(require('./lexer_block/paragraph')); diff --git a/lib/lexer_block/list.js b/lib/lexer_block/list.js index 86ec476..dc24c0f 100644 --- a/lib/lexer_block/list.js +++ b/lib/lexer_block/list.js @@ -3,78 +3,203 @@ 'use strict'; -var isEmpty = require('../helpers').isEmpty; +var isEmpty = require('../helpers').isEmpty; +var getLines = require('../helpers').getLines; +var skipSpaces = require('../helpers').skipSpaces; var skipEmptyLines = require('../helpers').skipEmptyLines; -function bullet_item(state, startLine, endLine, silent) { - var marker, nextLine, +function findEndOfItem(state, startLine, endLine, indent) { + var lastNonEmptyLine = startLine, rules_named = state.lexerBlock.rules_named, + nextLine = startLine + 1; + + // jump line-by-line until empty one or EOF + for (; nextLine < endLine; nextLine++) { + if (isEmpty(state, nextLine)) { + // two successive newlines end the list + if (lastNonEmptyLine < nextLine - 1) { break; } + continue; + } + + // if this line is indented more than with N spaces, + // it's the new paragraph of the same list item + if (state.tShift[nextLine] >= indent) { + lastNonEmptyLine = nextLine; + continue; + } + + // paragraph after linebreak - not a continuation + if (lastNonEmptyLine < nextLine - 1) { break; } + + // Otherwise it's a possible continuation, so we need to check + // other tags, same as with blockquote and paragraph. + + if (rules_named.fences(state, nextLine, endLine, true)) { break; } + if (rules_named.hr(state, nextLine, endLine, true)) { break; } + if (rules_named.heading(state, nextLine, endLine, true)) { break; } + if (rules_named.lheading(state, nextLine, endLine, true)) { break; } + if (rules_named.blockquote(state, nextLine, endLine, true)) { break; } + if (rules_named.list(state, nextLine, endLine, true)) { break; } + //if (rules_named.tag(state, nextLine, endLine, true)) { break; } + //if (rules_named.def(state, nextLine, endLine, true)) { break; } + lastNonEmptyLine = nextLine; + } + return lastNonEmptyLine; +} + +// skips `[-+*][\n ]`, returns -1 if not found +function skipBulletListMarker(state, startLine/*, endLine*/) { + var marker, pos = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine]; - // TODO: supporting list with only one paragraph for now - - if (pos > max) { return false; } + if (pos > max) { return -1; } marker = state.src.charCodeAt(pos++); - // Check bullet if (marker !== 0x2A/* * */ && marker !== 0x2D/* - */ && marker !== 0x2B/* + */) { - return false; + return -1; } - // Empty list item - if (pos > max) { - state.tokens.push({ type: 'list_item_open' }); - state.tokens.push({ type: 'list_item_close' }); - return true; + if (pos < max && state.src.charCodeAt(pos) !== 0x20) { + // " 1.test " - is not a list item + return -1; } + return pos; +} - if (state.src.charCodeAt(pos++) !== 0x20) { return false; } +// skips `\d+\.[\n ]`, returns -1 if not found +function skipOrderedListMarker(state, startLine/*, endLine*/) { + var marker, + haveMarker = false, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; - // If we reached this, it's surely a list item - if (silent) { return true; } + if (pos + 1 > max) { return -1; } - nextLine = startLine + 1; + marker = state.src.charCodeAt(pos++); + if (marker < 0x30/* 0 */ || marker > 0x39/* 9 */) { return -1; } - // jump line-by-line until empty one or EOF - while (nextLine < endLine && !isEmpty(state, nextLine)) { - // Some tags can terminate paragraph without empty line. - // Try those tags in validation more (without tokens generation) - if (rules_named.fences(state, nextLine, endLine, true)) { break; } - if (rules_named.hr(state, nextLine, endLine, true)) { break; } - if (rules_named.heading(state, nextLine, endLine, true)) { break; } - if (rules_named.lheading(state, nextLine, endLine, true)) { break; } - if (rules_named.blockquote(state, nextLine, endLine, true)) { break; } - if (bullet_item(state, nextLine, endLine, true)) { break; } - //if (rules_named.tag(state, nextLine, endLine, true)) { break; } - //if (rules_named.def(state, nextLine, endLine, true)) { break; } - nextLine++; + while (pos < max) { + marker = state.src.charCodeAt(pos++); + + // found valid marker + if (marker === 0x29 || marker === 0x2e) { + haveMarker = true; + break; + } + + // still skipping digits... + if (marker < 0x30/* 0 */ || marker > 0x39/* 9 */) { return -1; } + } + + if (!haveMarker) { + // " 1\n" + return -1; + } + if (pos < max && state.src.charCodeAt(pos) !== 0x20) { + // " 1.test " - is not a list item + return -1; + } + return pos; +} + +function bullet_item(state, startLine, endLine, isOrdered) { + var indentAfterMarker, indent, start, lastLine, subState, pos, + max = state.eMarks[startLine]; + + if (isOrdered) { + pos = skipOrderedListMarker(state, startLine, endLine); + } else { + pos = skipBulletListMarker(state, startLine, endLine); + } + + if (pos === -1) { return false; } + + start = pos; + pos = skipSpaces(state, pos); + + if (pos >= max) { + // trimming space in "- \n 3" case, indent is 1 here + indentAfterMarker = 1; + } else { + indentAfterMarker = pos - start; } + // If we have more than 4 spaces, the indent is 1 + // (the rest is just indented code block) + if (indentAfterMarker > 4) { indentAfterMarker = 1; } + + // If indent is less than 1, assume that it's one, example: + // "-\n test" + if (indentAfterMarker < 1) { indentAfterMarker = 1; } + + // " - test" + // ^^^^^ - calculating total length of this thing + indent = state.tShift[startLine] + indentAfterMarker + (isOrdered ? 2 : 1); + + lastLine = findEndOfItem(state, startLine, endLine, indent); + state.tokens.push({ type: 'list_item_open' }); - state.lexerInline.tokenize( + /*state.lexerInline.tokenize( state, state.bMarks[startLine], - state.eMarks[nextLine - 1] - ); + state.eMarks[lastLine] + );*/ + subState = state.clone(getLines(state, startLine, lastLine + 1, true) + .replace(RegExp('^ {' + indent + '}', 'mg'), '').substr(indent)); + state.lexerBlock.tokenize(subState, 0, subState.lineMax); + state.tokens.push({ type: 'list_item_close' }); - state.line = skipEmptyLines(state, nextLine); + state.line = lastLine + 1; return true; } module.exports = function list(state, startLine, endLine, silent) { - // TODO: supporting list with only one element for now - if (bullet_item(state, startLine, endLine, true)) { - if (silent) { return true; } + var line, start, markerInt, + orderedMarker = skipOrderedListMarker(state, startLine, endLine), + bulletMarker = skipBulletListMarker(state, startLine, endLine), + isOrdered; + + if (orderedMarker === -1 && bulletMarker === -1) { return false; } + if (silent) { return true; } + + isOrdered = orderedMarker !== -1; + if (isOrdered) { + start = state.bMarks[startLine] + state.tShift[startLine]; + markerInt = Number(state.src.substr(start, orderedMarker - start)); + if (markerInt > 1) { + state.tokens.push({ type: 'ordered_list_open', start: markerInt }); + } else { + state.tokens.push({ type: 'ordered_list_open' }); + } + } else { state.tokens.push({ type: 'bullet_list_open' }); - bullet_item(state, startLine, endLine); + } + + line = startLine; + while (line < endLine) { + if (bullet_item(state, line, endLine, isOrdered)) { + line = state.line; + + // if we have a trailing newline, skip it; + // if we have more than one, it should end the list, + // so can't use skipEmptyNewlines here + if (line < endLine && isEmpty(state, line)) { line++; } + } else { + break; + } + } + + if (isOrdered) { + state.tokens.push({ type: 'ordered_list_close' }); + } else { state.tokens.push({ type: 'bullet_list_close' }); - return true; } - return false; + state.line = skipEmptyLines(state, state.line); + return true; }; diff --git a/lib/lexer_block/table.js b/lib/lexer_block/table.js new file mode 100644 index 0000000..93eeb2b --- /dev/null +++ b/lib/lexer_block/table.js @@ -0,0 +1,82 @@ +// GFM table, non-standard + +'use strict'; + + +var skipEmptyLines = require('../helpers').skipEmptyLines; + + +function lineMatch(state, line, reg) { + var pos = state.bMarks[line], + max = state.eMarks[line]; + + return state.src.substr(pos, max - pos).match(reg); +} + + +module.exports = function table(state, startLine, endLine, silent) { + var ch, firstLineMatch, secondLineMatch, i, subState, nextLine, m, rows, + aligns, t; + + // should have at least three lines + if (startLine + 2 > endLine) { return false; } + + // first character of the second line should be '|' or '-' + ch = state.src.charCodeAt(state.bMarks[startLine + 1] + + state.tShift[startLine + 1]); + if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */) { return false; } + + secondLineMatch = lineMatch(state, startLine + 1, + /^ *\|?(( *[:-]-+[:-] *\|)+( *[:-]-+[:-] *))\|? *$/); + if (!secondLineMatch) { return false; } + + rows = secondLineMatch[1].split('|'); + aligns = []; + for (i = 0; i < rows.length; i++) { + t = rows[i].trim(); + if (t[t.length - 1] === ':') { + aligns[i] = t[0] === ':' ? 'center' : 'right'; + } else if (t[0] === ':') { + aligns[i] = 'left'; + } else { + aligns[i] = ''; + } + } + + firstLineMatch = lineMatch(state, startLine, /^ *\|?(.*?\|.*?)\|? *$/); + if (!firstLineMatch) { return false; } + + rows = firstLineMatch[1].split('|'); + if (aligns.length !== rows.length) { return false; } + if (silent) { return true; } + + state.tokens.push({ type: 'table_open' }); + state.tokens.push({ type: 'tr_open' }); + + for (i = 0; i < rows.length; i++) { + state.tokens.push({ type: 'th_open', align: aligns[i] }); + subState = state.clone(rows[i].trim()); + state.lexerInline.tokenize(subState, 0, subState.eMarks[0]); + state.tokens.push({ type: 'th_close' }); + } + state.tokens.push({ type: 'tr_close' }); + + for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { + m = lineMatch(state, nextLine, /^ *\|?(.*?\|.*?)\|? *$/); + if (!m) { break; } + rows = m[1].split('|'); + + state.tokens.push({ type: 'tr_open' }); + for (i = 0; i < rows.length; i++) { + state.tokens.push({ type: 'td_open', align: aligns[i] }); + subState = state.clone(rows[i].replace(/^\|? *| *\|?$/g, '')); + state.lexerInline.tokenize(subState, 0, subState.eMarks[0]); + state.tokens.push({ type: 'td_close' }); + } + state.tokens.push({ type: 'tr_close' }); + } + state.tokens.push({ type: 'table_close' }); + + state.line = skipEmptyLines(state, nextLine); + return true; +}; diff --git a/lib/renderer.js b/lib/renderer.js index 666afa0..8cdfe61 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -75,6 +75,16 @@ rules.list_item_close = function (state /*, token*/) { }; +rules.ordered_list_open = function (state, token) { + state.result += '\n'; +}; +rules.ordered_list_close = function (state /*, token*/) { + state.result += '\n'; +}; + + rules.paragraph_open = function (state /*, token*/) { state.result += '

'; }; @@ -83,6 +93,36 @@ rules.paragraph_close = function (state /*, token*/) { }; +rules.table_open = function (state /*, token*/) { + state.result += '\n'; +}; +rules.table_close = function (state /*, token*/) { + state.result += '
\n'; +}; +rules.tr_open = function (state /*, token*/) { + state.result += '\n'; +}; +rules.tr_close = function (state /*, token*/) { + state.result += '\n'; +}; +rules.th_open = function (state, token) { + state.result += ''; +}; +rules.th_close = function (state /*, token*/) { + state.result += '\n'; +}; +rules.td_open = function (state, token) { + state.result += ''; +}; +rules.td_close = function (state /*, token*/) { + state.result += '\n'; +}; + + rules.text = function (state, token) { state.result += escapeHtml(unescapeMd(token.content)); };