Repository: qutebrowser/qutebrowser

Issue:
Make hint groups/selectors configurable
With the new config we have dictionaries in the config.

With that, we could make hint groups/selectors configurable. Currently those are hardcoded in `webelem.py`:

```python
SELECTORS = {
    Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
                'frame, iframe, link, [onclick], [onmousedown], [role=link], '
                '[role=option], [role=button], img'),
    Group.links: 'a[href], area[href], link[href], [role=link][href]',
    Group.images: 'img',
    Group.url: '[src], [href]',
    Group.inputs: ('input[type=text], input[type=email], input[type=url], '
                   'input[type=tel], input[type=number], '
                   'input[type=password], input[type=search], '
                   'input:not([type]), textarea'),
}
```

This would make it possible to add hints for custom elements (if it's a per-url setting), or add new groups like "media" to hint audio/video elements to be opened with mpv.

Also, `:hint` could take a selector instead of a group name too.


Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -637,9 +637,8 @@ def _start_cb(self, elems):
                        star_args_optional=True, maxsplit=2)
     @cmdutils.argument('win_id', win_id=True)
     def start(self,  # pylint: disable=keyword-arg-before-vararg
-              group=webelem.Group.all, target=Target.normal,
-              *args, win_id, mode=None, add_history=False, rapid=False,
-              first=False):
+              group='all', target=Target.normal, *args, win_id, mode=None,
+              add_history=False, rapid=False, first=False):
         """Start hinting.
 
         Args:
@@ -741,10 +740,25 @@ def start(self,  # pylint: disable=keyword-arg-before-vararg
             raise cmdexc.CommandError("No URL set for this page yet!")
         self._context.args = args
         self._context.group = group
-        selector = webelem.SELECTORS[self._context.group]
+        selector = self._get_selector()
         self._context.tab.elements.find_css(selector, self._start_cb,
                                             only_visible=True)
 
+    def _get_selector(self):
+        """Get the CSS selectors for this url and hinting group."""
+        url = self._context.baseurl
+        group = self._context.group
+
+        selectors = config.instance.get('hints.selectors', url)
+        if group not in selectors:
+            selectors = config.val.hints.selectors
+
+            if group not in selectors:
+                raise cmdexc.CommandError("Undefined hinting group "
+                                          "'{}'!".format(group))
+
+        return ','.join(selectors[group])
+
     def current_mode(self):
         """Return the currently active hinting mode (or None otherwise)."""
         if self._context is None:
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -21,7 +21,6 @@
 
 import posixpath
 
-from qutebrowser.browser import webelem
 from qutebrowser.config import config
 from qutebrowser.utils import objreg, urlutils, log, message, qtutils
 from qutebrowser.mainwindow import mainwindow
@@ -147,5 +146,6 @@ def _prevnext_cb(elems):
         else:
             browsertab.openurl(url)
 
-    browsertab.elements.find_css(webelem.SELECTORS[webelem.Group.links],
-                                 _prevnext_cb)
+    selectors = config.instance.get('hints.selectors', baseurl)
+    link_selector = ','.join(selectors['links'])
+    browsertab.elements.find_css(link_selector, _prevnext_cb)
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -17,14 +17,8 @@
 # You should have received a copy of the GNU General Public License
 # along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.
 
-"""Generic web element related code.
+"""Generic web element related code."""
 
-Module attributes:
-    Group: Enum for different kinds of groups.
-    SELECTORS: CSS selectors for different groups of elements.
-"""
-
-import enum
 import collections.abc
 
 from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
@@ -36,25 +30,6 @@
 from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
 
 
-Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
-
-
-SELECTORS = {
-    Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
-                'frame, iframe, link, summary, [onclick], [onmousedown], '
-                '[role=link], [role=option], [role=button], img, '
-                # Angular 1 selectors
-                '[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
-    Group.links: 'a[href], area[href], link[href], [role=link][href]',
-    Group.images: 'img',
-    Group.url: '[src], [href]',
-    Group.inputs: ('input[type=text], input[type=email], input[type=url], '
-                   'input[type=tel], input[type=number], '
-                   'input[type=password], input[type=search], '
-                   'input:not([type]), textarea'),
-}
-
-
 class Error(Exception):
 
     """Base class for WebElement errors."""


Test patch:
diff --git a/tests/end2end/data/hints/custom_group.html b/tests/end2end/data/hints/custom_group.html
new file mode 100644
--- /dev/null
+++ b/tests/end2end/data/hints/custom_group.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>Custom hint groups</title>
+    </head>
+    <body>
+        <div class="clickable" onclick="console.log('beep!')">beep!</div>
+    </body>
+</html>
diff --git a/tests/end2end/data/hints/html/click_handler.html b/tests/end2end/data/hints/html/click_handler.html
new file mode 100644
--- /dev/null
+++ b/tests/end2end/data/hints/html/click_handler.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+
+<!-- target: hello.txt -->
+
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>Javascript link</title>
+    </head>
+    <body>
+        <button id="link">Follow me via JS!</button>
+        <script type="text/javascript">
+            document.getElementById('link').addEventListener('click', function() {
+                window.location.href = '/data/hello.txt';
+            });
+        </script>
+    </body>
+</html>
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -185,6 +185,30 @@ Feature: Using hints
         # The actual check is already done above
         Then no crash should happen
 
+    Scenario: Error with invalid hint group
+        When I open data/hints/buttons.html
+        And I run :hint INVALID_GROUP
+        Then the error "Undefined hinting group 'INVALID_GROUP'!" should be shown
+
+    Scenario: Custom hint group
+        When I open data/hints/custom_group.html
+        And I set hints.selectors to {"custom":[".clickable"]}
+        And I hint with args "custom" and follow a
+        Then the javascript message "beep!" should be logged
+
+    Scenario: Custom hint group with URL pattern
+        When I open data/hints/custom_group.html
+        And I run :set -u *://*/data/hints/custom_group.html hints.selectors '{"custom": [".clickable"]}'
+        And I hint with args "custom" and follow a
+        Then the javascript message "beep!" should be logged
+
+    Scenario: Fallback to global value with URL pattern set
+        When I open data/hints/custom_group.html
+        And I set hints.selectors to {"custom":[".clickable"]}
+        And I run :set -u *://*/data/hints/custom_group.html hints.selectors '{"other": [".other"]}'
+        And I hint with args "custom" and follow a
+        Then the javascript message "beep!" should be logged
+
     # https://github.com/qutebrowser/qutebrowser/issues/1613
     Scenario: Hinting inputs with padding
         When I open data/hints/input.html
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -29,7 +29,7 @@
 from PyQt5.QtCore import QRect, QPoint, QUrl
 QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement
 
-from qutebrowser.browser import webelem, browsertab
+from qutebrowser.browser import browsertab
 from qutebrowser.browser.webkit import webkitelem
 from qutebrowser.misc import objects
 from qutebrowser.utils import usertypes
@@ -146,53 +146,46 @@ class SelectionAndFilterTests:
     TESTS = [
         ('<foo />', []),
         ('<foo bar="baz"/>', []),
-        ('<foo href="baz"/>', [webelem.Group.url]),
-        ('<foo src="baz"/>', [webelem.Group.url]),
+        ('<foo href="baz"/>', ['url']),
+        ('<foo src="baz"/>', ['url']),
 
-        ('<a />', [webelem.Group.all]),
-        ('<a href="foo" />', [webelem.Group.all, webelem.Group.links,
-                              webelem.Group.url]),
-        ('<a href="javascript://foo" />', [webelem.Group.all,
-                                           webelem.Group.links,
-                                           webelem.Group.url]),
+        ('<a />', ['all']),
+        ('<a href="foo" />', ['all', 'links', 'url']),
+        ('<a href="javascript://foo" />', ['all', 'links', 'url']),
 
-        ('<area />', [webelem.Group.all]),
-        ('<area href="foo" />', [webelem.Group.all, webelem.Group.links,
-                                 webelem.Group.url]),
+        ('<area />', ['all']),
+        ('<area href="foo" />', ['all', 'links', 'url']),
 
-        ('<link />', [webelem.Group.all]),
-        ('<link href="foo" />', [webelem.Group.all, webelem.Group.links,
-                                 webelem.Group.url]),
+        ('<link />', ['all']),
+        ('<link href="foo" />', ['all', 'links', 'url']),
 
-        ('<textarea />', [webelem.Group.all, webelem.Group.inputs]),
-        ('<select />', [webelem.Group.all]),
+        ('<textarea />', ['all', 'inputs']),
+        ('<select />', ['all']),
 
-        ('<input />', [webelem.Group.all, webelem.Group.inputs]),
+        ('<input />', ['all', 'inputs']),
         ('<input type="hidden" />', []),
-        ('<input type="text" />', [webelem.Group.inputs, webelem.Group.all]),
-        ('<input type="email" />', [webelem.Group.inputs, webelem.Group.all]),
-        ('<input type="url" />', [webelem.Group.inputs, webelem.Group.all]),
-        ('<input type="tel" />', [webelem.Group.inputs, webelem.Group.all]),
-        ('<input type="number" />', [webelem.Group.inputs, webelem.Group.all]),
-        ('<input type="password" />', [webelem.Group.inputs,
-                                       webelem.Group.all]),
-        ('<input type="search" />', [webelem.Group.inputs, webelem.Group.all]),
-
-        ('<button />', [webelem.Group.all]),
-        ('<button href="foo" />', [webelem.Group.all, webelem.Group.url]),
+        ('<input type="text" />', ['inputs', 'all']),
+        ('<input type="email" />', ['inputs', 'all']),
+        ('<input type="url" />', ['inputs', 'all']),
+        ('<input type="tel" />', ['inputs', 'all']),
+        ('<input type="number" />', ['inputs', 'all']),
+        ('<input type="password" />', ['inputs', 'all']),
+        ('<input type="search" />', ['inputs', 'all']),
+
+        ('<button />', ['all']),
+        ('<button href="foo" />', ['all', 'url']),
 
         # We can't easily test <frame>/<iframe> as they vanish when setting
         # them via QWebFrame::setHtml...
 
-        ('<p onclick="foo" foo="bar"/>', [webelem.Group.all]),
-        ('<p onmousedown="foo" foo="bar"/>', [webelem.Group.all]),
-        ('<p role="option" foo="bar"/>', [webelem.Group.all]),
-        ('<p role="button" foo="bar"/>', [webelem.Group.all]),
-        ('<p role="button" href="bar"/>', [webelem.Group.all,
-                                           webelem.Group.url]),
+        ('<p onclick="foo" foo="bar"/>', ['all']),
+        ('<p onmousedown="foo" foo="bar"/>', ['all']),
+        ('<p role="option" foo="bar"/>', ['all']),
+        ('<p role="button" foo="bar"/>', ['all']),
+        ('<p role="button" href="bar"/>', ['all', 'url']),
     ]
 
-    GROUPS = list(webelem.Group)
+    GROUPS = ['all', 'links', 'images', 'url', 'inputs']
 
     COMBINATIONS = list(itertools.product(TESTS, GROUPS))
 
@@ -215,11 +208,12 @@ def test_test_generator(self):
         assert self.TESTS
 
     @pytest.mark.parametrize('group, val, matching', TESTS)
-    def test_selectors(self, webframe, group, val, matching):
+    def test_selectors(self, webframe, group, val, matching, config_stub):
         webframe.setHtml('<html><body>{}</body></html>'.format(val))
         # Make sure setting HTML succeeded and there's a new element
         assert len(webframe.findAllElements('*')) == 3
-        elems = webframe.findAllElements(webelem.SELECTORS[group])
+        selector = ','.join(config_stub.val.hints.selectors[group])
+        elems = webframe.findAllElements(selector)
         elems = [webkitelem.WebKitElement(e, tab=None) for e in elems]
         assert bool(elems) == matching