Repository: mkdocs/mkdocs

Issue:
URL are changed in <script src> from code blocks 
I face a really strange issue.

Please see [this file](https://github.com/angular-gantt/angular-gantt/blob/v1.0.0-rc5/docs/get-started.md), and notice in **Install Manually** section, step **4**, in the sample code.

```
<script src="assets/angular-gantt.js"></script>
```

When i generate the docs using `mkdocs build`, it gives the [following page](http://www.angular-gantt.com/get-started/), and the script source is changed : 

```
<script src="../assets/angular-gantt.js"></script>
```

Notice the `../` added to the begining of file. This may have sense for real `<script src...>` tag, but it should not occur inside a code block (4-space indent or quoted)

[Page generated by readthedocs](http://angular-gantt.readthedocs.org/en/latest/get-started/) has the same issue.



Discussion:

Resolving patch:
diff --git a/mkdocs/build.py b/mkdocs/build.py
--- a/mkdocs/build.py
+++ b/mkdocs/build.py
@@ -3,51 +3,15 @@
 
 from jinja2.exceptions import TemplateNotFound
 from mkdocs import nav, toc, utils
-from mkdocs.compat import urljoin, urlparse, urlunparse, PY2
+from mkdocs.compat import urljoin, PY2
+from mkdocs.relative_path_ext import RelativePathExtension
 import jinja2
 import json
 import markdown
 import os
-import re
-
-
-class PathToURL(object):
-    def __init__(self, template, nav=None):
-        self.template = template
-        self.nav = nav
-
-    def __call__(self, match):
-        url = match.groups()[0]
-        scheme, netloc, path, query, query, fragment = urlparse(url)
-
-        if scheme or netloc or not path:
-            # Ignore URLs unless they are a relative link to a markdown file.
-            return self.template % url
-
-        if self.nav and not utils.is_markdown_file(path):
-            path = utils.create_media_urls(self.nav, [path])[0]
-        elif self.nav:
-            # If the site navigation has been provided, then validate
-            # the internal hyperlink, making sure the target actually exists.
-            target_file = self.nav.file_context.make_absolute(path)
-            if target_file not in self.nav.source_files:
-                source_file = self.nav.file_context.current_file
-                msg = (
-                    'The page "%s" contained a hyperlink to "%s" which '
-                    'is not listed in the "pages" configuration.'
-                )
-                assert False, msg % (source_file, target_file)
-            path = utils.get_url_path(target_file, self.nav.use_directory_urls)
-            path = self.nav.url_context.make_relative(path)
-        else:
-            path = utils.get_url_path(path).lstrip('/')
-
-        # Convert the .md hyperlink to a relative hyperlink to the HTML page.
-        url = urlunparse((scheme, netloc, path, query, query, fragment))
-        return self.template % url
 
 
-def convert_markdown(markdown_source, extensions=()):
+def convert_markdown(markdown_source, site_navigation=None, extensions=()):
     """
     Convert the Markdown source file to HTML content, and additionally
     return the parsed table of contents, and a dictionary of any metadata
@@ -61,8 +25,11 @@ def convert_markdown(markdown_source, extensions=()):
     markdown_source = toc.pre_process(markdown_source)
 
     # Generate the HTML from the markdown source
+    builtin_extensions = ['meta', 'toc', 'tables', 'fenced_code']
+    mkdocs_extensions = [RelativePathExtension(site_navigation), ]
+    extensions = builtin_extensions + mkdocs_extensions + list(extensions)
     md = markdown.Markdown(
-        extensions=['meta', 'toc', 'tables', 'fenced_code'] + list(extensions)
+        extensions=extensions
     )
     html_content = md.convert(markdown_source)
     meta = md.Meta
@@ -76,17 +43,6 @@ def convert_markdown(markdown_source, extensions=()):
     return (html_content, table_of_contents, meta)
 
 
-def post_process_html(html_content, nav=None):
-
-    anchor_sub = PathToURL('a href="%s"', nav)
-    html_content = re.sub(r'a href="([^"]*)"', anchor_sub, html_content)
-
-    img_sub = PathToURL('src="%s"', nav)
-    html_content = re.sub(r'src="([^"]*)"', img_sub, html_content)
-
-    return html_content
-
-
 def get_global_context(nav, config):
     """
     Given the SiteNavigation and config, generate the context which is relevant
@@ -209,9 +165,9 @@ def build_pages(config, dump_json=False):
 
         # Process the markdown text
         html_content, table_of_contents, meta = convert_markdown(
-            input_content, extensions=config['markdown_extensions']
+            input_content, site_navigation,
+            extensions=config['markdown_extensions']
         )
-        html_content = post_process_html(html_content, site_navigation)
 
         context = get_global_context(site_navigation, config)
         context.update(get_page_context(
diff --git a/mkdocs/relative_path_ext.py b/mkdocs/relative_path_ext.py
new file mode 100644
--- /dev/null
+++ b/mkdocs/relative_path_ext.py
@@ -0,0 +1,120 @@
+"""
+# Relative Path Markdown Extension
+
+During the MkDocs build we rewrite URLs that link to local
+Markdown or media files. Using the following pages configuration
+we can look at how the output is changed.
+
+    pages:
+    - ['index.md']
+    - ['tutorial/install.md']
+    - ['tutorial/intro.md']
+
+## Markdown URLs
+
+When linking from `install.md` to `intro.md` the link would
+simply be `[intro](intro.md)`. However, when we build
+`install.md` we place it in a directory to create nicer URLs.
+This means that the path to `intro.md` becomes `../intro/`
+
+## Media URLs
+
+To make it easier to work with media files and store them all
+under one directory we re-write those to all be based on the
+root. So, with the following markdown to add an image.
+
+    ![The initial MkDocs layout](img/initial-layout.png)
+
+The output would depend on the location of the Markdown file it
+was added too.
+
+Source file         | Generated Path    | Image Path                   |
+------------------- | ----------------- | ---------------------------- |
+index.md            | /                 | ./img/initial-layout.png     |
+tutorial/install.md | tutorial/install/ | ../img/initial-layout.png    |
+tutorial/intro.md   | tutorial/intro/   | ../../img/initial-layout.png |
+
+"""
+from markdown.extensions import Extension
+from markdown.treeprocessors import Treeprocessor
+
+from mkdocs import utils
+from mkdocs.compat import urlparse, urlunparse
+
+
+def _iter(node):
+    # TODO: Remove when dropping Python 2.6. Replace this
+    # function call with note.iter()
+    return [node] + node.findall('.//*')
+
+
+def path_to_url(url, nav):
+    scheme, netloc, path, query, query, fragment = urlparse(url)
+
+    if scheme or netloc or not path:
+        # Ignore URLs unless they are a relative link to a markdown file.
+        return url
+
+    if nav and not utils.is_markdown_file(path):
+        path = utils.create_media_urls(nav, [path])[0]
+    elif nav:
+        # If the site navigation has been provided, then validate
+        # the internal hyperlink, making sure the target actually exists.
+        target_file = nav.file_context.make_absolute(path)
+        if target_file not in nav.source_files:
+            source_file = nav.file_context.current_file
+            msg = (
+                'The page "%s" contained a hyperlink to "%s" which '
+                'is not listed in the "pages" configuration.'
+            )
+            assert False, msg % (source_file, target_file)
+        path = utils.get_url_path(target_file, nav.use_directory_urls)
+        path = nav.url_context.make_relative(path)
+    else:
+        path = utils.get_url_path(path).lstrip('/')
+
+    # Convert the .md hyperlink to a relative hyperlink to the HTML page.
+    url = urlunparse((scheme, netloc, path, query, query, fragment))
+    return url
+
+
+class RelativePathTreeprocessor(Treeprocessor):
+
+    def __init__(self, site_navigation):
+        self.site_navigation = site_navigation
+
+    def run(self, root):
+        """Update urls on anchors and images to make them relative
+
+        Iterates through the full document tree looking for specific
+        tags and then makes them relative based on the site navigation
+        """
+
+        for element in _iter(root):
+
+            if element.tag == 'a':
+                key = 'href'
+            elif element.tag == 'img':
+                key = 'src'
+            else:
+                continue
+
+            url = element.get(key)
+            new_url = path_to_url(url, self.site_navigation)
+            element.set(key, new_url)
+
+        return root
+
+
+class RelativePathExtension(Extension):
+    """
+    The Extension class is what we pass to markdown, it then
+    registers the Treeprocessor.
+    """
+
+    def __init__(self, site_navigation):
+        self.site_navigation = site_navigation
+
+    def extendMarkdown(self, md, md_globals):
+        relpath = RelativePathTreeprocessor(self.site_navigation)
+        md.treeprocessors.add("relpath", relpath, "_end")


Test patch:
diff --git a/mkdocs/test.py b/mkdocs/test.py
--- a/mkdocs/test.py
+++ b/mkdocs/test.py
@@ -442,28 +442,24 @@ def test_convert_internal_link(self):
         md_text = 'An [internal link](internal.md) to another document.'
         expected = '<p>An <a href="internal/">internal link</a> to another document.</p>'
         html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_convert_multiple_internal_links(self):
         md_text = '[First link](first.md) [second link](second.md).'
         expected = '<p><a href="first/">First link</a> <a href="second/">second link</a>.</p>'
         html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_convert_internal_link_differing_directory(self):
         md_text = 'An [internal link](../internal.md) to another document.'
         expected = '<p>An <a href="../internal/">internal link</a> to another document.</p>'
         html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_convert_internal_link_with_anchor(self):
         md_text = 'An [internal link](internal.md#section1.1) to another document.'
         expected = '<p>An <a href="internal/#section1.1">internal link</a> to another document.</p>'
         html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_convert_internal_media(self):
@@ -476,16 +472,36 @@ def test_convert_internal_media(self):
         site_navigation = nav.SiteNavigation(pages)
 
         expected_results = (
-            '<p><img alt="The initial MkDocs layout" src="./img/initial-layout.png" /></p>',
-            '<p><img alt="The initial MkDocs layout" src="../img/initial-layout.png" /></p>',
-            '<p><img alt="The initial MkDocs layout" src="../../img/initial-layout.png" /></p>',
+            '<p><img alt="The initial MkDocs layout" src="./img/initial-layout.png" /></p>\n',
+            '<p><img alt="The initial MkDocs layout" src="../img/initial-layout.png" /></p>\n',
+            '<p><img alt="The initial MkDocs layout" src="../../img/initial-layout.png" /></p>\n',
         )
 
         for (page, expected) in zip(site_navigation.walk_pages(), expected_results):
-            html = '<p><img alt="The initial MkDocs layout" src="img/initial-layout.png" /></p>'
-            html = build.post_process_html(html, site_navigation)
+            md_text = '![The initial MkDocs layout](img/initial-layout.png)'
+            html, _, _ = build.convert_markdown(md_text, site_navigation=site_navigation)
             self.assertEqual(html, expected)
 
+    def test_dont_convert_code_block_urls(self):
+        pages = [
+            ('index.md',),
+            ('internal.md',),
+            ('sub/internal.md')
+        ]
+
+        site_navigation = nav.SiteNavigation(pages)
+
+        expected = dedent("""
+        <p>An HTML Anchor::</p>
+        <pre><code>&lt;a href="index.md"&gt;My example link&lt;/a&gt;
+        </code></pre>
+        """)
+
+        for page in site_navigation.walk_pages():
+            markdown = 'An HTML Anchor::\n\n    <a href="index.md">My example link</a>\n'
+            html, _, _ = build.convert_markdown(markdown, site_navigation=site_navigation)
+            self.assertEqual(dedent(html), expected)
+
     def test_anchor_only_link(self):
 
         pages = [
@@ -497,15 +513,14 @@ def test_anchor_only_link(self):
         site_navigation = nav.SiteNavigation(pages)
 
         for page in site_navigation.walk_pages():
-            html = '<p><a href="#test"> </a></p>'
-            result = build.post_process_html(html, site_navigation)
-            self.assertEqual(result, html)
+            markdown = '[test](#test)'
+            html, _, _ = build.convert_markdown(markdown, site_navigation=site_navigation)
+            self.assertEqual(html, '<p><a href="#test">test</a></p>\n')
 
     def test_ignore_external_link(self):
         md_text = 'An [external link](http://example.com/external.md).'
         expected = '<p>An <a href="http://example.com/external.md">external link</a>.</p>'
         html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_not_use_directory_urls(self):
@@ -515,8 +530,7 @@ def test_not_use_directory_urls(self):
             ('internal.md',)
         ]
         site_navigation = nav.SiteNavigation(pages, use_directory_urls=False)
-        html, toc, meta = build.convert_markdown(md_text)
-        html = build.post_process_html(html, site_navigation)
+        html, toc, meta = build.convert_markdown(md_text, site_navigation=site_navigation)
         self.assertEqual(html.strip(), expected.strip())
 
     def test_markdown_table_extension(self):
@@ -585,7 +599,7 @@ def test_markdown_custom_extension(self):
 
         # Check that the plugin is active when requested.
         expected_with_smartstrong = "<p>foo__bar__baz</p>"
-        html_ext, _, _ = build.convert_markdown(md_input, ['smart_strong'])
+        html_ext, _, _ = build.convert_markdown(md_input, extensions=['smart_strong'])
         self.assertEqual(html_ext.strip(), expected_with_smartstrong)
 
     def test_markdown_duplicate_custom_extension(self):