Repository: showdownjs/showdown (js)

Issue:
provide "topurl" to convert relative URL to the full one
May be it is already implemented but I could not determine by looking at the source, and please pardon my naivete in JavaScript

My example is http://datasets.datalad.org/?dir=/labs/haxby/attention where actual url for the page remains http://datasets.datalad.org/ while I am "browsing" nested directories.  README.md is getting rendered by showdown and includes relative URLs such as `stimuli/README.md`.  I would like to inform `Converter` that top URL for those should be http://datasets.datalad.org/labs/haxby/attention .  Is that may be already possible?

Thank you in advance for the clarification!

Pull request description:
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

Patch:
diff --git a/src/helpers.js b/src/helpers.js
index de0b785..554369b 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -400,6 +400,34 @@ showdown.helper._hashHTMLSpan = function (html, globals) {
   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
  * @param {string} name Name of the event
diff --git a/src/options.js b/src/options.js
index afefab0..789e6a7 100644
--- a/src/options.js
+++ b/src/options.js
@@ -160,7 +160,12 @@ function getDefaultOpts (simple) {
       defaultValue: false,
       description: 'Split adjacent blockquote blocks',
       type: 'boolean'
-    }
+    },
+    relativePathBaseUrl: {
+      defaultValue: false,
+      describe: 'Prepends a base URL to relative paths',
+      type: 'string'
+    },
   };
   if (simple === false) {
     return JSON.parse(JSON.stringify(defaultOptions));
diff --git a/src/subParsers/makehtml/images.js b/src/subParsers/makehtml/images.js
index 63d9f09..e7fbbc1 100644
--- a/src/subParsers/makehtml/images.js
+++ b/src/subParsers/makehtml/images.js
@@ -17,6 +17,12 @@ showdown.subParser('makehtml.images', function (text, options, globals) {
     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) {
 
     var gUrls   = globals.gUrls,
@@ -91,10 +97,10 @@ showdown.subParser('makehtml.images', function (text, options, globals) {
   text = text.replace(base64RegExp, writeImageTagBase64);
 
   // cases with crazy urls like ./image/cat1).png
-  text = text.replace(crazyRegExp, writeImageTag);
+  text = text.replace(crazyRegExp, writeImageTagBaseUrl);
 
   // normal cases
-  text = text.replace(inlineRegExp, writeImageTag);
+  text = text.replace(inlineRegExp, writeImageTagBaseUrl);
 
   // handle reference-style shortcuts: ![img text]
   text = text.replace(refShortcutRegExp, writeImageTag);
diff --git a/src/subParsers/makehtml/links.js b/src/subParsers/makehtml/links.js
index 0988621..cac80da 100644
--- a/src/subParsers/makehtml/links.js
+++ b/src/subParsers/makehtml/links.js
@@ -23,7 +23,7 @@
    * @param {{}} globals
    * @returns {Function}
    */
-  function replaceAnchorTag (rgx, evtRootName, options, globals, emptyCase) {
+  function replaceAnchorTagReference (rgx, evtRootName, options, globals, emptyCase) {
     emptyCase = !!emptyCase;
     return function (wholeMatch, text, id, url, m5, m6, title) {
       // 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
    * Helper function: Create a capture event
@@ -192,21 +201,21 @@
 
     // 1. Look for empty cases: []() and [empty]() and []("title")
     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
     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 ":
     // [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 ]*?)]\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))
     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();
 
@@ -222,7 +231,7 @@
     text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText();
 
     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();
 
@@ -238,7 +247,7 @@
     text = globals.converter._dispatch(evtRootName + '.start', text, options, globals).getText();
 
     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();
 
diff --git a/src/subParsers/makehtml/stripLinkDefinitions.js b/src/subParsers/makehtml/stripLinkDefinitions.js
index 350da9a..6aa2ef9 100644
--- a/src/subParsers/makehtml/stripLinkDefinitions.js
+++ b/src/subParsers/makehtml/stripLinkDefinitions.js
@@ -18,6 +18,8 @@ showdown.subParser('makehtml.stripLinkDefinitions', function (text, options, glo
       // remove newlines
       globals.gUrls[linkId] = url.replace(/\s/g, '');
     } else {
+      url = showdown.helper.applyBaseUrl(options.relativePathBaseUrl, url);
+
       globals.gUrls[linkId] = showdown.subParser('makehtml.encodeAmpsAndAngles')(url, options, globals);  // Link IDs are case-insensitive
     }
 


Test patch:
diff --git a/test/functional/makehtml/cases/features/relativePathBaseUrl.html b/test/functional/makehtml/cases/features/relativePathBaseUrl.html
new file mode 100644
index 0000000..1c11eec
--- /dev/null
+++ b/test/functional/makehtml/cases/features/relativePathBaseUrl.html
@@ -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>
diff --git a/test/functional/makehtml/cases/features/relativePathBaseUrl.md b/test/functional/makehtml/cases/features/relativePathBaseUrl.md
new file mode 100644
index 0000000..f03989e
--- /dev/null
+++ b/test/functional/makehtml/cases/features/relativePathBaseUrl.md
@@ -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
diff --git a/test/functional/makehtml/testsuite.features.js b/test/functional/makehtml/testsuite.features.js
index 4affe1c..b1498bd 100644
--- a/test/functional/makehtml/testsuite.features.js
+++ b/test/functional/makehtml/testsuite.features.js
@@ -95,6 +95,8 @@ describe('makeHtml() features testsuite', function () {
         converter = new showdown.Converter({openLinksInNewWindow: true});
       } else if (testsuite[i].name === '#355.simplifiedAutoLink-URLs-inside-parenthesis-followed-by-another-character-are-not-parsed-correctly') {
         converter = new showdown.Converter({simplifiedAutoLink: true});
+      } else if (testsuite[i].name === 'relativePathBaseUrl') {
+        converter = new showdown.Converter({relativePathBaseUrl: 'http://my.site.com/'});
       } else {
         converter = new showdown.Converter();
       }