diff --git a/properdocs/config/config_options.py b/properdocs/config/config_options.py
index d16faea7..7a9a82a6 100644
--- a/properdocs/config/config_options.py
+++ b/properdocs/config/config_options.py
@@ -17,7 +17,6 @@
from urllib.parse import urlsplit, urlunsplit
import markdown
-import pathspec
import pathspec.gitignore
from properdocs import plugins, theme, utils
@@ -497,8 +496,8 @@ def run_validation(self, value: object) -> str:
return value
try:
parsed_url = urlsplit(value)
- except (AttributeError, TypeError):
- raise ValidationError("Unable to parse the URL.")
+ except Exception:
+ raise ValidationError("The URL is invalid")
if parsed_url.scheme and parsed_url.netloc:
if self.is_dir and not parsed_url.path.endswith('/'):
diff --git a/properdocs/structure/nav.py b/properdocs/structure/nav.py
index d6bd8d1b..c885e646 100644
--- a/properdocs/structure/nav.py
+++ b/properdocs/structure/nav.py
@@ -164,9 +164,16 @@ def get_navigation(files: Files, config: ProperDocsConfig) -> Navigation:
links = _get_by_type(items, Link)
for link in links:
- scheme, netloc, path, query, fragment = urlsplit(link.url)
- if scheme or netloc:
- log.debug(f"An external link to '{link.url}' is included in the 'nav' configuration.")
+ invalid: str | None = None
+ try:
+ scheme, netloc, path, query, fragment = urlsplit(link.url)
+ except Exception as e:
+ invalid = 'invalid'
+ else:
+ if scheme or netloc:
+ invalid = 'external'
+ if invalid:
+ log.debug(f"An {invalid} link to '{link.url}' is included in the 'nav' configuration.")
elif (
link.url.startswith('/')
and config.validation.nav.absolute_links
diff --git a/properdocs/structure/pages.py b/properdocs/structure/pages.py
index 8e141a0c..ca15282e 100644
--- a/properdocs/structure/pages.py
+++ b/properdocs/structure/pages.py
@@ -417,7 +417,15 @@ def _possible_target_uris(
tried.add(guess)
def path_to_url(self, url: str) -> str:
- scheme, netloc, path, query, anchor = urlsplit(url)
+ try:
+ scheme, netloc, path, query, anchor = urlsplit(url)
+ except ValueError: # Invalid URL, e.g. invalid IPv6.
+ log.log(
+ self.config.validation.links.unrecognized_links,
+ f"Doc file '{self.file.src_uri}' contains an invalid link '{url}', "
+ f"it was left as is.",
+ )
+ return url
absolute_link = None
warning_level, warning = 0, ''
diff --git a/properdocs/tests/config/config_options_tests.py b/properdocs/tests/config/config_options_tests.py
index be13eb1a..258d3c0d 100644
--- a/properdocs/tests/config/config_options_tests.py
+++ b/properdocs/tests/config/config_options_tests.py
@@ -424,6 +424,7 @@ class Schema(Config):
conf = self.get_config(Schema, {'option': None})
self.assertEqual(conf.option, None)
+ @unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11")
def test_invalid_url(self) -> None:
class Schema(Config):
option = c.URL()
@@ -431,7 +432,16 @@ class Schema(Config):
with self.expect_error(option="Required configuration not provided."):
self.get_config(Schema, {'option': None})
- for url in "properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/":
+ for url in ["http://[a]/"]:
+ with self.subTest(url=url):
+ with self.expect_error(option="The URL is invalid"):
+ self.get_config(Schema, {'option': url})
+
+ def test_invalid_url_no_http(self) -> None:
+ class Schema(Config):
+ option = c.URL()
+
+ for url in ["properdocs.org", "//properdocs.org/test", "http:/properdocs.org/", "/hello/"]:
with self.subTest(url=url):
with self.expect_error(
option="The URL isn't valid, it should include the http:// (scheme)"
diff --git a/properdocs/tests/structure/page_tests.py b/properdocs/tests/structure/page_tests.py
index 4d6810e9..851ad0b0 100644
--- a/properdocs/tests/structure/page_tests.py
+++ b/properdocs/tests/structure/page_tests.py
@@ -1221,6 +1221,19 @@ def test_absolute_win_local_path(self):
'absolute local path',
)
+ @unittest.skipUnless(sys.version_info >= (3, 11), "new error kind in Python 3.11")
+ def test_invalid_link_ipv6(self):
+ for use_directory_urls in True, False:
+ self.assertEqual(
+ self.get_rendered_result(
+ use_directory_urls=use_directory_urls,
+ content='[invalid link](http://[a]/)',
+ files=['index.md'],
+ logs="INFO:Doc file 'index.md' contains an invalid link 'http://[a]/', it was left as is.",
+ ),
+ 'invalid link',
+ )
+
def test_email_link(self):
self.assertEqual(
self.get_rendered_result(content='', files=['index.md']),
diff --git a/properdocs/utils/__init__.py b/properdocs/utils/__init__.py
index 6bf97ffb..2cc6813c 100644
--- a/properdocs/utils/__init__.py
+++ b/properdocs/utils/__init__.py
@@ -221,8 +221,11 @@ def _get_norm_url(path: str) -> tuple[str, int]:
f"That will be unsupported in a future release. Please change it to '/'."
)
path = path.replace('\\', '/')
+ try:
+ parsed = urlsplit(path)
+ except ValueError:
+ return path, -1
# Allow links to be fully qualified URLs
- parsed = urlsplit(path)
if parsed.scheme or parsed.netloc or path.startswith(('/', '#')):
return path, -1