fix(simpleAutoLinks): URLs with emphasis/strikethrough are parsed

correctly

When a user enters a URL with emphasis or strikethrough, the html output
were incorrect.
Now, URLs inside emphasis or strikethrough are parsed corerctly

Closes #347
This commit is contained in:
Estevao Soares dos Santos 2017-02-26 19:13:52 +00:00
parent 1ebc1959dd
commit 5c50675cca
25 changed files with 289 additions and 126 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

@ -1,29 +1,16 @@
showdown.subParser('autoLinks', function (text, options, globals) { // url allowed chars [a-z\d_.~:/?#[]@!$&'()*+,;=-]
'use strict';
text = globals.converter._dispatch('autoLinks.before', text, options, globals); var simpleURLRegex = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+)()(?=\s|$)(?!["<>])/gi,
var simpleURLRegex = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+)()(?=\s|$)(?!["<>])/gi,
simpleURLRegex2 = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?()]?)(?=\s|$)(?!["<>])/gi, simpleURLRegex2 = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?()]?)(?=\s|$)(?!["<>])/gi,
//simpleURLRegex3 = /\b(((https?|ftp):\/\/|www\.)[a-z\d.-]+\.[a-z\d_.~:/?#\[\]@!$&'()*+,;=-]+?)([.!?()]?)(?=\s|$)(?!["<>])/gi,
delimUrlRegex = /<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)>/gi, delimUrlRegex = /<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)>/gi,
simpleMailRegex = /(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gmi, simpleMailRegex = /(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gmi,
delimMailRegex = /<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi; delimMailRegex = /<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
text = text.replace(delimUrlRegex, replaceLink); replaceLink = function (options) {
text = text.replace(delimMailRegex, replaceMail); 'use strict';
// simpleURLRegex = /\b(((https?|ftp|dict):\/\/|www\.)[-.+~:?#@!$&'()*,;=[\]\w]+)\b/gi,
// Email addresses: <address@domain.foo>
if (options.simplifiedAutoLink) { return function (wm, link, m2, m3, trailingPunctuation) {
if (options.excludeTrailingPunctuationFromURLs) {
text = text.replace(simpleURLRegex2, replaceLink);
} else {
text = text.replace(simpleURLRegex, replaceLink);
}
text = text.replace(simpleMailRegex, replaceMail);
}
function replaceLink (wm, link, m2, m3, trailingPunctuation) {
var lnkTxt = link, var lnkTxt = link,
append = ''; append = '';
if (/^www\./i.test(link)) { if (/^www\./i.test(link)) {
@ -33,9 +20,12 @@ showdown.subParser('autoLinks', function (text, options, globals) {
append = trailingPunctuation; append = trailingPunctuation;
} }
return '<a href="' + link + '">' + lnkTxt + '</a>' + append; return '<a href="' + link + '">' + lnkTxt + '</a>' + append;
} };
},
function replaceMail (wholeMatch, b, mail) { replaceMail = function (options, globals) {
'use strict';
return function (wholeMatch, b, mail) {
var href = 'mailto:'; var href = 'mailto:';
b = b || ''; b = b || '';
mail = showdown.subParser('unescapeSpecialChars')(mail, options, globals); mail = showdown.subParser('unescapeSpecialChars')(mail, options, globals);
@ -46,9 +36,39 @@ showdown.subParser('autoLinks', function (text, options, globals) {
href = href + mail; href = href + mail;
} }
return b + '<a href="' + href + '">' + mail + '</a>'; return b + '<a href="' + href + '">' + mail + '</a>';
} };
};
showdown.subParser('autoLinks', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('autoLinks.before', text, options, globals);
text = text.replace(delimUrlRegex, replaceLink(options));
text = text.replace(delimMailRegex, replaceMail(options, globals));
text = globals.converter._dispatch('autoLinks.after', text, options, globals); text = globals.converter._dispatch('autoLinks.after', text, options, globals);
return text; return text;
}); });
showdown.subParser('simplifiedAutoLinks', function (text, options, globals) {
'use strict';
if (!options.simplifiedAutoLink) {
return text;
}
text = globals.converter._dispatch('simplifiedAutoLinks.before', text, options, globals);
if (options.excludeTrailingPunctuationFromURLs) {
text = text.replace(simpleURLRegex2, replaceLink(options));
} else {
text = text.replace(simpleURLRegex, replaceLink(options));
}
text = text.replace(simpleMailRegex, replaceMail(options, globals));
text = globals.converter._dispatch('simplifiedAutoLinks.after', text, options, globals);
return text;
});

View File

@ -7,34 +7,47 @@ showdown.subParser('italicsAndBold', function (text, options, globals) {
// because of backtracing, in some cases, it could lead to an exponential effect // because of backtracing, in some cases, it could lead to an exponential effect
// called "catastrophic backtrace". Ominous! // called "catastrophic backtrace". Ominous!
function parseInside (txt, left, right) {
if (options.simplifiedAutoLink) {
txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals);
}
return left + txt + right;
}
// Parse underscores // Parse underscores
if (options.literalMidWordUnderscores) { if (options.literalMidWordUnderscores) {
text = text.replace(/\b___(\S[\s\S]*)___\b/g, '<strong><em>$1</em></strong>'); text = text.replace(/\b___(\S[\s\S]*)___\b/g, function (wm, txt) {
text = text.replace(/\b__(\S[\s\S]*)__\b/g, '<strong>$1</strong>'); return parseInside (txt, '<strong><em>', '</em></strong>');
text = text.replace(/\b_(\S[\s\S]*?)_\b/g, '<em>$1</em>'); });
text = text.replace(/\b__(\S[\s\S]*)__\b/g, function (wm, txt) {
return parseInside (txt, '<strong>', '</strong>');
});
text = text.replace(/\b_(\S[\s\S]*?)_\b/g, function (wm, txt) {
return parseInside (txt, '<em>', '</em>');
});
} else { } else {
text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) {
return (/\S$/.test(m)) ? '<strong><em>' + m + '</em></strong>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<strong><em>', '</em></strong>') : wm;
}); });
text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) {
return (/\S$/.test(m)) ? '<strong>' + m + '</strong>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<strong>', '</strong>') : wm;
}); });
text = text.replace(/_([^\s_][\s\S]*?)_/g, function (wm, m) { text = text.replace(/_([^\s_][\s\S]*?)_/g, function (wm, m) {
// !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it) // !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it)
return (/\S$/.test(m)) ? '<em>' + m + '</em>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<em>', '</em>') : wm;
}); });
} }
// Now parse asterisks // Now parse asterisks
text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, function (wm, m) { text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, function (wm, m) {
return (/\S$/.test(m)) ? '<strong><em>' + m + '</em></strong>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<strong><em>', '</em></strong>') : wm;
}); });
text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, function (wm, m) { text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, function (wm, m) {
return (/\S$/.test(m)) ? '<strong>' + m + '</strong>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<strong>', '</strong>') : wm;
}); });
text = text.replace(/\*([^\s*][\s\S]*?)\*/g, function (wm, m) { text = text.replace(/\*([^\s*][\s\S]*?)\*/g, function (wm, m) {
// !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it) // !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it)
return (/\S$/.test(m)) ? '<em>' + m + '</em>' : wm; return (/\S$/.test(m)) ? parseInside (m, '<em>', '</em>') : wm;
}); });
text = globals.converter._dispatch('italicsAndBold.after', text, options, globals); text = globals.converter._dispatch('italicsAndBold.after', text, options, globals);

View File

@ -16,11 +16,12 @@ showdown.subParser('spanGamut', function (text, options, globals) {
text = showdown.subParser('anchors')(text, options, globals); text = showdown.subParser('anchors')(text, options, globals);
// Make links out of things like `<http://example.com/>` // Make links out of things like `<http://example.com/>`
// Must come after _DoAnchors(), because you can use < and > // Must come after anchors, because you can use < and >
// delimiters in inline links like [this](<url>). // delimiters in inline links like [this](<url>).
text = showdown.subParser('autoLinks')(text, options, globals); text = showdown.subParser('autoLinks')(text, options, globals);
text = showdown.subParser('italicsAndBold')(text, options, globals); text = showdown.subParser('italicsAndBold')(text, options, globals);
text = showdown.subParser('strikethrough')(text, options, globals); text = showdown.subParser('strikethrough')(text, options, globals);
text = showdown.subParser('simplifiedAutoLinks')(text, options, globals);
// we need to hash HTML tags inside spans // we need to hash HTML tags inside spans
text = showdown.subParser('hashHTMLSpans')(text, options, globals); text = showdown.subParser('hashHTMLSpans')(text, options, globals);

View File

@ -1,9 +1,16 @@
showdown.subParser('strikethrough', function (text, options, globals) { showdown.subParser('strikethrough', function (text, options, globals) {
'use strict'; 'use strict';
function parseInside (txt) {
if (options.simplifiedAutoLink) {
txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals);
}
return '<del>' + txt + '</del>';
}
if (options.strikethrough) { if (options.strikethrough) {
text = globals.converter._dispatch('strikethrough.before', text, options, globals); text = globals.converter._dispatch('strikethrough.before', text, options, globals);
text = text.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g, '<del>$1</del>'); text = text.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g, function (wm, txt) { return parseInside(txt); });
text = globals.converter._dispatch('strikethrough.after', text, options, globals); text = globals.converter._dispatch('strikethrough.after', text, options, globals);
} }

View File

@ -0,0 +1,3 @@
<blockquote>
<p><a href="http://www.google.com">http://www.google.com</a></p>
</blockquote>

View File

@ -0,0 +1 @@
> http://www.google.com

View File

@ -0,0 +1 @@
<p><a href="http://www.google.com">www.google.com</a></p>

View File

@ -0,0 +1 @@
<a href="http://www.google.com">www.google.com</a>

View File

@ -0,0 +1,6 @@
<pre><code>some code with
a link
www.google.com
and another link http://www.google.com
</code></pre>

View File

@ -0,0 +1,5 @@
some code with
a link
www.google.com
and another link http://www.google.com

View File

@ -0,0 +1,7 @@
<p><em><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></em></p>
<p><strong><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></strong></p>
<p><strong><em><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></em></strong></p>
<p><del><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></del></p>
<p><em><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></em></p>
<p><strong><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></strong></p>
<p><strong><em><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></em></strong></p>

View File

@ -0,0 +1,13 @@
*http://www.google.com/foobar*
**http://www.google.com/foobar**
***http://www.google.com/foobar***
~~http://www.google.com/foobar~~
_http://www.google.com/foobar_
__http://www.google.com/foobar__
___http://www.google.com/foobar___

View File

@ -0,0 +1,11 @@
<ol>
<li><a href="http://www.google.com/listitem1">http://www.google.com/listitem1</a></li>
<li><a href="http://www.google.com/listitem2">http://www.google.com/listitem2</a></li>
<li><a href="http://www.google.com/listitem3">http://www.google.com/listitem3</a></li>
</ol>
<p>foo</p>
<ol>
<li><p><a href="http://www.google.com/listitem1">http://www.google.com/listitem1</a></p></li>
<li><p><a href="http://www.google.com/listitem2">http://www.google.com/listitem2</a></p></li>
<li><p><a href="http://www.google.com/listitem3">http://www.google.com/listitem3</a></p></li>
</ol>

View File

@ -0,0 +1,11 @@
1. http://www.google.com/listitem1
2. http://www.google.com/listitem2
3. http://www.google.com/listitem3
foo
1. http://www.google.com/listitem1
2. http://www.google.com/listitem2
3. http://www.google.com/listitem3

View File

@ -0,0 +1,6 @@
<p><a href="http://www.google.com/foobar">http://www.google.com/foobar</a></p>
<p><a href="http://www.google.com/foobar">www.google.com/foobar</a></p>
<p><a href="ftp://user:password@host.com:port/path">ftp://user:password@host.com:port/path</a></p>
<p>this has some <a href="http://www.google.com/foobar">http://www.google.com/foobar</a> in text</p>
<p>this has some <a href="http://www.google.com/foobar">www.google.com/foobar</a> in text</p>
<p>this has some <a href="ftp://user:password@host.com:port/path">ftp://user:password@host.com:port/path</a> in text</p>

View File

@ -0,0 +1,13 @@
http://www.google.com/foobar
www.google.com/foobar
ftp://user:password@host.com:port/path
this has some http://www.google.com/foobar in text
this has some www.google.com/foobar in text
this has some ftp://user:password@host.com:port/path in text

View File

@ -0,0 +1,11 @@
<ul>
<li><a href="http://www.google.com/foo">http://www.google.com/foo</a></li>
<li><a href="http://www.google.com/bar">http://www.google.com/bar</a></li>
<li><a href="http://www.google.com/baz">http://www.google.com/baz</a></li>
</ul>
<p>a</p>
<ul>
<li><p><a href="http://www.google.com/foo">http://www.google.com/foo</a></p></li>
<li><p><a href="http://www.google.com/bar">http://www.google.com/bar</a></p></li>
<li><p><a href="http://www.google.com/baz">http://www.google.com/baz</a></p></li>
</ul>

View File

@ -0,0 +1,11 @@
- http://www.google.com/foo
- http://www.google.com/bar
- http://www.google.com/baz
a
- http://www.google.com/foo
- http://www.google.com/bar
- http://www.google.com/baz

View File

@ -5,10 +5,13 @@ var bootstrap = require('../bootstrap.js'),
showdown = bootstrap.showdown, showdown = bootstrap.showdown,
assertion = bootstrap.assertion, assertion = bootstrap.assertion,
testsuite = bootstrap.getTestSuite('test/features/'), testsuite = bootstrap.getTestSuite('test/features/'),
tableSuite = bootstrap.getTestSuite('test/features/tables/'); tableSuite = bootstrap.getTestSuite('test/features/tables/'),
simplifiedAutoLinkSuite = bootstrap.getTestSuite('test/features/simplifiedAutoLink/');
describe('makeHtml() features testsuite', function () { describe('makeHtml() features testsuite', function () {
'use strict'; 'use strict';
describe('issues', function () {
for (var i = 0; i < testsuite.length; ++i) { for (var i = 0; i < testsuite.length; ++i) {
var converter; var converter;
if (testsuite[i].name === '#143.support-image-dimensions') { if (testsuite[i].name === '#143.support-image-dimensions') {
@ -25,8 +28,6 @@ describe('makeHtml() features testsuite', function () {
converter = new showdown.Converter({ghCodeBlocks: false}); converter = new showdown.Converter({ghCodeBlocks: false});
} else if (testsuite[i].name === '#164.4.tasklists') { } else if (testsuite[i].name === '#164.4.tasklists') {
converter = new showdown.Converter({tasklists: true}); converter = new showdown.Converter({tasklists: true});
} else if (testsuite[i].name === 'autolink-and-disallow-underscores') {
converter = new showdown.Converter({literalMidWordUnderscores: true, simplifiedAutoLink: true});
} else if (testsuite[i].name === '#198.literalMidWordUnderscores-changes-behavior-of-asterisk') { } else if (testsuite[i].name === '#198.literalMidWordUnderscores-changes-behavior-of-asterisk') {
converter = new showdown.Converter({literalMidWordUnderscores: true}); converter = new showdown.Converter({literalMidWordUnderscores: true});
} else if (testsuite[i].name === '#259.es6-template-strings-indentation-issues') { } else if (testsuite[i].name === '#259.es6-template-strings-indentation-issues') {
@ -69,23 +70,44 @@ describe('makeHtml() features testsuite', function () {
converter = new showdown.Converter({prefixHeaderId: 'my-prefix-', ghCompatibleHeaderId: true}); converter = new showdown.Converter({prefixHeaderId: 'my-prefix-', ghCompatibleHeaderId: true});
} else if (testsuite[i].name === 'prefixHeaderId-string-and-ghCompatibleHeaderId2') { } else if (testsuite[i].name === 'prefixHeaderId-string-and-ghCompatibleHeaderId2') {
converter = new showdown.Converter({prefixHeaderId: 'my prefix ', ghCompatibleHeaderId: true}); converter = new showdown.Converter({prefixHeaderId: 'my prefix ', ghCompatibleHeaderId: true});
} else if (testsuite[i].name === 'simplifiedAutoLink') {
converter = new showdown.Converter({simplifiedAutoLink: true, strikethrough: true});
} else { } else {
converter = new showdown.Converter(); converter = new showdown.Converter();
} }
it(testsuite[i].name.replace(/-/g, ' '), assertion(testsuite[i], converter)); it(testsuite[i].name.replace(/-/g, ' '), assertion(testsuite[i], converter));
} }
});
// test Table Syntax Support
describe('table support', function () { describe('table support', function () {
var converter; var converter,
for (var i = 0; i < tableSuite.length; ++i) { suite = tableSuite;
if (tableSuite[i].name === 'basic-with-header-ids') { for (var i = 0; i < suite.length; ++i) {
if (suite[i].name === 'basic-with-header-ids') {
converter = new showdown.Converter({tables: true, tableHeaderId: true}); converter = new showdown.Converter({tables: true, tableHeaderId: true});
} else if (tableSuite[i].name === '#179.parse-md-in-table-ths') { } else if (suite[i].name === '#179.parse-md-in-table-ths') {
converter = new showdown.Converter({tables: true, strikethrough: true}); converter = new showdown.Converter({tables: true, strikethrough: true});
} else { } else {
converter = new showdown.Converter({tables: true}); converter = new showdown.Converter({tables: true});
} }
it(tableSuite[i].name.replace(/-/g, ' '), assertion(tableSuite[i], converter)); it(suite[i].name.replace(/-/g, ' '), assertion(suite[i], converter));
}
});
// test simplifiedAutoLink Support
describe('simplifiedAutoLink support in', function () {
var converter,
suite = simplifiedAutoLinkSuite;
for (var i = 0; i < suite.length; ++i) {
if (suite[i].name === 'emphasis-and-strikethrough') {
converter = new showdown.Converter({simplifiedAutoLink: true, strikethrough: true});
} else if (suite[i].name === 'disallow-underscores') {
converter = new showdown.Converter({literalMidWordUnderscores: true, simplifiedAutoLink: true});
} else {
converter = new showdown.Converter({simplifiedAutoLink: true});
}
it(suite[i].name.replace(/-/g, ' '), assertion(suite[i], converter));
} }
}); });