feat(relativePathBaseUrl): Add support for prepending a base URL

This feature enables support for prepending a base URL to relative paths in
links and images when converting Markdown to HTML.

Closes #536
This commit is contained in:
Sam Harrison 2019-03-14 11:56:33 -05:00 committed by SyntaxRules
parent 9c362e832e
commit e3a5b5928f
8 changed files with 94 additions and 11 deletions

View File

@ -400,6 +400,34 @@ showdown.helper._hashHTMLSpan = function (html, globals) {
return '¨C' + (globals.gHtmlSpans.push(html) - 1) + 'C'; return '¨C' + (globals.gHtmlSpans.push(html) - 1) + 'C';
}; };
/**
* Prepends a base URL to relative paths.
*
* @param {string} baseUrl the base URL to prepend to a relative path
* @param {string} url the path to modify, which may be relative
* @returns {string} the full URL
*/
showdown.helper.applyBaseUrl = function (baseUrl, url) {
// Only prepend if given a base URL and the path is not absolute.
if (baseUrl && !this.isAbsolutePath(url)) {
var urlResolve = require('url').resolve;
url = urlResolve(baseUrl, url);
}
return url;
};
/**
* Checks if the given path is absolute.
*
* @param {string} path the path to test for absolution
* @returns {boolean} `true` if the given path is absolute, else `false`
*/
showdown.helper.isAbsolutePath = function (path) {
// Absolute paths begin with '[protocol:]//' or '#' (anchors)
return /(^([a-z]+:)?\/\/)|(^#)/i.test(path);
};
/** /**
* Showdown's Event Object * Showdown's Event Object
* @param {string} name Name of the event * @param {string} name Name of the event
@ -476,7 +504,7 @@ showdown.helper.Event = function (name, text, params) {
/** /**
* POLYFILLS * POLYFILLS
*/ */
// use this instead of builtin is undefined for IE8 compatibility // use this instead if builtin is undefined for IE8 compatibility
if (typeof(console) === 'undefined') { if (typeof(console) === 'undefined') {
console = { console = {
warn: function (msg) { warn: function (msg) {

View File

@ -160,7 +160,12 @@ function getDefaultOpts (simple) {
defaultValue: false, defaultValue: false,
description: 'Split adjacent blockquote blocks', description: 'Split adjacent blockquote blocks',
type: 'boolean' type: 'boolean'
} },
relativePathBaseUrl: {
defaultValue: false,
describe: 'Prepends a base URL to relative paths',
type: 'string'
},
}; };
if (simple === false) { if (simple === false) {
return JSON.parse(JSON.stringify(defaultOptions)); return JSON.parse(JSON.stringify(defaultOptions));

View File

@ -17,6 +17,12 @@ showdown.subParser('makehtml.images', function (text, options, globals) {
return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title); return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title);
} }
function writeImageTagBaseUrl (wholeMatch, altText, linkId, url, width, height, m5, title) {
url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url);
return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title);
}
function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) { function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) {
var gUrls = globals.gUrls, var gUrls = globals.gUrls,
@ -91,10 +97,10 @@ showdown.subParser('makehtml.images', function (text, options, globals) {
text = text.replace(base64RegExp, writeImageTagBase64); text = text.replace(base64RegExp, writeImageTagBase64);
// cases with crazy urls like ./image/cat1).png // cases with crazy urls like ./image/cat1).png
text = text.replace(crazyRegExp, writeImageTag); text = text.replace(crazyRegExp, writeImageTagBaseUrl);
// normal cases // normal cases
text = text.replace(inlineRegExp, writeImageTag); text = text.replace(inlineRegExp, writeImageTagBaseUrl);
// handle reference-style shortcuts: ![img text] // handle reference-style shortcuts: ![img text]
text = text.replace(refShortcutRegExp, writeImageTag); text = text.replace(refShortcutRegExp, writeImageTag);

View File

@ -23,7 +23,7 @@
* @param {{}} globals * @param {{}} globals
* @returns {Function} * @returns {Function}
*/ */
function replaceAnchorTag (rgx, evtRootName, options, globals, emptyCase) { function replaceAnchorTagReference (rgx, evtRootName, options, globals, emptyCase) {
emptyCase = !!emptyCase; emptyCase = !!emptyCase;
return function (wholeMatch, text, id, url, m5, m6, title) { return function (wholeMatch, text, id, url, m5, m6, title) {
// bail we we find 2 newlines somewhere // bail we we find 2 newlines somewhere
@ -36,6 +36,15 @@
}; };
} }
function replaceAnchorTagBaseUrl (rgx, evtRootName, options, globals, emptyCase) {
return function (wholeMatch, text, id, url, m5, m6, title) {
url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url);
var evt = createEvent(rgx, evtRootName + '.captureStart', wholeMatch, text, id, url, title, options, globals);
return writeAnchorTag(evt, options, globals, emptyCase);
};
}
/** /**
* TODO Normalize this * TODO Normalize this
* Helper function: Create a capture event * Helper function: Create a capture event
@ -192,21 +201,21 @@
// 1. Look for empty cases: []() and [empty]() and []("title") // 1. Look for empty cases: []() and [empty]() and []("title")
var rgxEmpty = /\[(.*?)]()()()()\(<? ?>? ?(?:["'](.*)["'])?\)/g; var rgxEmpty = /\[(.*?)]()()()()\(<? ?>? ?(?:["'](.*)["'])?\)/g;
text = text.replace(rgxEmpty, replaceAnchorTag(rgxEmpty, evtRootName, options, globals, true)); text = text.replace(rgxEmpty, replaceAnchorTagBaseUrl(rgxEmpty, evtRootName, options, globals, true));
// 2. Look for cases with crazy urls like ./image/cat1).png // 2. Look for cases with crazy urls like ./image/cat1).png
var rgxCrazy = /\[((?:\[[^\]]*]|[^\[\]])*)]()\s?\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g; var rgxCrazy = /\[((?:\[[^\]]*]|[^\[\]])*)]()\s?\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g;
text = text.replace(rgxCrazy, replaceAnchorTag(rgxCrazy, evtRootName, options, globals)); text = text.replace(rgxCrazy, replaceAnchorTagBaseUrl(rgxCrazy, evtRootName, options, globals));
// 3. inline links with no title or titles wrapped in ' or ": // 3. inline links with no title or titles wrapped in ' or ":
// [text](url.com) || [text](<url.com>) || [text](url.com "title") || [text](<url.com> "title") // [text](url.com) || [text](<url.com>) || [text](url.com "title") || [text](<url.com> "title")
//var rgx2 = /\[[ ]*[\s]?[ ]*([^\n\[\]]*?)[ ]*[\s]?[ ]*] ?()\(<?[ ]*[\s]?[ ]*([^\s'"]*)>?(?:[ ]*[\n]?[ ]*()(['"])(.*?)\5)?[ ]*[\s]?[ ]*\)/; // this regex is too slow!!! //var rgx2 = /\[[ ]*[\s]?[ ]*([^\n\[\]]*?)[ ]*[\s]?[ ]*] ?()\(<?[ ]*[\s]?[ ]*([^\s'"]*)>?(?:[ ]*[\n]?[ ]*()(['"])(.*?)\5)?[ ]*[\s]?[ ]*\)/; // this regex is too slow!!!
var rgx2 = /\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s*(?:()(['"])(.*?)\5)? *\)/g; var rgx2 = /\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s*(?:()(['"])(.*?)\5)? *\)/g;
text = text.replace(rgx2, replaceAnchorTag(rgx2, evtRootName, options, globals)); text = text.replace(rgx2, replaceAnchorTagBaseUrl(rgx2, evtRootName, options, globals));
// 4. inline links with titles wrapped in (): [foo](bar.com (title)) // 4. inline links with titles wrapped in (): [foo](bar.com (title))
var rgx3 = /\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s+()()\((.*?)\) *\)/g; var rgx3 = /\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s+()()\((.*?)\) *\)/g;
text = text.replace(rgx3, replaceAnchorTag(rgx3, evtRootName, options, globals)); text = text.replace(rgx3, replaceAnchorTagBaseUrl(rgx3, evtRootName, options, globals));
text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText(); text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText();
@ -222,7 +231,7 @@
text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText(); text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText();
var rgx = /\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g; var rgx = /\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g;
text = text.replace(rgx, replaceAnchorTag(rgx, evtRootName, options, globals)); text = text.replace(rgx, replaceAnchorTagReference(rgx, evtRootName, options, globals));
text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText(); text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText();
@ -238,7 +247,7 @@
text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText(); text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText();
var rgx = /\[([^\[\]]+)]()()()()()/g; var rgx = /\[([^\[\]]+)]()()()()()/g;
text = text.replace(rgx, replaceAnchorTag(rgx, evtRootName, options, globals)); text = text.replace(rgx, replaceAnchorTagReference(rgx, evtRootName, options, globals));
text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText(); text = globals.converter._dispatch(evtRootName + '.end', text, options, globals).getText();

View File

@ -18,6 +18,8 @@ showdown.subParser('makehtml.stripLinkDefinitions', function (text, options, glo
// remove newlines // remove newlines
globals.gUrls[linkId] = url.replace(/\s/g, ''); globals.gUrls[linkId] = url.replace(/\s/g, '');
} else { } else {
url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url);
globals.gUrls[linkId] = showdown.subParser('makehtml.encodeAmpsAndAngles')(url, options, globals); // Link IDs are case-insensitive globals.gUrls[linkId] = showdown.subParser('makehtml.encodeAmpsAndAngles')(url, options, globals); // Link IDs are case-insensitive
} }

View File

@ -0,0 +1,9 @@
<p><a href="http://my.site.com/that_dude_mike.js">inline relative linky</a></p>
<p><a href="ftp://wikis.com/micky.txt">inline absolute linky</a></p>
<p><a href="http://my.site.com/painters/Michelangelo.html">global relative linky</a></p>
<p><a href="https://www.my-wikis-site.com/peeps/Michelangelo.html">global absolute linky</a></p>
<p><img src="http://my.site.com/mona-lisa.png" alt="inline relative image" /></p>
<p><img src="http://images.com/mona-lisa.png" alt="inline absolute image" /></p>
<p><img src="http://my.site.com/mona-lisa.png" alt="global relative image" /></p>
<p><img src="https://www.my-photo-site.com/mona-lisa.png" alt="global absolute image" /></p>
<p><a href="#holdin_it_down">just an anchor</a></p>

View File

@ -0,0 +1,22 @@
[inline relative linky](that_dude_mike.js)
[inline absolute linky](ftp://wikis.com/micky.txt)
[global relative linky][relative_linky]
[global absolute linky][absolute_linky]
![inline relative image](mona-lisa.png)
![inline absolute image](http://images.com/mona-lisa.png)
![global relative image][relative_image]
![global absolute image][absolute_image]
[just an anchor](#holdin_it_down)
[relative_linky]: painters/Michelangelo.html
[relative_image]: ./mona-lisa.png
[absolute_linky]: https://www.my-wikis-site.com/peeps/Michelangelo.html
[absolute_image]: https://www.my-photo-site.com/mona-lisa.png

View File

@ -95,6 +95,8 @@ describe('makeHtml() features testsuite', function () {
converter = new showdown.Converter({openLinksInNewWindow: true}); converter = new showdown.Converter({openLinksInNewWindow: true});
} else if (testsuite[i].name === '#355.simplifiedAutoLink-URLs-inside-parenthesis-followed-by-another-character-are-not-parsed-correctly') { } else if (testsuite[i].name === '#355.simplifiedAutoLink-URLs-inside-parenthesis-followed-by-another-character-are-not-parsed-correctly') {
converter = new showdown.Converter({simplifiedAutoLink: true}); converter = new showdown.Converter({simplifiedAutoLink: true});
} else if (testsuite[i].name === 'relativePathBaseUrl') {
converter = new showdown.Converter({relativePathBaseUrl: 'http://my.site.com/'});
} else { } else {
converter = new showdown.Converter(); converter = new showdown.Converter();
} }