Skip to content

Commit

Permalink
feat(tasklists): add support for GFM tasklists
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tivie committed Jul 11, 2015
1 parent c33f988 commit dc72403
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 83 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.DS_Store
node_modules
npm-debug.log
localtest.html
68 changes: 29 additions & 39 deletions dist/showdown.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/showdown.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/showdown.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/showdown.min.js.map

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/showdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 27 additions & 38 deletions src/subParsers/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4) {
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() !== '');

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)
Expand All @@ -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
Expand All @@ -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 = '',
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions test/features/#164.4.tasklists.html
Original file line number Diff line number Diff line change
@@ -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>
8 changes: 8 additions & 0 deletions test/features/#164.4.tasklists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# my things

- foo
- [] bar
- [ ] baz
- [x] bazinga

otherthings
3 changes: 2 additions & 1 deletion test/node/showdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
4 changes: 3 additions & 1 deletion test/node/testsuite.features.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down

0 comments on commit dc72403

Please sign in to comment.