diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 08cd555cf..c770fa9cc 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -60,6 +60,8 @@ Fixed - Fixed entering of insert mode when certain disabled text fields were clicked. - Fixed a crash when using `:set` with `-p` and `!` (invert value) - Downloads with unknown size are now handled correctly. +- `:navigate increment/decrement` (``/``) now handles some + corner-cases better. Removed ~~~~~~~ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index ff117ca38..ef7a16933 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -19,7 +19,6 @@ """Command dispatcher for TabbedBrowser.""" -import re import os import shlex import posixpath @@ -472,29 +471,10 @@ class CommandDispatcher: background: Open the link in a new background tab. window: Open the link in a new window. """ - encoded = bytes(url.toEncoded()).decode('ascii') - # Get the last number in a string - match = re.match(r'(.*\D|^)(\d+)(.*)', encoded) - if not match: - raise cmdexc.CommandError("No number found in URL!") - pre, number, post = match.groups() - if not number: - raise cmdexc.CommandError("No number found in URL!") try: - val = int(number) - except ValueError: - raise cmdexc.CommandError("Could not parse number '{}'.".format( - number)) - if incdec == 'decrement': - if val <= 0: - raise cmdexc.CommandError("Can't decrement {}!".format(val)) - val -= 1 - elif incdec == 'increment': - val += 1 - else: - raise ValueError("Invalid value {} for indec!".format(incdec)) - urlstr = ''.join([pre, str(val), post]).encode('ascii') - new_url = QUrl.fromEncoded(urlstr) + new_url = urlutils.incdec_number(url, incdec) + except urlutils.IncDecError as error: + raise cmdexc.CommandError(error.msg) self._open(new_url, tab, background, window) def _navigate_up(self, url, tab, background, window): diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 50f60d523..5d8987138 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -443,3 +443,59 @@ class FuzzyUrlError(Exception): return self.msg else: return '{}: {}'.format(self.msg, self.url.errorString()) + + +class IncDecError(Exception): + + """Exception raised by incdec_number on problems. + + Attributes: + msg: The error message. + url: The QUrl which caused the error. + """ + + def __init__(self, msg, url): + super().__init__(msg) + self.url = url + self.msg = msg + + def __str__(self): + return '{}: {}'.format(self.msg, self.url.toString()) + + +def incdec_number(url, incdec): + """Find a number in the url and increment or decrement it. + + Args: + url: The current url + incdec: Either 'increment' or 'decrement' + + Return: + The new url with the number incremented/decremented. + + Raises IncDecError if the url contains no number. + """ + if not url.isValid(): + raise ValueError(get_errstring(url)) + + path = url.path() + # Get the last number in a string + match = re.match(r'(.*\D|^)(\d+)(.*)', path) + if not match: + raise IncDecError("No number found in URL!", url) + pre, number, post = match.groups() + # This should always succeed because we match \d+ + val = int(number) + if incdec == 'decrement': + if val <= 0: + raise IncDecError("Can't decrement {}!".format(val), url) + val -= 1 + elif incdec == 'increment': + val += 1 + else: + raise ValueError("Invalid value {} for indec!".format(incdec)) + new_path = ''.join([pre, str(val), post]) + # Make a copy of the QUrl so we don't modify the original + new_url = QUrl(url) + new_url.setPath(new_path) + return new_url diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py index fa1cfa665..cb07ca80e 100644 --- a/tests/utils/test_urlutils.py +++ b/tests/utils/test_urlutils.py @@ -524,3 +524,70 @@ def test_same_domain_invalid_url(url1, url2): """Test same_domain with invalid URLs.""" with pytest.raises(ValueError): urlutils.same_domain(QUrl(url1), QUrl(url2)) + +class TestIncDecNumber: + + """Tests for urlutils.incdec_number().""" + + @pytest.mark.parametrize('url, incdec, output', [ + ("http://example.com/index1.html", "increment", "http://example.com/index2.html"), + ("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"), + ("http://bbc.c0.uk:80/story_1", "increment", "http://bbc.c0.uk:80/story_2"), + ("http://mydomain.tld/1_%C3%A4", "increment", "http://mydomain.tld/2_%C3%A4"), + ("http://example.com/site/5#5", "increment", "http://example.com/site/6#5"), + + ("http://example.com/index10.html", "decrement", "http://example.com/index9.html"), + ("http://foo.bar/folder_1/image_3", "decrement", "http://foo.bar/folder_1/image_2"), + ("http://bbc.c0.uk:80/story_1", "decrement", "http://bbc.c0.uk:80/story_0"), + ("http://mydomain.tld/2_%C3%A4", "decrement", "http://mydomain.tld/1_%C3%A4"), + ("http://example.com/site/5#5", "decrement", "http://example.com/site/4#5"), + ]) + def test_incdec_number(self, url, incdec, output): + """Test incdec_number with valid URLs.""" + new_url = urlutils.incdec_number(QUrl(url), incdec) + assert new_url == QUrl(output) + + @pytest.mark.parametrize('url', [ + "http://example.com/long/path/but/no/number", + "http://ex4mple.com/number/in/hostname", + "http://example.com:42/number/in/port", + "http://www2.example.com/number/in/subdomain", + "http://example.com/%C3%B6/urlencoded/data", + "http://example.com/number/in/anchor#5", + "http://www2.ex4mple.com:42/all/of/the/%C3%A4bove#5", + ]) + def test_no_number(self, url): + """Test incdec_number with URLs that don't contain a number.""" + with pytest.raises(urlutils.IncDecError): + urlutils.incdec_number(QUrl(url), "increment") + + def test_number_below_0(self): + """Test incdec_number with a number that would be below zero + after decrementing.""" + with pytest.raises(urlutils.IncDecError): + urlutils.incdec_number(QUrl('http://example.com/page_0.html'), + 'decrement') + + def test_invalid_url(self): + """Test if incdec_number rejects an invalid URL.""" + with pytest.raises(ValueError): + urlutils.incdec_number(QUrl(""), "increment") + + def test_wrong_mode(self): + """Test if incdec_number rejects a wrong parameter for the incdec + argument.""" + valid_url = QUrl("http://example.com/0") + with pytest.raises(ValueError): + urlutils.incdec_number(valid_url, "foobar") + + @pytest.mark.parametrize("url, msg, expected_str", [ + ("http://example.com", "Invalid", "Invalid: http://example.com"), + ]) + def test_incdec_error(self, url, msg, expected_str): + """Test IncDecError.""" + url = QUrl(url) + with pytest.raises(urlutils.IncDecError) as excinfo: + raise urlutils.IncDecError(msg, url) + + assert excinfo.value.url == url + assert str(excinfo.value) == expected_str