diff --git a/.gitignore b/.gitignore index b84f3d8..9ea59d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ npm-debug.log +tmp/ .DS_Store? ehthumbs.db diff --git a/Gruntfile.coffee b/Gruntfile.coffee index de9d63a..0f65926 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -1,7 +1,7 @@ module.exports = (grunt) -> - # Configure - # --------- + # Configuration + # ------------- grunt.initConfig @@ -11,6 +11,7 @@ module.exports = (grunt) -> docs: ['docs/*'] lib: ['lib/*'] dist: ['dist/*'] + test: ['tmp'] coffee: all: @@ -46,4 +47,4 @@ module.exports = (grunt) -> grunt.registerTask 'build', ['clean', 'coffee', 'docco', 'uglify'] grunt.registerTask 'default', ['build', 'test'] - grunt.registerTask 'test', ['nodeunit'] + grunt.registerTask 'test', ['clean:test', 'nodeunit'] diff --git a/README.md b/README.md index 068d605..80a31fc 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ $ md -l . -h, --help output usage information -V, --version output the version number - -a, --absolute always use absolute URLs for links + -a, --absolute always use absolute URLs for links and images + -b, --base set base URL to resolve relative URLs from -d, --debug print additional debug information -e, --eval pass a string from the command line as input -i, --inline generate inline style links @@ -122,7 +123,11 @@ The following options are recognised by this method (all of which are optional); absolute - All links are parsed with absolute URLs + All links and images are parsed with absolute URLs + + + base + All relative links and images are resolved from this URLg debug @@ -134,6 +139,8 @@ The following options are recognised by this method (all of which are optional); +**Note:** The `base` option *only* works in the [node.js][] environment. + ### Miscellaneous #### `noConflict()` diff --git a/dist/md.min.js b/dist/md.min.js index a437882..6060563 100644 --- a/dist/md.min.js +++ b/dist/md.min.js @@ -1,3 +1,3 @@ /*! html-md v2.1.0 | (c) 2013 Alasdair Mercer | MIT License Make.text v1.5 | (c) 2007 Trevor Jim -*/ (function(){var t,e,i,s,n,r,o,h,a,u,l,p,c,f,d,E,y,N,m,g,T,b={}.hasOwnProperty,L=this;t={absolute:!1,debug:!1,inline:!1},s=this.md,r={"\\\\":"\\\\","\\[":"\\[","\\]":"\\]",">":"\\>",_:"\\_","\\*":"\\*","`":"\\`","#":"\\#","([0-9])\\.(\\s|$)":"$1\\.$2","©":"(c)","®":"(r)","™":"(tm)"," ":" ","·":"\\*"," ":" "," ":" "," ":" ","‘":"'","’":"'","“":'"',"”":'"',"…":"...","–":"--","—":"---"},o=/(display|visibility)\s*:\s*[a-z]+/gi,h=/(none|hidden)\s*$/i,a=/^(APPLET|AREA|AUDIO|BUTTON|CANVAS|COMMAND|DATALIST|EMBED|HEAD|INPUT|KEYGEN|MAP|MENU|METER|NOFRAMES|NOSCRIPT|OBJECT|OPTGROUP|OPTION|PARAM|PROGRESS|RP|RT|RUBY|SCRIPT|SELECT|SOURCE|STYLE|TEXTAREA|TITLE|TRACK|VIDEO)$/,u=/^(ADDRESS|ARTICLE|ASIDE|DIV|FIELDSET|FOOTER|HEADER|NAV|P|SECTION)$/,n=function(){E={};for(c in r)b.call(r,c)&&(E[c]=RegExp(c,"g"));return E}(),N="undefined"!=typeof window&&null!==window?window:null,null==N&&(p=require("jsdom"),l=p.jsdom(null,null,{features:{FetchExternalResources:!1}}),N=l.createWindow()),i=null!=(m=N.Node)?m:{},null==(g=i.ELEMENT_NODE)&&(i.ELEMENT_NODE=1),null==(T=i.TEXT_NODE)&&(i.TEXT_NODE=3),d=function(t,e,i){var s,n;if(null==t&&(t=""),null==e&&(e=0),null==i&&(i=" "),!i)return t;for(s=n=0;e>=0?e>n:n>e;s=e>=0?++n:--n)t=i+t;return t},y=function(t){return null==t&&(t=""),t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")},e=function(){function e(e,i){var s;this.html=null!=e?e:"",this.options=null!=i?i:{},this.atLeft=this.atNoWS=this.atP=!0,this.buffer="",this.exceptions=[],this.order=1,this.listDepth=0,this.inCode=this.inPre=this.inOrderedList=!1,this.last=null,this.left="\n",this.links=[],this.linkMap={},this.unhandled={},"object"!=typeof this.options&&(this.options={});for(c in t)b.call(t,c)&&(s=t[c],this.options[c]===void 0&&(this.options[c]=s))}return e.prototype.append=function(t){return null!=this.last&&(this.buffer+=this.last),this.last=t},e.prototype.attr=function(t,e,i){var s;return null==i&&(i=!0),s=i||"function"!=typeof t.getAttribute?t[e]:t.getAttribute(e),null!=s?s:""},e.prototype.br=function(){return this.append(" "+this.left),this.atLeft=this.atNoWS=!0},e.prototype.code=function(){var t,e=this;return t=this.inCode,this.inCode=!0,function(){return e.inCode=t}},e.prototype.has=function(t,e,i){return null==i&&(i=!0),i||"function"!=typeof t.hasAttribute?t.hasOwnProperty(e):t.hasAttribute(e)},e.prototype.inCodeProcess=function(t){return t.replace(/`/g,"\\`")},e.prototype.isVisible=function(t){var e,i,s,n,r,a,u,l,p;if(r=this.attr(t,"style",!1),s=null!=r?r.match(o):void 0,u=!0,null!=s)for(l=0,p=s.length;p>l;l++)n=s[l],u=!h.test(n);if(u&&"function"==typeof N.getComputedStyle)try{r=N.getComputedStyle(t,null),"function"==typeof(null!=r?r.getPropertyValue:void 0)&&(e=r.getPropertyValue("display"),a=r.getPropertyValue("visibility"),u="none"!==e&&"hidden"!==a)}catch(c){i=c,this.thrown(i,"getComputedStyle")}return u},e.prototype.li=function(){var t;return t=this.inOrderedList?""+this.order++ +". ":"* ",t=d(t,2*(this.listDepth-1)),this.append(t)},e.prototype.nonPreProcess=function(t){var e;t=t.replace(/\n([ \t]*\n)+/g,"\n"),t=t.replace(/\n[ \t]+/g,"\n"),t=t.replace(/[ \t]+/g," ");for(c in r)b.call(r,c)&&(e=r[c],t=t.replace(n[c],e));return t},e.prototype.ol=function(){var t,e,i=this;return 0===this.listDepth&&this.p(),t=this.inOrderedList,e=this.order,this.inOrderedList=!0,this.order=1,this.listDepth++,function(){return i.inOrderedList=t,i.order=e,i.listDepth--}},e.prototype.output=function(t){return t&&(this.inPre||(t=this.atNoWS?t.replace(/^[ \t\n]+/,""):/^[ \t]*\n/.test(t)?t.replace(/^[ \t\n]+/,"\n"):t.replace(/^[ \t]+/," ")),""!==t)?(this.atP=/\n\n$/.test(t),this.atLeft=/\n$/.test(t),this.atNoWS=/[ \t\n]$/.test(t),this.append(t.replace(/\n/g,this.left))):void 0},e.prototype.outputLater=function(t){var e=this;return function(){return e.output(t)}},e.prototype.p=function(){return this.atP?void 0:(this.atLeft||(this.append(this.left),this.atLeft=!0),this.append(this.left),this.atNoWS=this.atP=!0)},e.prototype.parse=function(){var t,e,i,s,n,r,o,h;if(this.buffer="",!this.html)return this.buffer;if(t=N.document.createElement("div"),"string"==typeof this.html?t.innerHTML=this.html:t.appendChild(this.html),this.process(t),this.links.length)for(this.append("\n\n"),h=this.links,e=r=0,o=h.length;o>r;e=++r)i=h[e],i&&this.append("["+e+"]: "+i+"\n");return this.options.debug&&(n=function(){var t,e;t=this.unhandled,e=[];for(s in t)b.call(t,s)&&e.push(s);return e}.call(this).sort(),console.log(n.length?"Ignored tags;\n"+n.join(", "):"No tags were ignored"),console.log(this.exceptions.length?"Exceptions;\n"+this.exceptions.join("\n"):"No exceptions were thrown")),this.append(""),this.buffer=y(this.buffer)},e.prototype.pre=function(){var t,e=this;return t=this.inPre,this.inPre=!0,function(){return e.inPre=t}},e.prototype.process=function(t){var e,s,n,r,o,h,l,p,c,f,d,E,y,N,m,g,T,b,L;if(this.isVisible(t)){if(t.nodeType===i.ELEMENT_NODE){c=!1;try{if(a.test(t.tagName))c=!0;else if(/^H[1-6]$/.test(t.tagName))p=parseInt(t.tagName.match(/([1-6])$/)[1]),this.p(),this.output(""+function(){var t,e;for(e=[],l=t=1;p>=1?p>=t:t>=p;l=p>=1?++t:--t)e.push("#");return e}().join("")+" ");else if(u.test(t.tagName))this.p();else switch(t.tagName){case"BODY":case"FORM":break;case"DETAILS":this.p(),this.has(t,"open",!1)||(c=!0,E=t.getElementsByTagName("summary")[0],E&&this.process(E));break;case"BR":this.br();break;case"HR":this.p(),this.output("---"),this.p();break;case"CITE":case"DFN":case"EM":case"I":case"U":case"VAR":this.output("_"),this.atNoWS=!0,e=this.outputLater("_");break;case"DT":case"B":case"STRONG":"DT"===t.tagName&&this.p(),this.output("**"),this.atNoWS=!0,e=this.outputLater("**");break;case"Q":this.output('"'),this.atNoWS=!0,e=this.outputLater('"');break;case"OL":case"UL":e="OL"===t.tagName?this.ol():this.ul();break;case"LI":this.li();break;case"PRE":s=this.pushLeft(" "),n=this.pre(),e=function(){return s(),n()};break;case"CODE":case"KBD":case"SAMP":if(this.inPre)break;this.output("`"),s=this.code(),n=this.outputLater("`"),e=function(){return s(),n()};break;case"BLOCKQUOTE":case"DD":e=this.pushLeft("> ");break;case"A":if(h=this.attr(t,"href",this.options.absolute),!h)break;y=this.attr(t,"title"),y&&(h+=' "'+y+'"'),d=this.options.inline?"("+h+")":"["+(null!=(T=(N=this.linkMap)[h])?T:N[h]=this.links.push(h)-1)+"]",this.output("["),this.atNoWS=!0,e=this.outputLater("]"+d);break;case"IMG":if(c=!0,f=this.attr(t,"src",this.options.absolute),!f)break;this.output("!["+this.attr(t,"alt")+"]("+f+")");break;case"FRAME":case"IFRAME":c=!0;try{(null!=(b=t.contentDocument)?b.documentElement:void 0)&&this.process(t.contentDocument.documentElement)}catch(O){o=O,this.thrown(o,"contentDocument")}break;case"TR":e=this.p;break;default:this.options.debug&&(this.unhandled[t.tagName]=null)}}catch(O){o=O,this.thrown(o,t.tagName)}if(!c)for(L=t.childNodes,m=0,g=L.length;g>m;m++)r=L[m],this.process(r);return null!=e?e.call(this):void 0}return t.nodeType===i.TEXT_NODE?this.output(this.inPre?t.nodeValue:this.inCode?this.inCodeProcess(t.nodeValue):this.nonPreProcess(t.nodeValue)):void 0}},e.prototype.pushLeft=function(t){var e,i=this;return e=this.left,this.left+=t,this.atP?this.append(t):this.p(),function(){return i.left=e,i.atLeft=i.atP=!1,i.p()}},e.prototype.replaceLeft=function(t){return this.atLeft?this.last?this.last=this.last.replace(/[ ]{2,4}$/,t):void 0:(this.append(this.left.replace(/[ ]{2,4}$/,t)),this.atLeft=this.atNoWS=this.atP=!0)},e.prototype.thrown=function(t,e){return this.options.debug?this.exceptions.push(""+e+": "+t):void 0},e.prototype.ul=function(){var t,e,i=this;return 0===this.listDepth&&this.p(),t=this.inOrderedList,e=this.order,this.inOrderedList=!1,this.order=1,this.listDepth++,function(){return i.inOrderedList=t,i.order=e,i.listDepth--}},e}(),this.md=f=function(t,i){return new e(t,i).parse()},("undefined"!=typeof module&&null!==module?module.exports:void 0)?module.exports=f:"function"==typeof define&&define.amd&&define("md",function(){return f}),f.version=f.VERSION="2.1.0",f.noConflict=function(){return L.md=s,f}}).call(this); \ No newline at end of file +*/ (function(){var t,e,i,s,n,r,o,h,a,u,p,l,c,d,f={}.hasOwnProperty,E=this;t={absolute:!1,base:"undefined"!=typeof window&&null!==window?window.document.baseURI:"file://"+process.cwd(),debug:!1,inline:!1},i=this.md,n={"\\\\":"\\\\","\\[":"\\[","\\]":"\\]",">":"\\>",_:"\\_","\\*":"\\*","`":"\\`","#":"\\#","([0-9])\\.(\\s|$)":"$1\\.$2","©":"(c)","®":"(r)","™":"(tm)"," ":" ","·":"\\*"," ":" "," ":" "," ":" ","‘":"'","’":"'","“":'"',"”":'"',"…":"...","–":"--","—":"---"},r=/(display|visibility)\s*:\s*[a-z]+/gi,o=/(none|hidden)\s*$/i,h=/^(APPLET|AREA|AUDIO|BUTTON|CANVAS|COMMAND|DATALIST|EMBED|HEAD|INPUT|KEYGEN|MAP|MENU|METER|NOFRAMES|NOSCRIPT|OBJECT|OPTGROUP|OPTION|PARAM|PROGRESS|RP|RT|RUBY|SCRIPT|SELECT|SOURCE|STYLE|TEXTAREA|TITLE|TRACK|VIDEO)$/,a=/^(ADDRESS|ARTICLE|ASIDE|DIV|FIELDSET|FOOTER|HEADER|NAV|P|SECTION)$/,s=function(){c={};for(u in n)f.call(n,u)&&(c[u]=RegExp(u,"g"));return c}(),l=function(t,e,i){var s,n;if(null==t&&(t=""),null==e&&(e=0),null==i&&(i=" "),!i)return t;for(s=n=0;e>=0?e>n:n>e;s=e>=0?++n:--n)t=i+t;return t},d=function(t){return null==t&&(t=""),t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")},e=function(){function e(e,i){var s,n;this.html=null!=e?e:"",this.options=null!=i?i:{},this.atLeft=this.atNoWS=this.atP=!0,this.buffer="",this.exceptions=[],this.order=1,this.listDepth=0,this.inCode=this.inPre=this.inOrderedList=!1,this.last=null,this.left="\n",this.links=[],this.linkMap={},this.unhandled={},"object"!=typeof this.options&&(this.options={});for(u in t)f.call(t,u)&&(s=t[u],this.options[u]===void 0&&(this.options[u]=s));this.win="undefined"!=typeof window&&null!==window?window:null,null==this.win&&(n=require("jsdom").jsdom(null,null,{features:{FetchExternalResources:!1},url:this.options.base}),this.win=n.createWindow())}return e.prototype.append=function(t){return null!=this.last&&(this.buffer+=this.last),this.last=t},e.prototype.attr=function(t,e,i){var s;return null==i&&(i=!0),s=i||"function"!=typeof t.getAttribute?t[e]:t.getAttribute(e),null!=s?s:""},e.prototype.br=function(){return this.append(" "+this.left),this.atLeft=this.atNoWS=!0},e.prototype.code=function(){var t,e=this;return t=this.inCode,this.inCode=!0,function(){return e.inCode=t}},e.prototype.has=function(t,e,i){return null==i&&(i=!0),i||"function"!=typeof t.hasAttribute?t.hasOwnProperty(e):t.hasAttribute(e)},e.prototype.inCodeProcess=function(t){return t.replace(/`/g,"\\`")},e.prototype.isVisible=function(t){var e,i,s,n,h,a,u,p,l;if(h=this.attr(t,"style",!1),s=null!=h?h.match(r):void 0,u=!0,null!=s)for(p=0,l=s.length;l>p;p++)n=s[p],u=!o.test(n);if(u&&"function"==typeof this.win.getComputedStyle)try{h=this.win.getComputedStyle(t,null),"function"==typeof(null!=h?h.getPropertyValue:void 0)&&(e=h.getPropertyValue("display"),a=h.getPropertyValue("visibility"),u="none"!==e&&"hidden"!==a)}catch(c){i=c,this.thrown(i,"getComputedStyle")}return u},e.prototype.li=function(){var t;return t=this.inOrderedList?""+this.order++ +". ":"* ",t=l(t,2*(this.listDepth-1)),this.append(t)},e.prototype.nonPreProcess=function(t){var e;t=t.replace(/\n([ \t]*\n)+/g,"\n"),t=t.replace(/\n[ \t]+/g,"\n"),t=t.replace(/[ \t]+/g," ");for(u in n)f.call(n,u)&&(e=n[u],t=t.replace(s[u],e));return t},e.prototype.ol=function(){var t,e,i=this;return 0===this.listDepth&&this.p(),t=this.inOrderedList,e=this.order,this.inOrderedList=!0,this.order=1,this.listDepth++,function(){return i.inOrderedList=t,i.order=e,i.listDepth--}},e.prototype.output=function(t){return t&&(this.inPre||(t=this.atNoWS?t.replace(/^[ \t\n]+/,""):/^[ \t]*\n/.test(t)?t.replace(/^[ \t\n]+/,"\n"):t.replace(/^[ \t]+/," ")),""!==t)?(this.atP=/\n\n$/.test(t),this.atLeft=/\n$/.test(t),this.atNoWS=/[ \t\n]$/.test(t),this.append(t.replace(/\n/g,this.left))):void 0},e.prototype.outputLater=function(t){var e=this;return function(){return e.output(t)}},e.prototype.p=function(){return this.atP?void 0:(this.atLeft||(this.append(this.left),this.atLeft=!0),this.append(this.left),this.atNoWS=this.atP=!0)},e.prototype.parse=function(){var t,e,i,s,n,r,o,h;if(this.buffer="",!this.html)return this.buffer;if(t=this.win.document.createElement("div"),"string"==typeof this.html?t.innerHTML=this.html:t.appendChild(this.html),this.process(t),this.links.length)for(this.append("\n\n"),h=this.links,e=r=0,o=h.length;o>r;e=++r)i=h[e],i&&this.append("["+e+"]: "+i+"\n");return this.options.debug&&(n=function(){var t,e;t=this.unhandled,e=[];for(s in t)f.call(t,s)&&e.push(s);return e}.call(this).sort(),console.log(n.length?"Ignored tags;\n"+n.join(", "):"No tags were ignored"),console.log(this.exceptions.length?"Exceptions;\n"+this.exceptions.join("\n"):"No exceptions were thrown")),this.append(""),this.buffer=d(this.buffer)},e.prototype.pre=function(){var t,e=this;return t=this.inPre,this.inPre=!0,function(){return e.inPre=t}},e.prototype.process=function(t){var e,i,s,n,r,o,u,p,l,c,d,f,E,y,m,b,g,N,L;if(this.isVisible(t)){if(t.nodeType===this.win.Node.ELEMENT_NODE){l=!1;try{if(h.test(t.tagName))l=!0;else if(/^H[1-6]$/.test(t.tagName))p=parseInt(t.tagName.match(/([1-6])$/)[1]),this.p(),this.output(""+function(){var t,e;for(e=[],u=t=1;p>=1?p>=t:t>=p;u=p>=1?++t:--t)e.push("#");return e}().join("")+" ");else if(a.test(t.tagName))this.p();else switch(t.tagName){case"BODY":case"FORM":break;case"DETAILS":this.p(),this.has(t,"open",!1)||(l=!0,f=t.getElementsByTagName("summary")[0],f&&this.process(f));break;case"BR":this.br();break;case"HR":this.p(),this.output("---"),this.p();break;case"CITE":case"DFN":case"EM":case"I":case"U":case"VAR":this.output("_"),this.atNoWS=!0,e=this.outputLater("_");break;case"DT":case"B":case"STRONG":"DT"===t.tagName&&this.p(),this.output("**"),this.atNoWS=!0,e=this.outputLater("**");break;case"Q":this.output('"'),this.atNoWS=!0,e=this.outputLater('"');break;case"OL":case"UL":e="OL"===t.tagName?this.ol():this.ul();break;case"LI":this.li();break;case"PRE":i=this.pushLeft(" "),s=this.pre(),e=function(){return i(),s()};break;case"CODE":case"KBD":case"SAMP":if(this.inPre)break;this.output("`"),i=this.code(),s=this.outputLater("`"),e=function(){return i(),s()};break;case"BLOCKQUOTE":case"DD":e=this.pushLeft("> ");break;case"A":if(o=this.attr(t,"href",this.options.absolute),!o)break;E=this.attr(t,"title"),E&&(o+=' "'+E+'"'),d=this.options.inline?"("+o+")":"["+(null!=(g=(y=this.linkMap)[o])?g:y[o]=this.links.push(o)-1)+"]",this.output("["),this.atNoWS=!0,e=this.outputLater("]"+d);break;case"IMG":if(l=!0,c=this.attr(t,"src",this.options.absolute),!c)break;this.output("!["+this.attr(t,"alt")+"]("+c+")");break;case"FRAME":case"IFRAME":l=!0;try{(null!=(N=t.contentDocument)?N.documentElement:void 0)&&this.process(t.contentDocument.documentElement)}catch(P){r=P,this.thrown(r,"contentDocument")}break;case"TR":e=this.p;break;default:this.options.debug&&(this.unhandled[t.tagName]=null)}}catch(P){r=P,this.thrown(r,t.tagName)}if(!l)for(L=t.childNodes,m=0,b=L.length;b>m;m++)n=L[m],this.process(n);return null!=e?e.call(this):void 0}return t.nodeType===this.win.Node.TEXT_NODE?this.output(this.inPre?t.nodeValue:this.inCode?this.inCodeProcess(t.nodeValue):this.nonPreProcess(t.nodeValue)):void 0}},e.prototype.pushLeft=function(t){var e,i=this;return e=this.left,this.left+=t,this.atP?this.append(t):this.p(),function(){return i.left=e,i.atLeft=i.atP=!1,i.p()}},e.prototype.replaceLeft=function(t){return this.atLeft?this.last?this.last=this.last.replace(/[ ]{2,4}$/,t):void 0:(this.append(this.left.replace(/[ ]{2,4}$/,t)),this.atLeft=this.atNoWS=this.atP=!0)},e.prototype.thrown=function(t,e){return this.options.debug?this.exceptions.push(""+e+": "+t):void 0},e.prototype.ul=function(){var t,e,i=this;return 0===this.listDepth&&this.p(),t=this.inOrderedList,e=this.order,this.inOrderedList=!1,this.order=1,this.listDepth++,function(){return i.inOrderedList=t,i.order=e,i.listDepth--}},e}(),this.md=p=function(t,i){return new e(t,i).parse()},("undefined"!=typeof module&&null!==module?module.exports:void 0)?module.exports=p:"function"==typeof define&&define.amd&&define("md",function(){return p}),p.version=p.VERSION="2.1.0",p.noConflict=function(){return E.md=i,p}}).call(this); \ No newline at end of file diff --git a/docs/command.html b/docs/command.html index 68c6c64..21778e5 100644 --- a/docs/command.html +++ b/docs/command.html @@ -16,21 +16,21 @@ code ?= 0 console[if code then 'error' else 'log'] message if message - process.exit code

Resolve the output path for source.

outputPath = (source, base) ->
+  process.exit code

Resolve the output path for source.

outputPath = (source, root) ->
   fileName = path.basename(source, path.extname(source)) + extension
   srcDir   = path.dirname source
   dir      = if program.output
-    path.join program.output, if base is '.' then srcDir else srcDir[base.length..]
+    path.join program.output, if root is '.' then srcDir else srcDir[root.length..]
   else
     srcDir
 
   path.join dir, fileName

Attempt to either parse the contents of the source path or, if it's a directory, the contents -of its children HTML files.

parsePath = (source, topLevel, base) ->
+of its children HTML files.

parsePath = (source, topLevel, root) ->
   fs.stat source, (err, stats) ->
     if err

We only care about ENOENT errors so everything else is thrown.

      throw err if err.code isnt NOT_FOUND

Either exit stating the file could not be found or attempt to make improve the file name's searchability and trying again.

      if topLevel and not R_HTML_EXT.test source
         source = sources[sources.indexOf(source)] = "#{source}.html"
-        parsePath source, topLevel, base
+        parsePath source, topLevel, root
       else if topLevel
         exit 1, "File not found: #{source}"
       return
@@ -39,19 +39,20 @@
 and check each of them.

      fs.readdir source, (err, files) ->
         throw err if err

Ensure files are relative to their parent directory.

        files = files.map (file) ->
           path.join source, file

Replace the directory in sources with all of its children files.

        index = sources.indexOf source
-        sources[index..index] = files

Now finally try to parse all of the directory files.

        parsePath file, no, base for file in files
+        sources[index..index] = files

Now finally try to parse all of the directory files.

        parsePath file, no, root for file in files
     else if topLevel or R_HTML_EXT.test source

Possible source file found so read its contents.

      fs.readFile source, (err, html) ->
-        throw err if err

Treat the files contents as HTML and try to parse it using md.

        parseHtml source, html.toString(), base
+        throw err if err

Treat the files contents as HTML and try to parse it using md.

        parseHtml source, html.toString(), root
     else

Doesn't appear to have a recognizable HTML file extension so just ignore the file and -remove it from the list of sources.

      sources.splice sources.indexOf(source), 1

Parse the HTML input and write it out accordingly.

parseHtml = (file, input, base) ->
-  try

Extract only relevant options from program.

    {absolute, debug, inline} = program

Let md work its magic on the HTML input.

    output                    = md input, {absolute, debug, inline}

Either write the output to stdout or to a corresponding Markdown file depending on whether +remove it from the list of sources.

      sources.splice sources.indexOf(source), 1

Parse the HTML input and write it out accordingly.

parseHtml = (file, input, root) ->
+  try

Extract only relevant options from program.

    {absolute, base,  debug, inline} = program

Let md work its magic on the HTML input.

    output                           = md input, {absolute, base, debug, inline}

Either write the output to stdout or to a corresponding Markdown file depending on whether the print option was enabled.

    if program.print then console.log output
-    else if file     then writeMarkdown file, output, base
+    else if file     then writeMarkdown file, output, root
   catch err

An error occured while parsing input, so stop processing all sources and exit, while also -letting the user know why.

    exit 1, err

Parse the options that could have been provided at runtime.

parseOptions = ->

Parse the specified options/switches/flags/whatever.

  program
+letting the user know why.

    exit 1, err

Parse the options that could have been provided at runtime.

parseOptions = ->

Parse the specified options/switches/flags/whatever.

  program
     .version(md.version)
     .usage('Usage: md [options] [ -e html | <file ...> ]')
-    .option('-a, --absolute',     'always use absolute URLs for links')
+    .option('-a, --absolute',     'always use absolute URLs for links and images')
+    .option('-b, --base <url>',   'set base URL to resolve relative URLs from')
     .option('-d, --debug',        'print additional debug information')
     .option('-e, --eval',         'pass a string from the command line as input')
     .option('-i, --inline',       'generate inline style links')
@@ -61,14 +62,14 @@
     .parse process.argv

Ensure that the longer Markdown file extension is used if/when the associated option is enabled.

  extension = '.markdown' if program.longExt

All left-over arguments are considered potential HTML source files/input and will be handled as such.

  sources   = program.args[..]

No additional arguments were specified? Then simply print out the usage as the user might not -know how to use this command.

  program.help() unless sources.length

Write the markdown converted from source to its relative output file.

writeMarkdown = (source, markdown, base) ->

Derive the best output file path based on the name of the source file.

  mdPath = outputPath source, base
+know how to use this command.

  program.help() unless sources.length

Write the markdown converted from source to its relative output file.

writeMarkdown = (source, markdown, root) ->

Derive the best output file path based on the name of the source file.

  mdPath = outputPath source, root
   mdDir  = path.dirname mdPath

Write markdown to its corresponding output file.

  write = (err) ->
     throw err if err

Ensure something is always written in the file.

    markdown = ' ' if markdown.length <= 0
     fs.writeFile mdPath, markdown, (err) ->
       throw err if err

If the target files parent directory doesn't already exists, create it before writing to the file itself.

  fs.exists mdDir, (exists) ->
-    if exists then do write else fs.mkdirs mdDir, write

Public API

Handle the runtime arguments accordingly.

exports.run = ->

Try to ensure graceful exits whenever possible.

  unless process.platform is 'win32'
-    process.on 'SIGTERM', ->
+    if exists then do write else fs.mkdirs mdDir, write

Public API

Handle the runtime arguments accordingly.

exports.run = ->

Try to ensure graceful exits whenever possible.

  unless process.platform is 'win32'
+    process.on 'SIGTERM', ->
       do exit
 
   try

Parse the arguments passed in at runtime as options/switches.

    do parseOptions
diff --git a/docs/md.html b/docs/md.html
index 6285aca..7f24a6d 100644
--- a/docs/md.html
+++ b/docs/md.html
@@ -6,6 +6,7 @@
 For all details and documentation: 
http://neocotic.com/html.md

Private constants

Default option values.

DEFAULT_OPTIONS   =
   absolute: no
+  base:     if window? then window.document.baseURI else "file://#{process.cwd()}"
   debug:    no
   inline:   no

Save the previous value of the global md variable for noConflict mode.

PREVIOUS_MD       = @md

Map of replacement strings for special Markdown characters.

REPLACEMENTS      =
   '\\\\':              '\\\\'
@@ -32,7 +33,7 @@
   '\u2026':            '...'
   '\u2013':            '--'
   '\u2014':            '---'

Regular expression to extract all display and visibility CSS properties from an inline style -attribute.

R_HIDDEN_STYLES   = /(display|visibility)\s*:\s*[a-z]+/gi

Regular expression to check for hidden values of CSS properties.

R_HIDDEN_VALUE    = /(none|hidden)\s*$/i

Regular expression to identify elements to be generally ignored, along with their children.

R_IGNORE_CHILDREN = /// ^ (
+attribute.

R_HIDDEN_STYLES   = /(display|visibility)\s*:\s*[a-z]+/gi

Regular expression to check for hidden values of CSS properties.

R_HIDDEN_VALUE    = /(none|hidden)\s*$/i

Regular expression to identify elements to be generally ignored, along with their children.

R_IGNORE_CHILDREN = /// ^ (
     APPLET
   | AREA
   | AUDIO
@@ -65,7 +66,7 @@
   | TITLE
   | TRACK
   | VIDEO
-) $ ///

Regular expression to identify elements to be parsed as simple paragraphs.

R_PARAGRAPH_ONLY  = /// ^ (
+) $ ///

Regular expression to identify elements to be parsed as simple paragraphs.

R_PARAGRAPH_ONLY  = /// ^ (
     ADDRESS
   | ARTICLE
   | ASIDE
@@ -79,23 +80,16 @@
 ) $ ///

Create a map of regular expressions for all of the special Markdown characters to simplify access.

REGEX             = (
   result = {}
-  result[key] = new RegExp key, 'g' for own key of REPLACEMENTS
+  result[key] = new RegExp key, 'g' for own key of REPLACEMENTS
   result
-)

Environment Support

Create a DOM if window doesn't exist (i.e. when running in node).

win = window ? null
-unless win?
-  jsdom = require 'jsdom'
-  doc   = jsdom.jsdom null, null, features: FetchExternalResources: no
-  win   = doc.createWindow()

Try to ensure Node is available with the required constants. Probably not required; more of a -sanity check.

Node = win.Node ? {}
-Node.ELEMENT_NODE ?= 1
-Node.TEXT_NODE    ?= 3

Helper functions

Left pad str with the given padding string for the specified number of times.

padLeft = (str = '', times = 0, padStr = ' ') ->
-  return str unless padStr
+)

Helper functions

Left pad str with the given padding string for the specified number of times.

padLeft = (str = '', times = 0, padStr = ' ') ->
+  return str unless padStr
 
   str = padStr + str for i in [0...times]
-  str

Remove whitespace from both ends of str.
+ str

Remove whitespace from both ends of str.
This tries to use the native String.prototype.trim function where possible.

trim = (str = '') ->
-  if str.trim then str.trim() else str.replace /^\s+|\s+$/g, ''

HTML Parser

Parses HTML code and/or elements into valid Markdown.
-Elements are parsed recursively, meaning their children are also parsed.

class HtmlParser

Creates a new HtmlParser for the arguments provided.

  constructor: (@html = '', @options = {}) ->
+  if str.trim then str.trim() else str.replace /^\s+|\s+$/g, ''

HTML Parser

Parses HTML code and/or elements into valid Markdown.
+Elements are parsed recursively, meaning their children are also parsed.

class HtmlParser

Creates a new HtmlParser for the arguments provided.

  constructor: (@html = '', @options = {}) ->
     @atLeft     = @atNoWS = @atP           = yes
     @buffer     = ''
     @exceptions = []
@@ -107,34 +101,39 @@
     @links      = []
     @linkMap    = {}
     @unhandled  = {}
-    @options    = {} if typeof @options isnt 'object'

Copy all default option values across to options only where they were not specified.

    for own key, defaultValue of DEFAULT_OPTIONS
-      @options[key] = defaultValue if typeof @options[key] is 'undefined'

Append str to the buffer string.

  append: (str) ->
+    @options    = {} if typeof @options isnt 'object'

Copy all default option values across to options only where they were not specified.

    for own key, defaultValue of DEFAULT_OPTIONS
+      @options[key] = defaultValue if typeof @options[key] is 'undefined'

Create a DOM if window doesn't exist (i.e. when running in node).

    @win = window ? null
+    unless @win?
+      doc  = require('jsdom').jsdom null, null,
+        features: FetchExternalResources: no
+        url:      @options.base
+      @win = doc.createWindow()

Append str to the buffer string.

  append: (str) ->
     @buffer += @last if @last?
-    @last    = str

Access the value of attribute either directly or using getAttribute if possible.

  attr: (ele, attribute, direct = yes) ->
+    @last    = str

Access the value of attribute either directly or using getAttribute if possible.

  attr: (ele, attribute, direct = yes) ->
     value = if direct or typeof ele.getAttribute isnt 'function'
       ele[attribute]
     else
       ele.getAttribute attribute
 
-    value ? ''

Append a Markdown line break to the buffer string.

  br: ->
+    value ? ''

Append a Markdown line break to the buffer string.

  br: ->
     @append "  #{@left}"
-    @atLeft = @atNoWS = yes

Prepare the parser for a code element.

  code: ->
+    @atLeft = @atNoWS = yes

Prepare the parser for a code element.

  code: ->
     old     = @inCode
     @inCode = yes
 
-    => @inCode = old

Determine whether the specified element has the attribute provided either directory or using + => @inCode = old

Determine whether the specified element has the attribute provided either directory or using hasAttribute if possible.

  has: (ele, attribute, direct = yes) ->
     if direct or typeof ele.hasAttribute isnt 'function'
       ele.hasOwnProperty attribute
     else
-      ele.hasAttribute attribute

Replace any special characters that can cause problems within code sections.

  inCodeProcess: (str) ->
-    str.replace /`/g, '\\`'

Determine whether or not the specified element is visible based on its CSS style.

  isVisible: (ele) ->
+      ele.hasAttribute attribute

Replace any special characters that can cause problems within code sections.

  inCodeProcess: (str) ->
+    str.replace /`/g, '\\`'

Determine whether or not the specified element is visible based on its CSS style.

  isVisible: (ele) ->
     style      = @attr ele, 'style', no
     properties = style?.match R_HIDDEN_STYLES
-    visible    = yes

Test all relevant CSS properties for possible hiding behaviours.

    if properties?
-      visible = not R_HIDDEN_VALUE.test property for property in properties

Attempt to derive elements visibility based on its computed CSS style where appropriate.

    if visible and typeof win.getComputedStyle is 'function'
+    visible    = yes

Test all relevant CSS properties for possible hiding behaviours.

    if properties?
+      visible = not R_HIDDEN_VALUE.test property for property in properties

Attempt to derive elements visibility based on its computed CSS style where appropriate.

    if visible and typeof @win.getComputedStyle is 'function'
       try
-        style = win.getComputedStyle ele, null
+        style = @win.getComputedStyle ele, null
 
         if typeof style?.getPropertyValue is 'function'
           display    = style.getPropertyValue 'display'
@@ -143,16 +142,16 @@
       catch err
         @thrown err, 'getComputedStyle'
 
-    visible

Append a Markdown list item based on the context of the current list.

  li: ->
+    visible

Append a Markdown list item based on the context of the current list.

  li: ->
     str = if @inOrderedList then "#{@order++}. " else '* '
     str = padLeft str, (@listDepth - 1) * 2
 
-    @append str

Replace any special characters that can cause problems in normal Markdown blocks.

  nonPreProcess: (str) ->
+    @append str

Replace any special characters that can cause problems in normal Markdown blocks.

  nonPreProcess: (str) ->
     str = str.replace /\n([ \t]*\n)+/g, '\n'
     str = str.replace /\n[ \t]+/g,      '\n'
     str = str.replace /[ \t]+/g,        ' '
-    str = str.replace REGEX[key], value for own key, value of REPLACEMENTS
-    str

Prepare the parser for an ol element.

  ol: ->
+    str = str.replace REGEX[key], value for own key, value of REPLACEMENTS
+    str

Prepare the parser for an ol element.

  ol: ->
     do @p if @listDepth is 0
 
     inOrderedList  = @inOrderedList
@@ -164,8 +163,8 @@
     =>
       @inOrderedList = inOrderedList
       @order         = order
-      @listDepth--

Append str to the buffer string.

  output: (str) ->
-    return unless str

Strip leading whitespace when code blocks accordingly.

    unless @inPre
+      @listDepth--

Append str to the buffer string.

  output: (str) ->
+    return unless str

Strip leading whitespace when code blocks accordingly.

    unless @inPre
       str = if @atNoWS
         str.replace /^[ \t\n]+/, ''
       else if /^[ \t]*\n/.test str
@@ -173,91 +172,91 @@
       else
         str.replace /^[ \t]+/, ' '
 
-    return if str is ''

Ensure this parser is in the correct context.

    @atP    = /\n\n$/.test str
+    return if str is ''

Ensure this parser is in the correct context.

    @atP    = /\n\n$/.test str
     @atLeft = /\n$/.test str
     @atNoWS = /[ \t\n]$/.test str
-    @append str.replace /\n/g, @left

Create a function that can be called later to append str to the buffer string while keeping + @append str.replace /\n/g, @left

Create a function that can be called later to append str to the buffer string while keeping the parser in context.
This function is just a lazy shorthand.

  outputLater: (str) ->
-    => @output str

Append a Markdown paragraph section to the buffer string.

  p: ->
+    => @output str

Append a Markdown paragraph section to the buffer string.

  p: ->
     return if @atP
 
-    unless @atLeft
+    unless @atLeft
       @append @left
       @atLeft = yes
 
     @append @left
-    @atNoWS = @atP = yes

Parse the configured HTML into valid Markdown.

  parse: ->
+    @atNoWS = @atP = yes

Parse the configured HTML into valid Markdown.

  parse: ->
     @buffer = ''
 
-    return @buffer unless @html

Create a wrapper element to insert the configured HTML into.

    container = win.document.createElement 'div'
+    return @buffer unless @html

Create a wrapper element to insert the configured HTML into.

    container = @win.document.createElement 'div'
     if typeof @html is 'string'
       container.innerHTML = @html
     else
-      container.appendChild @html

Process the contents (i.e. the preconfigured HTML) of the wrapper element.

    @process container

Ensure all link references are correctly appended to be end of the buffer string.

    if @links.length
+      container.appendChild @html

Process the contents (i.e. the preconfigured HTML) of the wrapper element.

    @process container

Ensure all link references are correctly appended to be end of the buffer string.

    if @links.length
       @append '\n\n'
-      @append "[#{i}]: #{link}\n" for link, i in @links when link

This isn't nice and I wouldn't really recommend users use this option but, when debug is -enabled, output all debug information either to the console (e.g. stdout in node).

    if @options.debug

List any tags that were ignored during processing.

      unhandledTags = (tag for own tag of @unhandled).sort()
+      @append "[#{i}]: #{link}\n" for link, i in @links when link

This isn't nice and I wouldn't really recommend users use this option but, when debug is +enabled, output all debug information either to the console (e.g. stdout in node).

    if @options.debug

List any tags that were ignored during processing.

      unhandledTags = (tag for own tag of @unhandled).sort()
       console.log if unhandledTags.length
         """
           Ignored tags;
           #{unhandledTags.join ', '}
         """
       else
-        'No tags were ignored'

List any exceptions that were caught (and swallowed) during processing.

      console.log if @exceptions.length
+        'No tags were ignored'

List any exceptions that were caught (and swallowed) during processing.

      console.log if @exceptions.length
         """
           Exceptions;
           #{@exceptions.join '\n'}
         """
       else
-        'No exceptions were thrown'

End the buffer string cleanly and we're done!

    @append ''
-    @buffer = trim @buffer

Prepare the parser for a pre element.

  pre: ->
+        'No exceptions were thrown'

End the buffer string cleanly and we're done!

    @append ''
+    @buffer = trim @buffer

Prepare the parser for a pre element.

  pre: ->
     old    = @inPre
     @inPre = yes
 
-    => @inPre = old

Parse the specified element and append the generated Markdown to the buffer string.

  process: (ele) ->

Only visible elements are processed. Doing our best to identify those that are hidden.

    return unless @isVisible ele
+    => @inPre = old

Parse the specified element and append the generated Markdown to the buffer string.

  process: (ele) ->

Only visible elements are processed. Doing our best to identify those that are hidden.

    return unless @isVisible ele
 
-    if ele.nodeType is Node.ELEMENT_NODE

Handle typical node elements (e.g. <span>foo bar</span>).

      skipChildren = no

Determine the best way (if any) to handle ele.

      try
-        if R_IGNORE_CHILDREN.test ele.tagName

Don't process the element or any of its children.

          skipChildren = yes
-        else if /^H[1-6]$/.test ele.tagName

Convert HTML headers (e.g. h3) to their Markdown equivalents (e.g. ###).

          level = parseInt ele.tagName.match(/([1-6])$/)[1]
+    if ele.nodeType is @win.Node.ELEMENT_NODE

Handle typical node elements (e.g. <span>foo bar</span>).

      skipChildren = no

Determine the best way (if any) to handle ele.

      try
+        if R_IGNORE_CHILDREN.test ele.tagName

Don't process the element or any of its children.

          skipChildren = yes
+        else if /^H[1-6]$/.test ele.tagName

Convert HTML headers (e.g. h3) to their Markdown equivalents (e.g. ###).

          level = parseInt ele.tagName.match(/([1-6])$/)[1]
 
           do @p
           @output "#{('#' for i in [1..level]).join ''} "
-        else if R_PARAGRAPH_ONLY.test ele.tagName

Paragraphs are easy as Pi.

          do @p
+        else if R_PARAGRAPH_ONLY.test ele.tagName

Paragraphs are easy as Pi.

          do @p
         else
-          switch ele.tagName

Ignore the element, but we still want to process their children (obviously).

            when 'BODY', 'FORM' then break

Can be a simple paragraph but, if ele doesn't have an open attribute specified, + switch ele.tagName

Ignore the element, but we still want to process their children (obviously).

            when 'BODY', 'FORM' then break

Can be a simple paragraph but, if ele doesn't have an open attribute specified, we don't want to process anything other than the first nested summary element.

            when 'DETAILS'
               do @p
 
-              unless @has ele, 'open', no
+              unless @has ele, 'open', no
                 skipChildren = yes
                 summary      = ele.getElementsByTagName('summary')[0]
-                @process summary if summary

Easiest of the bunch... just a Markdown line break.

            when 'BR' then do @br

Insert a horizontal ruler padded on before and after.

            when 'HR'
+                @process summary if summary

Easiest of the bunch... just a Markdown line break.

            when 'BR' then do @br

Insert a horizontal ruler padded on before and after.

            when 'HR'
               do @p
               @output '---'
-              do @p

Any element that is commonly displayed in italics as well as U (i.e. underline) + do @p

Any element that is commonly displayed in italics as well as U (i.e. underline) since vanilla Markdown does not support this but the site did try to highlight the contents.

            when 'CITE', 'DFN', 'EM', 'I', 'U', 'VAR'
               @output '_'
               @atNoWS = yes
-              after   = @outputLater '_'

Any element that is commonly display in bold.

            when 'DT', 'B', 'STRONG'
+              after   = @outputLater '_'

Any element that is commonly display in bold.

            when 'DT', 'B', 'STRONG'
               do @p if ele.tagName is 'DT'
 
               @output '**'
               @atNoWS = yes
-              after   = @outputLater '**'

Uncommon element but just wrap its contens in quotation marks. Job done!

            when 'Q'
+              after   = @outputLater '**'

Uncommon element but just wrap its contens in quotation marks. Job done!

            when 'Q'
               @output '"'
               @atNoWS = yes
-              after   = @outputLater '"'

Lists need their items to be displayed correctly depending on their type while also + after = @outputLater '"'

Lists need their items to be displayed correctly depending on their type while also ensuring nested lists are indented properly.

            when 'OL', 'UL'
-              after = if ele.tagName is 'OL' then do @ol else do @ul

List items are displayed differently depending on what kind of list they're parent -is (i.e. ordered or unordered).

            when 'LI' then do @li

A pre-formatted element just needs to have its contents properly indented.

            when 'PRE'
+              after = if ele.tagName is 'OL' then do @ol else do @ul

List items are displayed differently depending on what kind of list they're parent +is (i.e. ordered or unordered).

            when 'LI' then do @li

A pre-formatted element just needs to have its contents properly indented.

            when 'PRE'
               after1 = @pushLeft '    '
               after2 = do @pre
 
-              after  = ->
+              after  = ->
                 do after1
-                do after2

Inline code elements translate pretty easily but we need to make sure we don't do + do after2

Inline code elements translate pretty easily but we need to make sure we don't do anything dumb inside a pre element.

            when 'CODE', 'KBD', 'SAMP'
               break if @inPre
 
@@ -266,20 +265,20 @@
               after1 = do @code
               after2 = @outputLater '`'
 
-              after  = ->
+              after  = ->
                 do after1
-                do after2

Block quotes (and similar elements) are relatively straight forward.

            when 'BLOCKQUOTE', 'DD' then after = @pushLeft '> '

Links on the other hand are probably the trickiest.

            when 'A'

Extract the link URL from ele.
+ do after2

Block quotes (and similar elements) are relatively straight forward.

            when 'BLOCKQUOTE', 'DD' then after = @pushLeft '> '

Links on the other hand are probably the trickiest.

            when 'A'

Extract the link URL from ele.
Links with no URLs are treated just like text-containing elements (e.g. span).
a.href always returns an absolute URL while a.getAttribute('href') always returns the exact value of the href attribute. For this reason we need to be sure that we extract the URL correctly based on the state of the absolute option.

              href = @attr ele, 'href', @options.absolute
-              break unless href

Be sure to include the title after each link reference that will be displayed at + break unless href

Be sure to include the title after each link reference that will be displayed at the end of the Markdown output, where possible.

              title  = @attr ele, 'title'
-              href  += " \"#{title}\"" if title

Determine what style the link should be generated in (i.e. inline or -reference) depending on the state of the inline option.

              suffix = if @options.inline

Inline style means all links have their URLs (and possible titles) included + href += " \"#{title}\"" if title

Determine what style the link should be generated in (i.e. inline or +reference) depending on the state of the inline option.

              suffix = if @options.inline

Inline style means all links have their URLs (and possible titles) included immediately after their contents (e.g. [my link](/path/to/page.html "My title")).

                "(#{href})"
-              else

Reference style means all links have an index included immediately after their + else

Reference style means all links have an index included immediately after their contents that directly maps to their URL (and possible title) which are displayed at the end of the buffer string (e.g. [my link][0] and then later [0]: /path/to/page.html "My title").
@@ -287,33 +286,33 @@ @output '[' @atNoWS = yes - after = @outputLater "]#{suffix}"

Images are very similar to links, just without the added complexity of references.

            when 'IMG'

Extract the image URL from ele.
+ after = @outputLater "]#{suffix}"

Images are very similar to links, just without the added complexity of references.

            when 'IMG'

Extract the image URL from ele.
Unlike links, any image without a URL is just ignored. Obviously, any contents of an img element are always ignored since they can never contain anything valid.
img.src always returns an absolute URL while img.getAttribute('src') always returns the exact value of the src attribute. For this reason we need to be sure that we extract the URL correctly based on the state of the absolute option.

              skipChildren = yes
               src          = @attr ele, 'src', @options.absolute
-              break unless src
+              break unless src
 
-              @output "![#{@attr ele, 'alt'}](#{src})"

Frames are HELL (fact!), but we'll do our best to support their contents.

            when 'FRAME', 'IFRAME'
+              @output "![#{@attr ele, 'alt'}](#{src})"

Frames are HELL (fact!), but we'll do our best to support their contents.

            when 'FRAME', 'IFRAME'
               skipChildren = yes
 
               try
                 if ele.contentDocument?.documentElement
                   @process ele.contentDocument.documentElement
               catch err
-                @thrown err, 'contentDocument'

Table rows should just be separated, that's all.

            when 'TR' then after = @p

Couldn't find a suitable match for ele so let's ignore it, but we'll still process + @thrown err, 'contentDocument'

Table rows should just be separated, that's all.

            when 'TR' then after = @p

Couldn't find a suitable match for ele so let's ignore it, but we'll still process any children it has.

            else
               @unhandled[ele.tagName] = null if @options.debug
       catch err
-        @thrown err, ele.tagName

Process all child elements of ele if it has any and we've not been told to ignore them.

      @process childNode for childNode in ele.childNodes unless skipChildren

Ensure any callback are invoked being proceeding if they are specified.

      after?.call this
-    else if ele.nodeType is Node.TEXT_NODE

Handle simple text nodes (e.g. "foo bar") according to the current context.

      @output if @inPre
+        @thrown err, ele.tagName

Process all child elements of ele if it has any and we've not been told to ignore them.

      @process childNode for childNode in ele.childNodes unless skipChildren

Ensure any callback are invoked being proceeding if they are specified.

      after?.call this
+    else if ele.nodeType is @win.Node.TEXT_NODE

Handle simple text nodes (e.g. "foo bar") according to the current context.

      @output if @inPre
         ele.nodeValue
       else if @inCode
         @inCodeProcess ele.nodeValue
       else
-        @nonPreProcess ele.nodeValue

Attach str to the start of the current line.

  pushLeft: (str) ->
+        @nonPreProcess ele.nodeValue

Attach str to the start of the current line.

  pushLeft: (str) ->
     old    = @left
     @left += str
 
@@ -323,15 +322,15 @@
       @left   = old
       @atLeft = @atP = no
 
-      do @p

Replace the left indent with str.

  replaceLeft: (str) ->
-    unless @atLeft
+      do @p

Replace the left indent with str.

  replaceLeft: (str) ->
+    unless @atLeft
       @append @left.replace /[ ]{2,4}$/, str
 
       @atLeft = @atNoWS = @atP = yes
     else if @last
-      @last   = @last.replace /[ ]{2,4}$/, str

Ensure that the exception and the corresponding message is logged if the debug option is + @last = @last.replace /[ ]{2,4}$/, str

Ensure that the exception and the corresponding message is logged if the debug option is enabled.

  thrown: (exception, message) ->
-    @exceptions.push "#{message}: #{exception}" if @options.debug

Prepare the parser for a ul element.

  ul: ->
+    @exceptions.push "#{message}: #{exception}" if @options.debug

Prepare the parser for a ul element.

  ul: ->
     do @p if @listDepth is 0
 
     inOrderedList  = @inOrderedList
@@ -343,11 +342,11 @@
     =>
       @inOrderedList = inOrderedList
       @order         = order
-      @listDepth--

html.md setup

Build the publicly exposed API.

@md = md = (html, options) ->
-  new HtmlParser(html, options).parse()

Export md for NodeJS and CommonJS.

if module?.exports
+      @listDepth--

html.md setup

Build the publicly exposed API.

@md = md = (html, options) ->
+  new HtmlParser(html, options).parse()

Export md for NodeJS and CommonJS.

if module?.exports
   module.exports = md
 else if typeof define is 'function' and define.amd
-  define 'md', -> md

Public constants

Current version of html.md.

md.version = md.VERSION = '2.1.0'

Public functions

Run html.md in noConflict mode, returning the md variable to its previous owner.
+ define 'md', -> md

Public constants

Current version of html.md.

md.version = md.VERSION = '2.1.0'

Public functions

Run html.md in noConflict mode, returning the md variable to its previous owner.
Returns a reference to our md.

md.noConflict = =>
   @md = PREVIOUS_MD
   md
diff --git a/lib/command.js b/lib/command.js
index 6b45208..b513ad6 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -31,16 +31,16 @@
     return process.exit(code);
   };
 
-  outputPath = function(source, base) {
+  outputPath = function(source, root) {
     var dir, fileName, srcDir;
 
     fileName = path.basename(source, path.extname(source)) + extension;
     srcDir = path.dirname(source);
-    dir = program.output ? path.join(program.output, base === '.' ? srcDir : srcDir.slice(base.length)) : srcDir;
+    dir = program.output ? path.join(program.output, root === '.' ? srcDir : srcDir.slice(root.length)) : srcDir;
     return path.join(dir, fileName);
   };
 
-  parsePath = function(source, topLevel, base) {
+  parsePath = function(source, topLevel, root) {
     return fs.stat(source, function(err, stats) {
       if (err) {
         if (err.code !== NOT_FOUND) {
@@ -48,7 +48,7 @@
         }
         if (topLevel && !R_HTML_EXT.test(source)) {
           source = sources[sources.indexOf(source)] = "" + source + ".html";
-          parsePath(source, topLevel, base);
+          parsePath(source, topLevel, root);
         } else if (topLevel) {
           exit(1, "File not found: " + source);
         }
@@ -69,7 +69,7 @@
           _results = [];
           for (_i = 0, _len = files.length; _i < _len; _i++) {
             file = files[_i];
-            _results.push(parsePath(file, false, base));
+            _results.push(parsePath(file, false, root));
           }
           return _results;
         });
@@ -78,7 +78,7 @@
           if (err) {
             throw err;
           }
-          return parseHtml(source, html.toString(), base);
+          return parseHtml(source, html.toString(), root);
         });
       } else {
         return sources.splice(sources.indexOf(source), 1);
@@ -86,20 +86,21 @@
     });
   };
 
-  parseHtml = function(file, input, base) {
-    var absolute, debug, err, inline, output;
+  parseHtml = function(file, input, root) {
+    var absolute, base, debug, err, inline, output;
 
     try {
-      absolute = program.absolute, debug = program.debug, inline = program.inline;
+      absolute = program.absolute, base = program.base, debug = program.debug, inline = program.inline;
       output = md(input, {
         absolute: absolute,
+        base: base,
         debug: debug,
         inline: inline
       });
       if (program.print) {
         return console.log(output);
       } else if (file) {
-        return writeMarkdown(file, output, base);
+        return writeMarkdown(file, output, root);
       }
     } catch (_error) {
       err = _error;
@@ -108,7 +109,7 @@
   };
 
   parseOptions = function() {
-    program.version(md.version).usage('Usage: md [options] [ -e html |  ]').option('-a, --absolute', 'always use absolute URLs for links').option('-d, --debug', 'print additional debug information').option('-e, --eval', 'pass a string from the command line as input').option('-i, --inline', 'generate inline style links').option('-l, --long-ext', 'use long extension for Markdown files').option('-o, --output ', 'set the output directory for converted Markdown').option('-p, --print', 'print out the converted Markdown').parse(process.argv);
+    program.version(md.version).usage('Usage: md [options] [ -e html |  ]').option('-a, --absolute', 'always use absolute URLs for links and images').option('-b, --base ', 'set base URL to resolve relative URLs from').option('-d, --debug', 'print additional debug information').option('-e, --eval', 'pass a string from the command line as input').option('-i, --inline', 'generate inline style links').option('-l, --long-ext', 'use long extension for Markdown files').option('-o, --output ', 'set the output directory for converted Markdown').option('-p, --print', 'print out the converted Markdown').parse(process.argv);
     if (program.longExt) {
       extension = '.markdown';
     }
@@ -118,10 +119,10 @@
     }
   };
 
-  writeMarkdown = function(source, markdown, base) {
+  writeMarkdown = function(source, markdown, root) {
     var mdDir, mdPath, write;
 
-    mdPath = outputPath(source, base);
+    mdPath = outputPath(source, root);
     mdDir = path.dirname(mdPath);
     write = function(err) {
       if (err) {
diff --git a/lib/md.js b/lib/md.js
index 2e2ee1b..05f01e8 100644
--- a/lib/md.js
+++ b/lib/md.js
@@ -1,10 +1,11 @@
 (function() {
-  var DEFAULT_OPTIONS, HtmlParser, Node, PREVIOUS_MD, REGEX, REPLACEMENTS, R_HIDDEN_STYLES, R_HIDDEN_VALUE, R_IGNORE_CHILDREN, R_PARAGRAPH_ONLY, doc, jsdom, key, md, padLeft, result, trim, win, _ref, _ref1, _ref2,
+  var DEFAULT_OPTIONS, HtmlParser, PREVIOUS_MD, REGEX, REPLACEMENTS, R_HIDDEN_STYLES, R_HIDDEN_VALUE, R_IGNORE_CHILDREN, R_PARAGRAPH_ONLY, key, md, padLeft, result, trim,
     __hasProp = {}.hasOwnProperty,
     _this = this;
 
   DEFAULT_OPTIONS = {
     absolute: false,
+    base: typeof window !== "undefined" && window !== null ? window.document.baseURI : "file://" + (process.cwd()),
     debug: false,
     inline: false
   };
@@ -55,28 +56,6 @@
     return result;
   })());
 
-  win = typeof window !== "undefined" && window !== null ? window : null;
-
-  if (win == null) {
-    jsdom = require('jsdom');
-    doc = jsdom.jsdom(null, null, {
-      features: {
-        FetchExternalResources: false
-      }
-    });
-    win = doc.createWindow();
-  }
-
-  Node = (_ref = win.Node) != null ? _ref : {};
-
-  if ((_ref1 = Node.ELEMENT_NODE) == null) {
-    Node.ELEMENT_NODE = 1;
-  }
-
-  if ((_ref2 = Node.TEXT_NODE) == null) {
-    Node.TEXT_NODE = 3;
-  }
-
   padLeft = function(str, times, padStr) {
     var i, _i;
 
@@ -111,7 +90,7 @@
 
   HtmlParser = (function() {
     function HtmlParser(html, options) {
-      var defaultValue;
+      var defaultValue, doc;
 
       this.html = html != null ? html : '';
       this.options = options != null ? options : {};
@@ -136,6 +115,16 @@
           this.options[key] = defaultValue;
         }
       }
+      this.win = typeof window !== "undefined" && window !== null ? window : null;
+      if (this.win == null) {
+        doc = require('jsdom').jsdom(null, null, {
+          features: {
+            FetchExternalResources: false
+          },
+          url: this.options.base
+        });
+        this.win = doc.createWindow();
+      }
     }
 
     HtmlParser.prototype.append = function(str) {
@@ -198,9 +187,9 @@
           visible = !R_HIDDEN_VALUE.test(property);
         }
       }
-      if (visible && typeof win.getComputedStyle === 'function') {
+      if (visible && typeof this.win.getComputedStyle === 'function') {
         try {
-          style = win.getComputedStyle(ele, null);
+          style = this.win.getComputedStyle(ele, null);
           if (typeof (style != null ? style.getPropertyValue : void 0) === 'function') {
             display = style.getPropertyValue('display');
             visibility = style.getPropertyValue('visibility');
@@ -292,13 +281,13 @@
     };
 
     HtmlParser.prototype.parse = function() {
-      var container, i, link, tag, unhandledTags, _i, _len, _ref3;
+      var container, i, link, tag, unhandledTags, _i, _len, _ref;
 
       this.buffer = '';
       if (!this.html) {
         return this.buffer;
       }
-      container = win.document.createElement('div');
+      container = this.win.document.createElement('div');
       if (typeof this.html === 'string') {
         container.innerHTML = this.html;
       } else {
@@ -307,9 +296,9 @@
       this.process(container);
       if (this.links.length) {
         this.append('\n\n');
-        _ref3 = this.links;
-        for (i = _i = 0, _len = _ref3.length; _i < _len; i = ++_i) {
-          link = _ref3[i];
+        _ref = this.links;
+        for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+          link = _ref[i];
           if (link) {
             this.append("[" + i + "]: " + link + "\n");
           }
@@ -317,12 +306,12 @@
       }
       if (this.options.debug) {
         unhandledTags = ((function() {
-          var _ref4, _results;
+          var _ref1, _results;
 
-          _ref4 = this.unhandled;
+          _ref1 = this.unhandled;
           _results = [];
-          for (tag in _ref4) {
-            if (!__hasProp.call(_ref4, tag)) continue;
+          for (tag in _ref1) {
+            if (!__hasProp.call(_ref1, tag)) continue;
             _results.push(tag);
           }
           return _results;
@@ -346,12 +335,12 @@
     };
 
     HtmlParser.prototype.process = function(ele) {
-      var after, after1, after2, childNode, err, href, i, level, skipChildren, src, suffix, summary, title, _base, _i, _len, _ref3, _ref4, _ref5;
+      var after, after1, after2, childNode, err, href, i, level, skipChildren, src, suffix, summary, title, _base, _i, _len, _ref, _ref1, _ref2;
 
       if (!this.isVisible(ele)) {
         return;
       }
-      if (ele.nodeType === Node.ELEMENT_NODE) {
+      if (ele.nodeType === this.win.Node.ELEMENT_NODE) {
         skipChildren = false;
         try {
           if (R_IGNORE_CHILDREN.test(ele.tagName)) {
@@ -460,7 +449,7 @@
                 if (title) {
                   href += " \"" + title + "\"";
                 }
-                suffix = this.options.inline ? "(" + href + ")" : "[" + ((_ref3 = (_base = this.linkMap)[href]) != null ? _ref3 : _base[href] = this.links.push(href) - 1) + "]";
+                suffix = this.options.inline ? "(" + href + ")" : "[" + ((_ref = (_base = this.linkMap)[href]) != null ? _ref : _base[href] = this.links.push(href) - 1) + "]";
                 this.output('[');
                 this.atNoWS = true;
                 after = this.outputLater("]" + suffix);
@@ -477,7 +466,7 @@
               case 'IFRAME':
                 skipChildren = true;
                 try {
-                  if ((_ref4 = ele.contentDocument) != null ? _ref4.documentElement : void 0) {
+                  if ((_ref1 = ele.contentDocument) != null ? _ref1.documentElement : void 0) {
                     this.process(ele.contentDocument.documentElement);
                   }
                 } catch (_error) {
@@ -499,14 +488,14 @@
           this.thrown(err, ele.tagName);
         }
         if (!skipChildren) {
-          _ref5 = ele.childNodes;
-          for (_i = 0, _len = _ref5.length; _i < _len; _i++) {
-            childNode = _ref5[_i];
+          _ref2 = ele.childNodes;
+          for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
+            childNode = _ref2[_i];
             this.process(childNode);
           }
         }
         return after != null ? after.call(this) : void 0;
-      } else if (ele.nodeType === Node.TEXT_NODE) {
+      } else if (ele.nodeType === this.win.Node.TEXT_NODE) {
         return this.output(this.inPre ? ele.nodeValue : this.inCode ? this.inCodeProcess(ele.nodeValue) : this.nonPreProcess(ele.nodeValue));
       }
     };
diff --git a/man/md.1 b/man/md.1
index ce2d84e..9f766ce 100644
--- a/man/md.1
+++ b/man/md.1
@@ -15,6 +15,10 @@
 .I output_dir
 ]
 [
+.B \-b
+.I base_url
+]
+[
 .I paths
 ]
 .br
@@ -40,7 +44,15 @@ systems to provide a complete command line utility.
 
 .TP 15
 .B \-a/\-\-absolute
-Always use absolute URLs for converted links.
+Always use absolute URLs for converted links and
+images.
+
+.HP
+.B \-b/\-\-base
+.I url
+.br
+Specifies the base URL to resolve relative URLs
+from.
 
 .TP
 .B \-d/\-\-debug
diff --git a/src/command.coffee b/src/command.coffee
index 43ddf43..2917b46 100644
--- a/src/command.coffee
+++ b/src/command.coffee
@@ -46,11 +46,11 @@ exit = (code, message) ->
   process.exit code
 
 # Resolve the output path for `source`.
-outputPath = (source, base) ->
+outputPath = (source, root) ->
   fileName = path.basename(source, path.extname(source)) + extension
   srcDir   = path.dirname source
   dir      = if program.output
-    path.join program.output, if base is '.' then srcDir else srcDir[base.length..]
+    path.join program.output, if root is '.' then srcDir else srcDir[root.length..]
   else
     srcDir
 
@@ -58,7 +58,7 @@ outputPath = (source, base) ->
 
 # Attempt to either parse the contents of the `source` path or, if it's a directory, the contents
 # of its children HTML files.
-parsePath = (source, topLevel, base) ->
+parsePath = (source, topLevel, root) ->
   fs.stat source, (err, stats) ->
     if err
       # We only care about `ENOENT` errors so everything else is thrown.
@@ -67,7 +67,7 @@ parsePath = (source, topLevel, base) ->
       # searchability and trying again.
       if topLevel and not R_HTML_EXT.test source
         source = sources[sources.indexOf(source)] = "#{source}.html"
-        parsePath source, topLevel, base
+        parsePath source, topLevel, root
       else if topLevel
         exit 1, "File not found: #{source}"
       return
@@ -87,31 +87,31 @@ parsePath = (source, topLevel, base) ->
         sources[index..index] = files
 
         # Now finally try to parse all of the directory `files`.
-        parsePath file, no, base for file in files
+        parsePath file, no, root for file in files
     else if topLevel or R_HTML_EXT.test source
       # Possible source file found so read its contents.
       fs.readFile source, (err, html) ->
         throw err if err
 
         # Treat the files contents as HTML and try to parse it using `md`.
-        parseHtml source, html.toString(), base
+        parseHtml source, html.toString(), root
     else
       # Doesn't appear to have a recognizable HTML file extension so just ignore the file and
       # remove it from the list of `sources`.
       sources.splice sources.indexOf(source), 1
 
 # Parse the HTML `input` and write it out accordingly.
-parseHtml = (file, input, base) ->
+parseHtml = (file, input, root) ->
   try
     # Extract only relevant options from `program`.
-    {absolute, debug, inline} = program
+    {absolute, base,  debug, inline} = program
     # Let `md` work its magic on the HTML `input`.
-    output                    = md input, {absolute, debug, inline}
+    output                           = md input, {absolute, base, debug, inline}
 
     # Either write the output to `stdout` or to a corresponding Markdown file depending on whether
     # the `print` option was enabled.
     if program.print then console.log output
-    else if file     then writeMarkdown file, output, base
+    else if file     then writeMarkdown file, output, root
   catch err
     # An error occured while parsing `input`, so stop processing all sources and exit, while also
     # letting the user know why.
@@ -123,7 +123,8 @@ parseOptions = ->
   program
     .version(md.version)
     .usage('Usage: md [options] [ -e html |  ]')
-    .option('-a, --absolute',     'always use absolute URLs for links')
+    .option('-a, --absolute',     'always use absolute URLs for links and images')
+    .option('-b, --base ',   'set base URL to resolve relative URLs from')
     .option('-d, --debug',        'print additional debug information')
     .option('-e, --eval',         'pass a string from the command line as input')
     .option('-i, --inline',       'generate inline style links')
@@ -144,9 +145,9 @@ parseOptions = ->
   program.help() unless sources.length
 
 # Write the `markdown` converted from `source` to its relative output file.
-writeMarkdown = (source, markdown, base) ->
+writeMarkdown = (source, markdown, root) ->
   # Derive the best output file path based on the name of the `source` file.
-  mdPath = outputPath source, base
+  mdPath = outputPath source, root
   mdDir  = path.dirname mdPath
 
   # Write `markdown` to its corresponding output file.
diff --git a/src/md.coffee b/src/md.coffee
index dc9f17d..c7c0a86 100644
--- a/src/md.coffee
+++ b/src/md.coffee
@@ -12,6 +12,7 @@
 # Default option values.
 DEFAULT_OPTIONS   =
   absolute: no
+  base:     if window? then window.document.baseURI else "file://#{process.cwd()}"
   debug:    no
   inline:   no
 # Save the previous value of the global `md` variable for *noConflict* mode.
@@ -103,22 +104,6 @@ REGEX             = (
   result
 )
 
-# Environment Support
-# -------------------
-
-# Create a DOM if `window` doesn't exist (i.e. when running in node).
-win = window ? null
-unless win?
-  jsdom = require 'jsdom'
-  doc   = jsdom.jsdom null, null, features: FetchExternalResources: no
-  win   = doc.createWindow()
-
-# Try to ensure `Node` is available with the required constants. Probably not required; more of a
-# sanity check.
-Node = win.Node ? {}
-Node.ELEMENT_NODE ?= 1
-Node.TEXT_NODE    ?= 3
-
 # Helper functions
 # ----------------
 
@@ -160,6 +145,14 @@ class HtmlParser
     for own key, defaultValue of DEFAULT_OPTIONS
       @options[key] = defaultValue if typeof @options[key] is 'undefined'
 
+    # Create a DOM if `window` doesn't exist (i.e. when running in node).
+    @win = window ? null
+    unless @win?
+      doc  = require('jsdom').jsdom null, null,
+        features: FetchExternalResources: no
+        url:      @options.base
+      @win = doc.createWindow()
+
   # Append `str` to the buffer string.
   append: (str) ->
     @buffer += @last if @last?
@@ -209,9 +202,9 @@ class HtmlParser
       visible = not R_HIDDEN_VALUE.test property for property in properties
 
     # Attempt to derive elements visibility based on its computed CSS style where appropriate.
-    if visible and typeof win.getComputedStyle is 'function'
+    if visible and typeof @win.getComputedStyle is 'function'
       try
-        style = win.getComputedStyle ele, null
+        style = @win.getComputedStyle ele, null
 
         if typeof style?.getPropertyValue is 'function'
           display    = style.getPropertyValue 'display'
@@ -297,7 +290,7 @@ class HtmlParser
     return @buffer unless @html
 
     # Create a wrapper element to insert the configured HTML into.
-    container = win.document.createElement 'div'
+    container = @win.document.createElement 'div'
     if typeof @html is 'string'
       container.innerHTML = @html
     else
@@ -349,7 +342,7 @@ class HtmlParser
     # Only *visible* elements are processed. Doing our best to identify those that are hidden.
     return unless @isVisible ele
 
-    if ele.nodeType is Node.ELEMENT_NODE
+    if ele.nodeType is @win.Node.ELEMENT_NODE
       # Handle typical node elements (e.g. `foo bar`).
       skipChildren = no
 
@@ -505,7 +498,7 @@ class HtmlParser
 
       # Ensure any callback are invoked being proceeding **if** they are specified.
       after?.call this
-    else if ele.nodeType is Node.TEXT_NODE
+    else if ele.nodeType is @win.Node.TEXT_NODE
       # Handle simple text nodes (e.g. `"foo bar"`) according to the current context.
       @output if @inPre
         ele.nodeValue
diff --git a/test/.gitignore b/test/.gitignore
deleted file mode 100644
index ea1472e..0000000
--- a/test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-output/
diff --git a/test/cli_test.coffee b/test/cli_test.coffee
index eafe236..18b4b2b 100755
--- a/test/cli_test.coffee
+++ b/test/cli_test.coffee
@@ -11,11 +11,12 @@ path   = require 'path'
 
 COMMAND      = './bin/md'
 ENCODING     = 'utf8'
+EXPECTED_DIR = path.join __dirname, 'expected'
 FIXTURES_DIR = path.join __dirname, 'fixtures'
 HTML_EXT     = '.html'
 MD_EXT       = '.md'
 MD_FULL_EXT  = '.markdown'
-OUTPUT_DIR   = path.join __dirname, 'output'
+OUTPUT_DIR   = 'tmp'
 USAGE        = """
 
   Usage: md Usage: md [options] [ -e html |  ]
@@ -24,7 +25,8 @@ USAGE        = """
 
     -h, --help          output usage information
     -V, --version       output the version number
-    -a, --absolute      always use absolute URLs for links
+    -a, --absolute      always use absolute URLs for links and images
+    -b, --base     set base URL to resolve relative URLs from
     -d, --debug         print additional debug information
     -e, --eval          pass a string from the command line as input
     -i, --inline        generate inline style links
@@ -46,7 +48,7 @@ toFileUrl = (relativePath) ->
   "file://#{toPathName relativePath}"
 
 toPathName = (relativePath) ->
-  pathName = path.resolve('lib', relativePath).replace ///\\///g, '/'
+  pathName = path.resolve(process.cwd(), '..', relativePath).replace ///\\///g, '/'
   if pathName[0] isnt '/' then "/#{pathName}" else pathName
 
 # Tests
@@ -55,7 +57,7 @@ toPathName = (relativePath) ->
 exports.fixtures = do ->
   testFixture = (name) ->
     htmlPath = path.join FIXTURES_DIR, "#{name}#{HTML_EXT}"
-    expected = md fs.readFileSync htmlPath, ENCODING
+    expected = fs.readFileSync path.join(EXPECTED_DIR, "#{name}#{MD_EXT}"), ENCODING
 
     standard: (test) ->
       test.expect 2
@@ -97,15 +99,9 @@ exports.fixtures = do ->
   )
 
   tests =
-    tearDown: (callback) ->
-      return do callback unless fs.existsSync OUTPUT_DIR
-
-      fs.readdirSync(OUTPUT_DIR).forEach (file) ->
-        fs.unlinkSync path.join OUTPUT_DIR, file
-
-      fs.rmdirSync OUTPUT_DIR
-
-      do callback
+    setUp: (callback) ->
+      fs.exists OUTPUT_DIR, (exists) ->
+        if exists then do callback else fs.mkdir OUTPUT_DIR, callback
 
   tests[fixture] = testFixture fixture for fixture in fixtures
   tests
@@ -159,6 +155,61 @@ exports.absolute = do ->
 
   """, 'Image should be absolute', '-epa')
 
+exports.base = do ->
+  testBase = (command, expected, desc, flags) ->
+    (test) ->
+      test.expect 2
+
+      exec command, (err, stdout) ->
+        test.ifError err, "Error should not be thrown using '#{flags}' flags"
+        test.equal stdout, expected, "#{desc} using #{flags} flags"
+
+        test.done()
+
+  defaultLink: testBase("#{COMMAND} -epa \"anchor\"", """
+    [anchor][0]
+
+    [0]: #{toFileUrl 'mock'}
+
+  """, 'Link should be relative to the current working directory', '-epa')
+
+  defaultRootLink: testBase("#{COMMAND} -epa \"anchor\"", """
+    [anchor][0]
+
+    [0]: #{toFileUrl '/mock'}
+
+  """, 'Root link should be relative to the current working directory', '-epa')
+
+  defaultImage: testBase("#{COMMAND} -epa \"\"", """
+    ![](#{toFileUrl 'mock'})
+
+  """, 'Image should be relative to the current working directory', '-epa')
+
+  baseLink: testBase("""
+    #{COMMAND} -epab "http://example.com/path/to/page/" "anchor"
+  """, """
+    [anchor][0]
+
+    [0]: http://example.com/path/to/page/mock
+
+  """, 'Link should be relative to custom URL', '-epab')
+
+  baseRootLink: testBase("""
+      #{COMMAND} -epab "http://example.com/path/to/page/" "anchor"
+  """, """
+    [anchor][0]
+
+    [0]: http://example.com/mock
+
+  """, 'Link should be relative to custom URL', '-epab')
+
+  baseImage: testBase("""
+    #{COMMAND} -epab "http://example.com/path/to/page/" ""
+  """, """
+    ![](http://example.com/path/to/page/mock)
+
+  """, 'Image should be relative to custom URL', '-epab')
+
 exports.inline = do ->
   testInline = (command, expected, desc, flags) ->
     (test) ->
diff --git a/test/core_test.coffee b/test/core_test.coffee
index f67d1f9..5e1b683 100755
--- a/test/core_test.coffee
+++ b/test/core_test.coffee
@@ -9,6 +9,7 @@ path = require 'path'
 # ---------
 
 ENCODING     = 'utf8'
+EXPECTED_DIR = path.join __dirname, 'expected'
 FIXTURES_DIR = path.join __dirname, 'fixtures'
 HTML_EXT     = '.html'
 
@@ -22,7 +23,7 @@ toFileUrl = (relativePath) ->
   "file://#{toPathName relativePath}"
 
 toPathName = (relativePath) ->
-  pathName = path.resolve('lib', relativePath).replace ///\\///g, '/'
+  pathName = path.resolve(process.cwd(), '..', relativePath).replace ///\\///g, '/'
   if pathName[0] isnt '/' then "/#{pathName}" else pathName
 
 # Tests
@@ -32,7 +33,7 @@ exports.fixtures = (
   testFixture = (name) ->
     (test) ->
       html     = fs.readFileSync path.join(FIXTURES_DIR, "#{name}.html"), ENCODING
-      markdown = fs.readFileSync path.join(FIXTURES_DIR, "#{name}.md"),   ENCODING
+      markdown = fs.readFileSync path.join(EXPECTED_DIR, "#{name}.md"),   ENCODING
 
       test.equal md(html), markdown, "#{name} fixtures should match"
       test.done()
@@ -82,6 +83,42 @@ exports.options =
 
     test.done()
 
+  base: (test) ->
+    absolute = on
+    base     = 'http://example.com/path/to/page/'
+
+    test.equal md('anchor', {absolute}), """
+      [anchor][0]
+
+      [0]: #{toFileUrl 'mock'}
+    """, 'Link should be relative to the current working directory'
+
+    test.equal md('anchor', {absolute}), """
+      [anchor][0]
+
+      [0]: #{toFileUrl '/mock'}
+    """, 'Root link should be relative to the current working directory'
+
+    test.equal md('', {absolute}), "![](#{toFileUrl 'mock'})",
+      'Image should be relative to the current working directory'
+
+    test.equal md('anchor', {absolute, base}), """
+      [anchor][0]
+
+      [0]: #{base}mock
+    """, 'Link should be relative to custom URL'
+
+    test.equal md('anchor', {absolute, base}), """
+      [anchor][0]
+
+      [0]: http://example.com/mock
+    """, 'Root link should be relative to custom URL'
+
+    test.equal md('', {absolute, base}), "![](#{base}mock)",
+      'Image should be relative to custom URL'
+
+    test.done()
+
   inline: (test) ->
     options = inline: on
 
diff --git a/test/fixtures/content.md b/test/expected/content.md
similarity index 100%
rename from test/fixtures/content.md
rename to test/expected/content.md
diff --git a/test/fixtures/lists.md b/test/expected/lists.md
similarity index 100%
rename from test/fixtures/lists.md
rename to test/expected/lists.md
diff --git a/test/fixtures/scaffolding.md b/test/expected/scaffolding.md
similarity index 100%
rename from test/fixtures/scaffolding.md
rename to test/expected/scaffolding.md
diff --git a/test/fixtures/tables.md b/test/expected/tables.md
similarity index 100%
rename from test/fixtures/tables.md
rename to test/expected/tables.md
diff --git a/test/fixtures/visibility.md b/test/expected/visibility.md
similarity index 100%
rename from test/fixtures/visibility.md
rename to test/expected/visibility.md