From 9362d22c9505cc3045df4c7f7ac9647f9d90e5dc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 2 Jan 2019 14:11:30 +0100 Subject: [PATCH] doxygen: make it possible to search directly from browser URL bar. Adds an ability to specify search query by adding ?q={query}#search to URL, which can be then bookmarked in the browsers to provide search directly from the URL bar (or search input field). On browsers supporting OpenSearch discovery (Firefox, Chrome, probably Edge too), it also provides metadata that allow "single-click" addition of the search engine to browser's search engine list. This option is unfortunately well-hidden in the current versions (Firefox has it in the three-dots menu, in Chrome you need to go to Settings while being on this page and then it shows a suggestion, wtf). The OpenSearch metadata are provided through a new opensearch.xml file and referenced from page . OpenSearch also supports autocompletion and search directly from browser address bar, but for that to work I would need to implement a server-side search functionality. Not yet, since I don't have any immediate plans to turn my cloud file-serving Apache installation into a smart and vulnerable attack target. This also wraps the input in a
, so browsers not supporting OpenSearch discovery (Vivaldi) can still add the search engine by right-clicking on the input field. This works in Firefox as well. Also, because of the added, Vivaldi started autocompleting crap suggestions for me, so I had to explicitly disable that. --- doc/doxygen.rst | 32 +++++++- doxygen/dox2html5.py | 21 ++++- doxygen/search.js | 1 + doxygen/templates/base.html | 7 +- doxygen/templates/opensearch.xml | 9 +++ doxygen/test/layout/pages.html | 4 +- .../test/layout_generated_doxyfile/index.html | 4 +- doxygen/test/layout_minimal/index.html | 4 +- doxygen/test/layout_search_binary/index.html | 4 +- .../test/layout_search_opensearch/Doxyfile | 10 +++ .../test/layout_search_opensearch/index.html | 76 +++++++++++++++++++ .../layout_search_opensearch/indexpage.xml | 11 +++ .../opensearch.xml.html | 7 ++ doxygen/test/test_doxyfile.py | 1 + doxygen/test/test_layout.py | 11 +++ 15 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 doxygen/templates/opensearch.xml create mode 100644 doxygen/test/layout_search_opensearch/Doxyfile create mode 100644 doxygen/test/layout_search_opensearch/index.html create mode 100644 doxygen/test/layout_search_opensearch/indexpage.xml create mode 100644 doxygen/test/layout_search_opensearch/opensearch.xml.html diff --git a/doc/doxygen.rst b/doc/doxygen.rst index b04faee7..78e950cb 100644 --- a/doc/doxygen.rst +++ b/doc/doxygen.rst @@ -378,17 +378,22 @@ Variable Description ``NO`` is used. :ini:`M_SEARCH_DOWNLOAD_BINARY` Download search data as a binary to save bandwidth and initial processing time. If - not set, ``NO`` is used. See `Search`_ for - more information. + not set, ``NO`` is used. See + `Search options`_ for more information. :ini:`M_SEARCH_HELP` HTML code to display as help text on empty search popup. If not set, a default message is used. Has effect only if :ini:`M_SEARCH_DISABLED` is not ``YES``. +:ini:`M_SEARCH_BASE_URL` Base URL for OpenSearch-based search engine + suggestions for web browsers. See + `Search options`_ for more information. Has + effect only if :ini:`M_SEARCH_DISABLED` is + not ``YES``. :ini:`M_SEARCH_EXTERNAL_URL` URL for external search. The ``{query}`` placeholder is replaced with urlencoded search string. If not set, no external - search is offered. See `Search`_ for more - information. Has effect only if + search is offered. See `Search options`_ + for more information. Has effect only if :ini:`M_SEARCH_DISABLED` is not ``YES``. =================================== =========================================== @@ -531,6 +536,25 @@ being 25% larger, but since this is for serving from a local filesystem, it's not considered a problem. If your docs are accessed through a server (or you don't need Chrome support), enable the :ini:`M_SEARCH_DOWNLOAD_BINARY` option. +The site can provide search engine metadata using the `OpenSearch `_ +specification. On supported browsers this means you can add the search field to +search engines and search directly from the address bar. To enable search +engine metadata, point :ini:`M_SEARCH_BASE_URL` to base URL of your +documentation, for example: + +.. code:: ini + + M_SEARCH_BASE_URL = "https://doc.magnum.graphics/magnum/" + +In general, even without the above setting, appending ``?q={query}#search`` to +the URL will directly open the search popup with results for ``{query}``. + +.. note-info:: + + OpenSearch also makes it possible to have autocompletion and search results + directly in the browser address bar. However that requires a server-side + search implementation and is not supported at the moment. + If :ini:`M_SEARCH_EXTERNAL_URL` is specified, full-text search using an external search engine is offered if nothing is found for given string or if the user has JavaScript disabled. It's recommended to restrict the search to diff --git a/doxygen/dox2html5.py b/doxygen/dox2html5.py index 83c43867..6fab057f 100755 --- a/doxygen/dox2html5.py +++ b/doxygen/dox2html5.py @@ -42,6 +42,7 @@ import logging from enum import Flag from types import SimpleNamespace as Empty from typing import Tuple, Dict, Any, List +from urllib.parse import urljoin from jinja2 import Environment, FileSystemLoader @@ -3229,6 +3230,7 @@ suffix lists all members of given symbol or directory. Navigate through the list using and , press Enter to go."""], + 'M_SEARCH_BASE_URL': [''], 'M_SEARCH_EXTERNAL_URL': [''] } @@ -3329,7 +3331,8 @@ list using and 'M_FAVICON', 'M_MATH_CACHE_FILE', 'M_SEARCH_HELP', - 'M_SEARCH_EXTERNAL_URL']: + 'M_SEARCH_EXTERNAL_URL', + 'M_SEARCH_BASE_URL']: if i in config: state.doxyfile[i] = '\n'.join(config[i]) # Int values that we want @@ -3401,6 +3404,7 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_ if urllib.parse.urlparse(path).netloc: return path return os.path.basename(path) env.filters['basename_or_url'] = basename_or_url + env.filters['urljoin'] = urljoin # Do a pre-pass and gather: # - brief descriptions of all classes, namespaces, dirs and files because @@ -3492,6 +3496,21 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_ with open(os.path.join(html_output, "searchdata.js"), 'wb') as f: f.write(base85encode_search_data(data)) + # OpenSearch metadata, in case we have the base URL + if state.doxyfile['M_SEARCH_BASE_URL']: + logging.debug("writing OpenSearch metadata file") + + template = env.get_template('opensearch.xml') + rendered = template.render(**state.doxyfile) + output = os.path.join(html_output, 'opensearch.xml') + with open(output, 'wb') as f: + f.write(rendered.encode('utf-8')) + # Add back a trailing newline so we don't need to bother with + # patching test files to include a trailing newline to make Git + # happy + # TODO could keep_trailing_newline fix this better? + f.write(b'\n') + # Copy all referenced files for i in state.images + state.doxyfile['HTML_EXTRA_STYLESHEET'] + state.doxyfile['HTML_EXTRA_FILES'] + ([state.doxyfile['M_FAVICON'][0]] if state.doxyfile['M_FAVICON'] else []) + ([] if state.doxyfile['M_SEARCH_DISABLED'] else ['search.js']): # Skip absolute URLs diff --git a/doxygen/search.js b/doxygen/search.js index 0480e185..a796b530 100644 --- a/doxygen/search.js +++ b/doxygen/search.js @@ -609,6 +609,7 @@ if(typeof document !== 'undefined') { and prevent page layout jumps */ document.body.style.overflow = 'auto'; document.body.style.paddingRight = '0'; + return false; /* so the form doesn't get sent */ } /* Search hidden */ diff --git a/doxygen/templates/base.html b/doxygen/templates/base.html index 7f4a571e..b4ec324b 100644 --- a/doxygen/templates/base.html +++ b/doxygen/templates/base.html @@ -9,6 +9,9 @@ {% if M_FAVICON %} {% endif %} + {% if not M_SEARCH_DISABLED and M_SEARCH_BASE_URL %} + + {% endif %} {% block header_links %} {% endblock %} @@ -120,7 +123,9 @@
- + + +
{{ M_SEARCH_HELP|indent(12) }} diff --git a/doxygen/templates/opensearch.xml b/doxygen/templates/opensearch.xml new file mode 100644 index 00000000..d477ab27 --- /dev/null +++ b/doxygen/templates/opensearch.xml @@ -0,0 +1,9 @@ + + + {{ PROJECT_NAME }}{% if PROJECT_BRIEF %} {{ PROJECT_BRIEF }}{% endif %} + Search {{ PROJECT_NAME }} documentation + {% if M_FAVICON %} + {{ M_SEARCH_BASE_URL|urljoin(M_FAVICON[0])|e }} + {% endif %} + + diff --git a/doxygen/test/layout/pages.html b/doxygen/test/layout/pages.html index 6462a713..dfc3efb8 100644 --- a/doxygen/test/layout/pages.html +++ b/doxygen/test/layout/pages.html @@ -96,7 +96,9 @@
- +
+ +
Some help. diff --git a/doxygen/test/layout_generated_doxyfile/index.html b/doxygen/test/layout_generated_doxyfile/index.html index e770f90c..4bc45dbe 100644 --- a/doxygen/test/layout_generated_doxyfile/index.html +++ b/doxygen/test/layout_generated_doxyfile/index.html @@ -60,7 +60,9 @@
- +
+ +
Search for symbols, directories, files, pages or modules. You can omit any diff --git a/doxygen/test/layout_minimal/index.html b/doxygen/test/layout_minimal/index.html index e770f90c..4bc45dbe 100644 --- a/doxygen/test/layout_minimal/index.html +++ b/doxygen/test/layout_minimal/index.html @@ -60,7 +60,9 @@
- +
+ +
Search for symbols, directories, files, pages or modules. You can omit any diff --git a/doxygen/test/layout_search_binary/index.html b/doxygen/test/layout_search_binary/index.html index 1455459c..c67b6207 100644 --- a/doxygen/test/layout_search_binary/index.html +++ b/doxygen/test/layout_search_binary/index.html @@ -54,7 +54,9 @@
- +
+ +
Halp. diff --git a/doxygen/test/layout_search_opensearch/Doxyfile b/doxygen/test/layout_search_opensearch/Doxyfile new file mode 100644 index 00000000..cdcf5af1 --- /dev/null +++ b/doxygen/test/layout_search_opensearch/Doxyfile @@ -0,0 +1,10 @@ +PROJECT_NAME = "A project" +PROJECT_BRIEF = "is cool" +XML_OUTPUT = + +##! M_PAGE_FINE_PRINT = +##! M_THEME_COLOR = +##! M_LINKS_NAVBAR1 = +##! M_LINKS_NAVBAR2 = +##! M_SEARCH_BASE_URL = http://localhost:8000 +##! M_SEARCH_HELP = "Right-click to add a search engine." diff --git a/doxygen/test/layout_search_opensearch/index.html b/doxygen/test/layout_search_opensearch/index.html new file mode 100644 index 00000000..8b5ac6de --- /dev/null +++ b/doxygen/test/layout_search_opensearch/index.html @@ -0,0 +1,76 @@ + + + + + A project is cool + + + + + + + +
+
+
+
+
+

+ A project +

+
+
+
+
+ + + + + diff --git a/doxygen/test/layout_search_opensearch/indexpage.xml b/doxygen/test/layout_search_opensearch/indexpage.xml new file mode 100644 index 00000000..fa3eefa3 --- /dev/null +++ b/doxygen/test/layout_search_opensearch/indexpage.xml @@ -0,0 +1,11 @@ + + + + index + A project + + + + + + diff --git a/doxygen/test/layout_search_opensearch/opensearch.xml.html b/doxygen/test/layout_search_opensearch/opensearch.xml.html new file mode 100644 index 00000000..87d0eb1d --- /dev/null +++ b/doxygen/test/layout_search_opensearch/opensearch.xml.html @@ -0,0 +1,7 @@ + + + A project is cool + Search A project documentation + http://localhost:8000/favicon-dark.png + + diff --git a/doxygen/test/test_doxyfile.py b/doxygen/test/test_doxyfile.py index d4e451c6..9948518a 100644 --- a/doxygen/test/test_doxyfile.py +++ b/doxygen/test/test_doxyfile.py @@ -58,6 +58,7 @@ class Doxyfile(unittest.TestCase): 'M_PAGE_HEADER': 'this is "quotes" \'apostrophes\'', 'M_SEARCH_DISABLED': False, 'M_SEARCH_DOWNLOAD_BINARY': False, + 'M_SEARCH_BASE_URL': '', 'M_SEARCH_EXTERNAL_URL': '', 'M_SEARCH_HELP': """Search for symbols, directories, files, pages or modules. You can omit any diff --git a/doxygen/test/test_layout.py b/doxygen/test/test_layout.py index 0acf223d..a7a32b72 100644 --- a/doxygen/test/test_layout.py +++ b/doxygen/test/test_layout.py @@ -91,3 +91,14 @@ class SearchBinary(BaseTestCase): self.run_dox2html5(wildcard='indexpage.xml') self.assertEqual(*self.actual_expected_contents('index.html')) self.assertTrue(os.path.exists(os.path.join(self.path, 'html', 'searchdata.bin'))) + +class SearchOpenSearch(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'search_opensearch', *args, **kwargs) + + def test(self): + self.run_dox2html5(wildcard='indexpage.xml') + self.assertEqual(*self.actual_expected_contents('index.html')) + # Renamed with a HTML extension so dox2html5's metadata parser doesn't + # pick it up + self.assertEqual(*self.actual_expected_contents('opensearch.xml', 'opensearch.xml.html')) -- 2.30.2