Repository: mkdocs/mkdocs

Issue:
relative url start with "../" in nav no longer works
When I tried to make a link to an external resource in `nav` section of mkdos.yml, using the following example, the actual output HTML omits "../" in the relative URL and I couldn't point to external resources out of mkdocs with relative URL.
https://www.mkdocs.org/user-guide/configuration/#nav

The minimal mkdocs.yml to reproduce the problem is as follows:
```yaml
site_name: Example
site_url: https://example.com/foo/

nav:
    - Parent: '../'
    - Sibling: '../some/dir/'
    - Home: './'
    - Index: 'index.md'
```
When I compile it with empty docs/index.md file, the navigation section in site/index.html became as follows:
```html
<!-- Main navigation -->
<ul class="nav navbar-nav">
    <li class="navitem">
        <a href="./" class="nav-link">Parent</a>
    </li>
    <li class="navitem">
        <a href="some/dir/" class="nav-link">Sibling</a>
    </li>
    <li class="navitem">
        <a href="./" class="nav-link">Home</a>
    </li>
    <li class="navitem active">
        <a href="." class="nav-link">Index</a>
    </li>
</ul>
```
What I expect was that the link of "Parent" should point to "../"(= https://example.com/) and the link of Sibling should point to "../some/dir"(=https://example.com/some/dir/), which are outside of the generated doc.

I remember this way (Sibling case) worked in past, but when I recompiled the document with the latest mkdocs,  it started emitting unexpected URL.

Discussion:

Patch:
diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py
index e3ed45a7..dbef68a5 100644
--- a/mkdocs/structure/files.py
+++ b/mkdocs/structure/files.py
@@ -38,6 +38,7 @@ class Files:
     def __init__(self, files: List[File]) -> None:
         self._files = files
         self._src_uris: Optional[Dict[str, File]] = None
+        self._documentation_pages: Optional[Sequence[File]] = None
 
     def __iter__(self) -> Iterator[File]:
         """Iterate over the files within."""
@@ -70,12 +71,12 @@ class Files:
 
     def append(self, file: File) -> None:
         """Append file to Files collection."""
-        self._src_uris = None
+        self._src_uris = self._documentation_pages = None
         self._files.append(file)
 
     def remove(self, file: File) -> None:
         """Remove file from Files collection."""
-        self._src_uris = None
+        self._src_uris = self._documentation_pages = None
         self._files.remove(file)
 
     def copy_static_files(self, dirty: bool = False) -> None:
@@ -86,7 +87,9 @@ class Files:
 
     def documentation_pages(self) -> Sequence[File]:
         """Return iterable of all Markdown page file objects."""
-        return [file for file in self if file.is_documentation_page()]
+        if self._documentation_pages is None:
+            self._documentation_pages = [file for file in self if file.is_documentation_page()]
+        return self._documentation_pages
 
     def static_pages(self) -> Sequence[File]:
         """Return iterable of all static page file objects."""
@@ -183,14 +186,24 @@ class File:
 
     page: Optional[Page]
 
-    def __init__(self, path: str, src_dir: str, dest_dir: str, use_directory_urls: bool) -> None:
+    def __init__(
+        self,
+        path: str,
+        src_dir: str,
+        dest_dir: str,
+        use_directory_urls: bool,
+        *,
+        dest_uri: Optional[str] = None,
+    ) -> None:
         self.page = None
         self.src_path = path
-        self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_path))
         self.name = self._get_stem()
-        self.dest_uri = self._get_dest_path(use_directory_urls)
-        self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_path))
+        if dest_uri is None:
+            dest_uri = self._get_dest_path(use_directory_urls)
+        self.dest_uri = dest_uri
         self.url = self._get_url(use_directory_urls)
+        self.abs_src_path = os.path.normpath(os.path.join(src_dir, self.src_uri))
+        self.abs_dest_path = os.path.normpath(os.path.join(dest_dir, self.dest_uri))
 
     def __eq__(self, other) -> bool:
         return (
@@ -207,10 +220,10 @@ class File:
         )
 
     def _get_stem(self) -> str:
-        """Return the name of the file without it's extension."""
+        """Return the name of the file without its extension."""
         filename = posixpath.basename(self.src_uri)
         stem, ext = posixpath.splitext(filename)
-        return 'index' if stem in ('index', 'README') else stem
+        return 'index' if stem == 'README' else stem
 
     def _get_dest_path(self, use_directory_urls: bool) -> str:
         """Return destination path based on source path."""
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
index 6592495b..9330a846 100644
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -88,7 +88,10 @@ class Page:
     @property
     def url(self) -> str:
         """The URL of the page relative to the MkDocs `site_dir`."""
-        return '' if self.file.url in ('.', './') else self.file.url
+        url = self.file.url
+        if url in ('.', './'):
+            return ''
+        return url
 
     file: File
     """The documentation [`File`][mkdocs.structure.files.File] that the page is being rendered from."""
diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py
index 8fa39ff5..abc6647d 100644
--- a/mkdocs/utils/__init__.py
+++ b/mkdocs/utils/__init__.py
@@ -297,16 +297,20 @@ def get_relative_url(url: str, other: str) -> str:
 
 def normalize_url(path: str, page: Optional[Page] = None, base: str = '') -> str:
     """Return a URL relative to the given page or using the base."""
-    path, is_abs = _get_norm_url(path)
-    if is_abs:
+    path, relative_level = _get_norm_url(path)
+    if relative_level == -1:
         return path
     if page is not None:
-        return get_relative_url(path, page.url)
+        result = get_relative_url(path, page.url)
+        if relative_level > 0:
+            result = '../' * relative_level + result
+        return result
+
     return posixpath.join(base, path)
 
 
 @functools.lru_cache(maxsize=None)
-def _get_norm_url(path: str) -> Tuple[str, bool]:
+def _get_norm_url(path: str) -> Tuple[str, int]:
     if not path:
         path = '.'
     elif '\\' in path:
@@ -318,8 +322,14 @@ def _get_norm_url(path: str) -> Tuple[str, bool]:
     # Allow links to be fully qualified URLs
     parsed = urlsplit(path)
     if parsed.scheme or parsed.netloc or path.startswith(('/', '#')):
-        return path, True
-    return path, False
+        return path, -1
+
+    # Relative path - preserve information about it
+    norm = posixpath.normpath(path) + '/'
+    relative_level = 0
+    while norm.startswith('../', relative_level * 3):
+        relative_level += 1
+    return path, relative_level
 
 
 def create_media_urls(


Test patch:
diff --git a/mkdocs/tests/structure/file_tests.py b/mkdocs/tests/structure/file_tests.py
index 8d8257f0..2316ab81 100644
--- a/mkdocs/tests/structure/file_tests.py
+++ b/mkdocs/tests/structure/file_tests.py
@@ -339,6 +339,26 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
         self.assertEqual(f.url, 'foo%20bar.html')
         self.assertEqual(f.name, 'foo bar')
 
+    def test_file_name_with_custom_dest_uri(self):
+        for use_directory_urls in True, False:
+            with self.subTest(use_directory_urls=use_directory_urls):
+                f = File(
+                    'stuff/foo.md',
+                    src_dir='/path/to/docs',
+                    dest_dir='/path/to/site',
+                    use_directory_urls=use_directory_urls,
+                    dest_uri='stuff/1-foo/index.html',
+                )
+                self.assertEqual(f.src_uri, 'stuff/foo.md')
+                self.assertPathsEqual(f.abs_src_path, '/path/to/docs/stuff/foo.md')
+                self.assertEqual(f.dest_uri, 'stuff/1-foo/index.html')
+                self.assertPathsEqual(f.abs_dest_path, '/path/to/site/stuff/1-foo/index.html')
+                if use_directory_urls:
+                    self.assertEqual(f.url, 'stuff/1-foo/')
+                else:
+                    self.assertEqual(f.url, 'stuff/1-foo/index.html')
+                self.assertEqual(f.name, 'foo')
+
     def test_files(self):
         fs = [
             File('index.md', '/path/to/docs', '/path/to/site', use_directory_urls=True),
@@ -360,13 +380,14 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
         self.assertEqual(files.get_file_from_path('foo/bar.jpg'), fs[3])
         self.assertEqual(files.get_file_from_path('missing.jpg'), None)
         self.assertTrue(fs[2].src_uri in files.src_uris)
-        self.assertTrue(fs[2].src_uri in files.src_uris)
         extra_file = File('extra.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
         self.assertFalse(extra_file.src_uri in files.src_uris)
         files.append(extra_file)
         self.assertEqual(len(files), 7)
         self.assertTrue(extra_file.src_uri in files.src_uris)
         self.assertEqual(files.documentation_pages(), [fs[0], fs[1], extra_file])
+        files.remove(fs[1])
+        self.assertEqual(files.documentation_pages(), [fs[0], extra_file])
 
     @tempdir(
         files=[
@@ -444,7 +465,6 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             'foo/bar.md',
             'foo/bar/baz.md',
         ]
-
         to_file_urls = [
             './',
             'foo/',
@@ -456,6 +476,8 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
         ]
 
         from_file = File('img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True)
+        self.assertEqual(from_file.url, 'img.jpg')
+
         expected = [
             'img.jpg',  # img.jpg relative to .
             '../img.jpg',  # img.jpg relative to foo/
@@ -465,15 +487,15 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../../img.jpg',  # img.jpg relative to foo/bar
             '../../../img.jpg',  # img.jpg relative to foo/bar/baz
         ]
-
         for i, filename in enumerate(to_files):
             file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
-            self.assertEqual(from_file.url, 'img.jpg')
             self.assertEqual(file.url, to_file_urls[i])
             self.assertEqual(from_file.url_relative_to(file.url), expected[i])
             self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('foo/img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=True)
+        self.assertEqual(from_file.url, 'foo/img.jpg')
+
         expected = [
             'foo/img.jpg',  # foo/img.jpg relative to .
             'img.jpg',  # foo/img.jpg relative to foo/
@@ -483,15 +505,15 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../img.jpg',  # foo/img.jpg relative to foo/bar
             '../../img.jpg',  # foo/img.jpg relative to foo/bar/baz
         ]
-
         for i, filename in enumerate(to_files):
             file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
-            self.assertEqual(from_file.url, 'foo/img.jpg')
             self.assertEqual(file.url, to_file_urls[i])
             self.assertEqual(from_file.url_relative_to(file.url), expected[i])
             self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('index.html', '/path/to/docs', '/path/to/site', use_directory_urls=True)
+        self.assertEqual(from_file.url, './')
+
         expected = [
             './',  # . relative to .
             '../',  # . relative to foo/
@@ -501,15 +523,15 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../../',  # . relative to foo/bar
             '../../../',  # . relative to foo/bar/baz
         ]
-
         for i, filename in enumerate(to_files):
             file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
-            self.assertEqual(from_file.url, './')
             self.assertEqual(file.url, to_file_urls[i])
             self.assertEqual(from_file.url_relative_to(file.url), expected[i])
             self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('file.md', '/path/to/docs', '/path/to/site', use_directory_urls=True)
+        self.assertEqual(from_file.url, 'file/')
+
         expected = [
             'file/',  # file relative to .
             '../file/',  # file relative to foo/
@@ -519,10 +541,8 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../../file/',  # file relative to foo/bar
             '../../../file/',  # file relative to foo/bar/baz
         ]
-
         for i, filename in enumerate(to_files):
             file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=True)
-            self.assertEqual(from_file.url, 'file/')
             self.assertEqual(file.url, to_file_urls[i])
             self.assertEqual(from_file.url_relative_to(file.url), expected[i])
             self.assertEqual(from_file.url_relative_to(file), expected[i])
@@ -537,7 +557,6 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             'foo/bar.md',
             'foo/bar/baz.md',
         ]
-
         to_file_urls = [
             'index.html',
             'foo/index.html',
@@ -549,6 +568,8 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
         ]
 
         from_file = File('img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=False)
+        self.assertEqual(from_file.url, 'img.jpg')
+
         expected = [
             'img.jpg',  # img.jpg relative to .
             '../img.jpg',  # img.jpg relative to foo/
@@ -558,16 +579,16 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../img.jpg',  # img.jpg relative to foo/bar.html
             '../../img.jpg',  # img.jpg relative to foo/bar/baz.html
         ]
-
         for i, filename in enumerate(to_files):
             with self.subTest(from_file=from_file.src_path, to_file=filename):
                 file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
-                self.assertEqual(from_file.url, 'img.jpg')
                 self.assertEqual(file.url, to_file_urls[i])
                 self.assertEqual(from_file.url_relative_to(file.url), expected[i])
                 self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('foo/img.jpg', '/path/to/docs', '/path/to/site', use_directory_urls=False)
+        self.assertEqual(from_file.url, 'foo/img.jpg')
+
         expected = [
             'foo/img.jpg',  # foo/img.jpg relative to .
             'img.jpg',  # foo/img.jpg relative to foo/
@@ -577,16 +598,16 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             'img.jpg',  # foo/img.jpg relative to foo/bar.html
             '../img.jpg',  # foo/img.jpg relative to foo/bar/baz.html
         ]
-
         for i, filename in enumerate(to_files):
             with self.subTest(from_file=from_file.src_path, to_file=filename):
                 file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
-                self.assertEqual(from_file.url, 'foo/img.jpg')
                 self.assertEqual(file.url, to_file_urls[i])
                 self.assertEqual(from_file.url_relative_to(file.url), expected[i])
                 self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('index.html', '/path/to/docs', '/path/to/site', use_directory_urls=False)
+        self.assertEqual(from_file.url, 'index.html')
+
         expected = [
             'index.html',  # index.html relative to .
             '../index.html',  # index.html relative to foo/
@@ -596,16 +617,16 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../index.html',  # index.html relative to foo/bar.html
             '../../index.html',  # index.html relative to foo/bar/baz.html
         ]
-
         for i, filename in enumerate(to_files):
             with self.subTest(from_file=from_file.src_path, to_file=filename):
                 file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
-                self.assertEqual(from_file.url, 'index.html')
                 self.assertEqual(file.url, to_file_urls[i])
                 self.assertEqual(from_file.url_relative_to(file.url), expected[i])
                 self.assertEqual(from_file.url_relative_to(file), expected[i])
 
         from_file = File('file.html', '/path/to/docs', '/path/to/site', use_directory_urls=False)
+        self.assertEqual(from_file.url, 'file.html')
+
         expected = [
             'file.html',  # file.html relative to .
             '../file.html',  # file.html relative to foo/
@@ -615,11 +636,9 @@ class TestFiles(PathAssertionMixin, unittest.TestCase):
             '../file.html',  # file.html relative to foo/bar.html
             '../../file.html',  # file.html relative to foo/bar/baz.html
         ]
-
         for i, filename in enumerate(to_files):
             with self.subTest(from_file=from_file.src_path, to_file=filename):
                 file = File(filename, '/path/to/docs', '/path/to/site', use_directory_urls=False)
-                self.assertEqual(from_file.url, 'file.html')
                 self.assertEqual(file.url, to_file_urls[i])
                 self.assertEqual(from_file.url_relative_to(file.url), expected[i])
                 self.assertEqual(from_file.url_relative_to(file), expected[i])
diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py
index 5be58fb7..a72044e2 100644
--- a/mkdocs/tests/utils/utils_tests.py
+++ b/mkdocs/tests/utils/utils_tests.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 
+import dataclasses
 import datetime
 import logging
 import os
@@ -9,9 +10,7 @@ import unittest
 from unittest import mock
 
 from mkdocs import exceptions, utils
-from mkdocs.structure.files import File
-from mkdocs.structure.pages import Page
-from mkdocs.tests.base import dedent, load_config, tempdir
+from mkdocs.tests.base import dedent, tempdir
 from mkdocs.utils import meta
 
 BASEYML = """
@@ -111,174 +110,76 @@ class UtilsTests(unittest.TestCase):
         self.assertEqual(utils.get_relative_url('/', '.'), './')
         self.assertEqual(utils.get_relative_url('/', '/.'), './')
 
-    def test_create_media_urls(self):
-        expected_results = {
-            'https://media.cdn.org/jq.js': [
-                'https://media.cdn.org/jq.js',
-                'https://media.cdn.org/jq.js',
-                'https://media.cdn.org/jq.js',
-            ],
-            'http://media.cdn.org/jquery.js': [
-                'http://media.cdn.org/jquery.js',
-                'http://media.cdn.org/jquery.js',
-                'http://media.cdn.org/jquery.js',
-            ],
-            '//media.cdn.org/jquery.js': [
-                '//media.cdn.org/jquery.js',
-                '//media.cdn.org/jquery.js',
-                '//media.cdn.org/jquery.js',
-            ],
-            'media.cdn.org/jquery.js': [
-                'media.cdn.org/jquery.js',
-                'media.cdn.org/jquery.js',
-                '../media.cdn.org/jquery.js',
-            ],
-            'local/file/jquery.js': [
-                'local/file/jquery.js',
-                'local/file/jquery.js',
-                '../local/file/jquery.js',
-            ],
-            'image.png': [
-                'image.png',
-                'image.png',
-                '../image.png',
-            ],
-            'style.css?v=20180308c': [
-                'style.css?v=20180308c',
-                'style.css?v=20180308c',
-                '../style.css?v=20180308c',
-            ],
-            '#some_id': [
-                '#some_id',
-                '#some_id',
-                '#some_id',
-            ],
-        }
-
-        cfg = load_config(use_directory_urls=False)
-        pages = [
-            Page(
-                'Home',
-                File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'About',
-                File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'FooBar',
-                File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-        ]
-
-        for i, page in enumerate(pages):
-            urls = utils.create_media_urls(expected_results.keys(), page)
-            self.assertEqual([v[i] for v in expected_results.values()], urls)
-
-    def test_create_media_urls_use_directory_urls(self):
-        expected_results = {
-            'https://media.cdn.org/jq.js': [
-                'https://media.cdn.org/jq.js',
-                'https://media.cdn.org/jq.js',
-                'https://media.cdn.org/jq.js',
-            ],
-            'http://media.cdn.org/jquery.js': [
-                'http://media.cdn.org/jquery.js',
-                'http://media.cdn.org/jquery.js',
-                'http://media.cdn.org/jquery.js',
-            ],
-            '//media.cdn.org/jquery.js': [
-                '//media.cdn.org/jquery.js',
-                '//media.cdn.org/jquery.js',
-                '//media.cdn.org/jquery.js',
-            ],
-            'media.cdn.org/jquery.js': [
-                'media.cdn.org/jquery.js',
-                '../media.cdn.org/jquery.js',
-                '../../media.cdn.org/jquery.js',
-            ],
-            'local/file/jquery.js': [
-                'local/file/jquery.js',
-                '../local/file/jquery.js',
-                '../../local/file/jquery.js',
-            ],
-            'image.png': [
-                'image.png',
-                '../image.png',
-                '../../image.png',
-            ],
-            'style.css?v=20180308c': [
-                'style.css?v=20180308c',
-                '../style.css?v=20180308c',
-                '../../style.css?v=20180308c',
-            ],
-            '#some_id': [
-                '#some_id',
-                '#some_id',
-                '#some_id',
-            ],
-        }
-
-        cfg = load_config(use_directory_urls=True)
-        pages = [
-            Page(
-                'Home',
-                File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'About',
-                File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'FooBar',
-                File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-        ]
-
-        for i, page in enumerate(pages):
-            urls = utils.create_media_urls(expected_results.keys(), page)
-            self.assertEqual([v[i] for v in expected_results.values()], urls)
+    def test_normalize_url(self):
+        def test(path, base, expected):
+            self.assertEqual(utils.normalize_url(path, _Page(base)), expected)
+
+        # Absolute paths and anchors are unaffected.
+        for base in 'about.html', 'foo/bar.html', 'index.html', '', 'about/', 'foo/bar/':
+            test('https://media.cdn.org/jq.js', base, 'https://media.cdn.org/jq.js')
+            test('http://media.cdn.org/jquery.js', base, 'http://media.cdn.org/jquery.js')
+            test('//media.cdn.org/jquery.js', base, '//media.cdn.org/jquery.js')
+            test('#some_id', base, '#some_id')
+
+        path = 'media.cdn.org/jquery.js'
+        test(path, '', 'media.cdn.org/jquery.js')
+        test(path, 'index.html', 'media.cdn.org/jquery.js')
+        test(path, 'about.html', 'media.cdn.org/jquery.js')
+        test(path, 'about/', '../media.cdn.org/jquery.js')
+        test(path, 'foo/bar.html', '../media.cdn.org/jquery.js')
+        test(path, 'foo/bar/', '../../media.cdn.org/jquery.js')
+
+        path = 'local/file/jquery.js'
+        test(path, '', 'local/file/jquery.js')
+        test(path, 'index.html', 'local/file/jquery.js')
+        test(path, 'about.html', 'local/file/jquery.js')
+        test(path, 'about/', '../local/file/jquery.js')
+        test(path, 'foo/bar.html', '../local/file/jquery.js')
+        test(path, 'foo/bar/', '../../local/file/jquery.js')
+
+        path = '../../../../above/jquery.js'
+        test(path, '', '../../../../above/jquery.js')
+        test(path, 'index.html', '../../../../above/jquery.js')
+        test(path, 'about.html', '../../../../above/jquery.js')
+        test(path, 'about/', '../../../../../above/jquery.js')
+        test(path, 'foo/bar.html', '../../../../../above/jquery.js')
+        test(path, 'foo/bar/', '../../../../../../above/jquery.js')
+
+        path = '../some/dir/'
+        test(path, '', '../some/dir/')
+        test(path, 'index.html', '../some/dir/')
+        test(path, 'about.html', '../some/dir/')
+        test(path, 'about/', '../../some/dir/')
+        test(path, 'foo/bar.html', '../../some/dir/')
+        test(path, 'foo/bar/', '../../../some/dir/')
+
+        path = 'image.png'
+        test(path, '', 'image.png')
+        test(path, 'index.html', 'image.png')
+        test(path, 'about.html', 'image.png')
+        test(path, 'about/', '../image.png')
+        test(path, 'foo/bar.html', '../image.png')
+        test(path, 'foo/bar/', '../../image.png')
+
+        path = 'style.css?v=20180308c'
+        test(path, '', 'style.css?v=20180308c')
+        test(path, 'index.html', 'style.css?v=20180308c')
+        test(path, 'about.html', 'style.css?v=20180308c')
+        test(path, 'about/', '../style.css?v=20180308c')
+        test(path, 'foo/bar.html', '../style.css?v=20180308c')
+        test(path, 'foo/bar/', '../../style.css?v=20180308c')
 
     # TODO: This shouldn't pass on Linux
     # @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
-    def test_create_media_urls_windows(self):
-        expected_results = {
-            'local\\windows\\file\\jquery.js': [
-                'local/windows/file/jquery.js',
-                'local/windows/file/jquery.js',
-                '../local/windows/file/jquery.js',
-            ],
-        }
-
-        cfg = load_config(use_directory_urls=False)
-        pages = [
-            Page(
-                'Home',
-                File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'About',
-                File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-            Page(
-                'FooBar',
-                File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
-                cfg,
-            ),
-        ]
+    def test_normalize_url_windows(self):
+        def test(path, base, expected):
+            self.assertEqual(utils.normalize_url(path, _Page(base)), expected)
 
         with self.assertLogs('mkdocs', level='WARNING'):
-            for i, page in enumerate(pages):
-                urls = utils.create_media_urls(expected_results.keys(), page)
-                self.assertEqual([v[i] for v in expected_results.values()], urls)
+            path = 'local\\windows\\file\\jquery.js'
+            test(path, '', 'local/windows/file/jquery.js')
+            test(path, 'about/', '../local/windows/file/jquery.js')
+            test(path, 'foo/bar/', '../../local/windows/file/jquery.js')
 
     def test_reduce_list(self):
         self.assertEqual(
@@ -689,3 +590,8 @@ class LogCounterTests(unittest.TestCase):
         self.log.warning('counted')
         self.log.info('not counted')
         self.assertEqual(self.counter.get_counts(), [('ERROR', 2), ('WARNING', 1)])
+
+
+@dataclasses.dataclass
+class _Page:
+    url: str