feat(tasklists): add support for GFM tasklists

Github Flavored Markdown supports tasklist by `[x]` or `[ ]` after list item marker.
This commit adds this feature to showdown through an option called "tasklists".

Related to #164
This commit is contained in:
Estevão Soares dos Santos 2015-07-11 23:02:02 +01:00
parent c33f98884b
commit dc72403acc
11 changed files with 83 additions and 81 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
.DS_Store
node_modules
npm-debug.log
localtest.html

66
dist/showdown.js vendored
View File

@ -19,7 +19,8 @@ var showdown = {},
strikethrough: false,
tables: false,
tablesHeaderId: false,
ghCodeBlocks: true // true due to historical reasons
ghCodeBlocks: true, // true due to historical reasons
tasklists: false
},
globalOptions = JSON.parse(JSON.stringify(defaultOptions)); //clone default options out of laziness =P
@ -1667,25 +1668,28 @@ showdown.subParser('lists', function (text, options, globals) {
// attacklab: add sentinel to emulate \z
listStr += '~0';
/*
list_str = list_str.replace(/
(\n)? // leading line = $1
(^[ \t]*) // leading whitespace = $2
([*+-]|\d+[.]) [ \t]+ // list marker = $3
([^\r]+? // list item text = $4
(\n{1,2}))
(?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+))
/gm, function(){...});
*/
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm;
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+((\[(x| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm;
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) {
checked = (checked && checked.trim() !== '');
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4) {
var item = showdown.subParser('outdent')(m4, options, globals);
//m1 - LeadingLine
//m1 - LeadingLine
if (m1 || (item.search(/\n{2,}/) > -1)) {
item = showdown.subParser('blockGamut')(item, options, globals);
} else {
if (taskbtn && options.tasklists) {
item = item.replace(taskbtn, function () {
var otp = '<input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"';
if (checked) {
otp += ' checked';
}
otp += '>';
return otp;
});
}
// Recursion for sub-lists:
item = showdown.subParser('lists')(item, options, globals);
item = item.replace(/\n$/, ''); // chomp(item)
@ -1694,8 +1698,14 @@ showdown.subParser('lists', function (text, options, globals) {
// this is a "hack" to differentiate between ordered and unordered lists
// related to issue #142
var tp = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return spl + tp + '<li>' + item + '</li>\n';
var tp = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol',
bulletStyle = '';
if (taskbtn) {
bulletStyle = ' style="list-style-type: none;"';
}
return spl + tp + '<li' + bulletStyle + '>' + item + '</li>\n';
});
// attacklab: strip sentinel
@ -1712,6 +1722,8 @@ showdown.subParser('lists', function (text, options, globals) {
* @returns {string|*}
*/
function splitConsecutiveLists (results, listType) {
// parsing html with regex...
// This will surely fail if some extension decides to change paragraph markup directly
var cthulhu = /(<p[^>]+?>|<p>|<\/p>)/img,
holder = [[]],
res = '',
@ -1750,28 +1762,6 @@ showdown.subParser('lists', function (text, options, globals) {
text += '~0';
// Re-usable pattern to match any entire ul or ol list:
/*
var whole_list = /
( // $1 = whole list
( // $2
[ ]{0,3} // attacklab: g_tab_width - 1
([*+-]|\d+[.]) // $3 = first list item marker
[ \t]+
)
[^\r]+?
( // $4
~0 // sentinel for workaround; should be $
|
\n{2,}
(?=\S)
(?! // Negative lookahead for another list item marker
[ \t]*
(?:[*+-]|\d+[.])[ \t]+
)
)
)/g
*/
var wholeList = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
if (globals.gListLevel) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,8 @@ var showdown = {},
strikethrough: false,
tables: false,
tablesHeaderId: false,
ghCodeBlocks: true // true due to historical reasons
ghCodeBlocks: true, // true due to historical reasons
tasklists: false
},
globalOptions = JSON.parse(JSON.stringify(defaultOptions)); //clone default options out of laziness =P

View File

@ -42,25 +42,28 @@ showdown.subParser('lists', function (text, options, globals) {
// attacklab: add sentinel to emulate \z
listStr += '~0';
/*
list_str = list_str.replace(/
(\n)? // leading line = $1
(^[ \t]*) // leading whitespace = $2
([*+-]|\d+[.]) [ \t]+ // list marker = $3
([^\r]+? // list item text = $4
(\n{1,2}))
(?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+))
/gm, function(){...});
*/
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm;
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+((\[(x| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm;
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) {
checked = (checked && checked.trim() !== '');
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4) {
var item = showdown.subParser('outdent')(m4, options, globals);
//m1 - LeadingLine
//m1 - LeadingLine
if (m1 || (item.search(/\n{2,}/) > -1)) {
item = showdown.subParser('blockGamut')(item, options, globals);
} else {
if (taskbtn && options.tasklists) {
item = item.replace(taskbtn, function () {
var otp = '<input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"';
if (checked) {
otp += ' checked';
}
otp += '>';
return otp;
});
}
// Recursion for sub-lists:
item = showdown.subParser('lists')(item, options, globals);
item = item.replace(/\n$/, ''); // chomp(item)
@ -69,8 +72,14 @@ showdown.subParser('lists', function (text, options, globals) {
// this is a "hack" to differentiate between ordered and unordered lists
// related to issue #142
var tp = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return spl + tp + '<li>' + item + '</li>\n';
var tp = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol',
bulletStyle = '';
if (taskbtn) {
bulletStyle = ' style="list-style-type: none;"';
}
return spl + tp + '<li' + bulletStyle + '>' + item + '</li>\n';
});
// attacklab: strip sentinel
@ -87,6 +96,8 @@ showdown.subParser('lists', function (text, options, globals) {
* @returns {string|*}
*/
function splitConsecutiveLists (results, listType) {
// parsing html with regex...
// This will surely fail if some extension decides to change paragraph markup directly
var cthulhu = /(<p[^>]+?>|<p>|<\/p>)/img,
holder = [[]],
res = '',
@ -125,28 +136,6 @@ showdown.subParser('lists', function (text, options, globals) {
text += '~0';
// Re-usable pattern to match any entire ul or ol list:
/*
var whole_list = /
( // $1 = whole list
( // $2
[ ]{0,3} // attacklab: g_tab_width - 1
([*+-]|\d+[.]) // $3 = first list item marker
[ \t]+
)
[^\r]+?
( // $4
~0 // sentinel for workaround; should be $
|
\n{2,}
(?=\S)
(?! // Negative lookahead for another list item marker
[ \t]*
(?:[*+-]|\d+[.])[ \t]+
)
)
)/g
*/
var wholeList = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
if (globals.gListLevel) {

View File

@ -0,0 +1,10 @@
<h1 id="mythings">my things</h1>
<ul>
<li>foo</li>
<li style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> bar</li>
<li style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> baz</li>
<li style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;" checked> bazinga</li>
</ul>
<p>otherthings</p>

View File

@ -0,0 +1,8 @@
# my things
- foo
- [] bar
- [ ] baz
- [x] bazinga
otherthings

View File

@ -28,7 +28,8 @@ describe('showdown.options', function () {
strikethrough: false,
tables: false,
tablesHeaderId: false,
ghCodeBlocks: true
ghCodeBlocks: true,
tasklists: false
};
expect(showdown.getDefaultOptions()).to.be.eql(opts);
});

View File

@ -21,8 +21,10 @@ describe('makeHtml() features testsuite', function () {
converter = new showdown.Converter({literalMidWordUnderscores: true});
} else if (testsuite[i].name === '#164.3.strikethrough') {
converter = new showdown.Converter({strikethrough: true});
} else if (testsuite[i].name === 'disable_gh_codeblocks') {
} else if (testsuite[i].name === 'disable_gh_codeblocks') {
converter = new showdown.Converter({ghCodeBlocks: false});
} else if (testsuite[i].name === '#164.4.tasklists') {
converter = new showdown.Converter({tasklists: true});
} else {
converter = new showdown.Converter();
}