fix(lists): enforce 4 space indentation in sublists

Acording to the spec, multi paragraph (or block) list item requires subblocks
to be indented 4 spaces (or 1 tab). Although, this is mentioned in the documentation,
Showdown didn't enforce this rule in sublists because other implementations,
such as GFM also didn't. However, in some edge cases, this led to inconsistent behavior,
as shown in issue #299. This commit makes 4 space indentation in sublists
mandatory.

BREAKING CHANGE: syntax for sublists is more restrictive. Before, sublists SHOULD be
indented by 4 spaces, but indenting 2 spaces would work. Now, sublists MUST be
indented 4 spaces or they won't work.

With this input:
```md
* one
  * two
    * three
```

Before (ouput):
```html
<ul>
  <li>one
    <ul>
      <li>two
        <ul><li>three</li></ul>
      <li>
    </ul>
  </li>
<ul>
```

After (output):
```html
<ul>
  <li>one</li>
  <li>two
    <ul><li>three</li></ul>
  </li>
</ul>
```

To migrate either fix source md files or activate the option `disableForced4SpacesIndentedSublists` (coming in v1.5.0):

```md
showdown.setOption('disableForced4SpacesIndentedSublists', true);
```
This commit is contained in:
Estevao Soares dos Santos 2016-11-11 07:56:29 +00:00
parent 9cfe8b1412
commit d51be6e0b4
13 changed files with 211 additions and 36 deletions

BIN
dist/showdown.js vendored

Binary file not shown.

BIN
dist/showdown.js.map vendored

Binary file not shown.

BIN
dist/showdown.min.js vendored

Binary file not shown.

Binary file not shown.

View File

@ -41,11 +41,12 @@ showdown.subParser('lists', function (text, options, globals) {
// attacklab: add sentinel to emulate \z // attacklab: add sentinel to emulate \z
listStr += '~0'; listStr += '~0';
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, var rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(~0| {0,3}([*+-]|\d+[.])[ \t]+))/gm,
isParagraphed = (/\n[ \t]*\n(?!~0)/.test(listStr)); isParagraphed = (/\n[ \t]*\n(?!~0)/.test(listStr));
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) { listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) {
checked = (checked && checked.trim() !== ''); checked = (checked && checked.trim() !== '');
var item = showdown.subParser('outdent')(m4, options, globals), var item = showdown.subParser('outdent')(m4, options, globals),
bulletStyle = ''; bulletStyle = '';
@ -61,6 +62,7 @@ showdown.subParser('lists', function (text, options, globals) {
return otp; return otp;
}); });
} }
// m1 - Leading line or // m1 - Leading line or
// Has a double return (multi paragraph) or // Has a double return (multi paragraph) or
// Has sublist // Has sublist
@ -103,7 +105,9 @@ showdown.subParser('lists', function (text, options, globals) {
function parseConsecutiveLists(list, listType, trimTrailing) { function parseConsecutiveLists(list, listType, trimTrailing) {
// check if we caught 2 or more consecutive lists by mistake // check if we caught 2 or more consecutive lists by mistake
// we use the counterRgx, meaning if listType is UL we look for OL and vice versa // we use the counterRgx, meaning if listType is UL we look for OL and vice versa
var counterRxg = (listType === 'ul') ? /^\d+\.[ \t]/gm : /^[*+-][ \t]/gm, var olRgx = /^ {0,3}\d+\.[ \t]/gm,
ulRgx = /^ {0,3}[*+-][ \t]/gm,
counterRxg = (listType === 'ul') ? olRgx : ulRgx,
result = ''; result = '';
if (list.search(counterRxg) !== -1) { if (list.search(counterRxg) !== -1) {
@ -115,7 +119,7 @@ showdown.subParser('lists', function (text, options, globals) {
// invert counterType and listType // invert counterType and listType
listType = (listType === 'ul') ? 'ol' : 'ul'; listType = (listType === 'ul') ? 'ol' : 'ul';
counterRxg = (listType === 'ul') ? /^ {0,2}\d+\.[ \t]/gm : /^ {0,2}[*+-][ \t]/gm; counterRxg = (listType === 'ul') ? olRgx : ulRgx;
//recurse //recurse
parseCL(txt.slice(pos)); parseCL(txt.slice(pos));
@ -134,21 +138,20 @@ showdown.subParser('lists', function (text, options, globals) {
// http://bugs.webkit.org/show_bug.cgi?id=11231 // http://bugs.webkit.org/show_bug.cgi?id=11231
text += '~0'; text += '~0';
// Re-usable pattern to match any entire ul or ol list:
var wholeList = /^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
if (globals.gListLevel) { if (globals.gListLevel) {
text = text.replace(wholeList, function (wholeMatch, list, m2) { text = text.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,
function (wholeMatch, list, m2) {
var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return parseConsecutiveLists(list, listType, true); return parseConsecutiveLists(list, listType, true);
}); }
);
} else { } else {
wholeList = /(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; text = text.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,
text = text.replace(wholeList, function (wholeMatch, m1, list, m3) { function (wholeMatch, m1, list, m3) {
var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return parseConsecutiveLists(list, listType, false); return parseConsecutiveLists(list, listType, false);
}); }
);
} }
// strip sentinel // strip sentinel

View File

@ -0,0 +1,54 @@
<ul>
<li>one</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one
<ol>
<li>two</li></ol></li>
</ul>
<p>foo</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>foo</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>foo</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>foo</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<p>foo</p>
<ul>
<li>one
<ul>
<li>two</li></ul></li>
</ul>

View File

@ -0,0 +1,42 @@
* one
1. two
foo
* one
1. two
foo
* one
1. two
foo
* one
1. two
foo
* one
* two
foo
* one
* two
foo
* one
* two
foo
* one
* two
foo
* one
* two

View File

@ -0,0 +1,15 @@
<ul>
<li>one long paragraph of
text</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one long paragraph of
text</li>
</ul>
<ol>
<li>two</li>
</ol>

View File

@ -0,0 +1,9 @@
* one long paragraph of
text
1. two
foo
* one long paragraph of
text
1. two

View File

@ -6,15 +6,42 @@
</ol> </ol>
<p>foo</p> <p>foo</p>
<ul> <ul>
<li>one <li>one</li>
</ul>
<ol> <ol>
<li>two</li></ol></li> <li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>one</li>
</ul>
<ol>
<li>two</li>
</ol>
<p>foo</p>
<ul>
<li>uli one</li>
<li>uli two</li>
</ul> </ul>
<p>foo</p> <p>foo</p>
<ul> <ul>
<li>one <li>uli one</li>
<li>uli two</li>
<ul> </ul>
<li>two</li></ul></li> <p>foo</p>
<ul>
<li>uli one</li>
<li>uli two</li>
</ul>
<p>foo</p>
<ul>
<li>uli one</li>
<li>uli two</li>
</ul> </ul>

View File

@ -9,4 +9,29 @@ foo
foo foo
* one * one
* two 1. two
foo
* one
1. two
foo
* uli one
* uli two
foo
* uli one
* uli two
foo
* uli one
* uli two
foo
* uli one
* uli two