We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
`?` search syntax is broken
The [user documentation says](https://docs.searxng.org/user/search_syntax.html):
  
>**Prefix: `?`**
>to add engines and categories to the currently selected categories

But to me it is unclear how it is different from the `!` bang syntax. The following queries all yield exactly the same results:

* `!bing test !images`
* `!bing test ?images`
* `?bing test !images`

Only `?bing test ?images` for some reason only yields images??? :confused: 

Can somebody explain to me why somebody would want to use `?` over `!`?

If there is no satisfying answer to that question, I vote that we remove (or at least deprecate) the `?` syntax.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `searx/query.py`
Content:
```
1 # SPDX-License-Identifier: AGPL-3.0-or-later
2 
3 from abc import abstractmethod, ABC
4 import re
5 
6 from searx import settings
7 from searx.languages import language_codes
8 from searx.engines import categories, engines, engine_shortcuts
9 from searx.external_bang import get_bang_definition_and_autocomplete
10 from searx.search import EngineRef
11 from searx.webutils import VALID_LANGUAGE_CODE
12 
13 
14 class QueryPartParser(ABC):
15 
16     __slots__ = "raw_text_query", "enable_autocomplete"
17 
18     @staticmethod
19     @abstractmethod
20     def check(raw_value):
21         """Check if raw_value can be parsed"""
22 
23     def __init__(self, raw_text_query, enable_autocomplete):
24         self.raw_text_query = raw_text_query
25         self.enable_autocomplete = enable_autocomplete
26 
27     @abstractmethod
28     def __call__(self, raw_value):
29         """Try to parse raw_value: set the self.raw_text_query properties
30 
31         return True if raw_value has been parsed
32 
33         self.raw_text_query.autocomplete_list is also modified
34         if self.enable_autocomplete is True
35         """
36 
37     def _add_autocomplete(self, value):
38         if value not in self.raw_text_query.autocomplete_list:
39             self.raw_text_query.autocomplete_list.append(value)
40 
41 
42 class TimeoutParser(QueryPartParser):
43     @staticmethod
44     def check(raw_value):
45         return raw_value[0] == '<'
46 
47     def __call__(self, raw_value):
48         value = raw_value[1:]
49         found = self._parse(value) if len(value) > 0 else False
50         if self.enable_autocomplete and not value:
51             self._autocomplete()
52         return found
53 
54     def _parse(self, value):
55         if not value.isdigit():
56             return False
57         raw_timeout_limit = int(value)
58         if raw_timeout_limit < 100:
59             # below 100, the unit is the second ( <3 = 3 seconds timeout )
60             self.raw_text_query.timeout_limit = float(raw_timeout_limit)
61         else:
62             # 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )
63             self.raw_text_query.timeout_limit = raw_timeout_limit / 1000.0
64         return True
65 
66     def _autocomplete(self):
67         for suggestion in ['<3', '<850']:
68             self._add_autocomplete(suggestion)
69 
70 
71 class LanguageParser(QueryPartParser):
72     @staticmethod
73     def check(raw_value):
74         return raw_value[0] == ':'
75 
76     def __call__(self, raw_value):
77         value = raw_value[1:].lower().replace('_', '-')
78         found = self._parse(value) if len(value) > 0 else False
79         if self.enable_autocomplete and not found:
80             self._autocomplete(value)
81         return found
82 
83     def _parse(self, value):
84         found = False
85         # check if any language-code is equal with
86         # declared language-codes
87         for lc in language_codes:
88             lang_id, lang_name, country, english_name = map(str.lower, lc)
89 
90             # if correct language-code is found
91             # set it as new search-language
92 
93             if (
94                 value == lang_id or value == lang_name or value == english_name or value.replace('-', ' ') == country
95             ) and value not in self.raw_text_query.languages:
96                 found = True
97                 lang_parts = lang_id.split('-')
98                 if len(lang_parts) == 2:
99                     self.raw_text_query.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())
100                 else:
101                     self.raw_text_query.languages.append(lang_id)
102                 # to ensure best match (first match is not necessarily the best one)
103                 if value == lang_id:
104                     break
105 
106         # user may set a valid, yet not selectable language
107         if VALID_LANGUAGE_CODE.match(value):
108             lang_parts = value.split('-')
109             if len(lang_parts) > 1:
110                 value = lang_parts[0].lower() + '-' + lang_parts[1].upper()
111             if value not in self.raw_text_query.languages:
112                 self.raw_text_query.languages.append(value)
113                 found = True
114 
115         return found
116 
117     def _autocomplete(self, value):
118         if not value:
119             # show some example queries
120             if len(settings['search']['languages']) < 10:
121                 for lang in settings['search']['languages']:
122                     self.raw_text_query.autocomplete_list.append(':' + lang)
123             else:
124                 for lang in [":en", ":en_us", ":english", ":united_kingdom"]:
125                     self.raw_text_query.autocomplete_list.append(lang)
126             return
127 
128         for lc in language_codes:
129             if lc[0] not in settings['search']['languages']:
130                 continue
131             lang_id, lang_name, country, english_name = map(str.lower, lc)
132 
133             # check if query starts with language-id
134             if lang_id.startswith(value):
135                 if len(value) <= 2:
136                     self._add_autocomplete(':' + lang_id.split('-')[0])
137                 else:
138                     self._add_autocomplete(':' + lang_id)
139 
140             # check if query starts with language name
141             if lang_name.startswith(value) or english_name.startswith(value):
142                 self._add_autocomplete(':' + lang_name)
143 
144             # check if query starts with country
145             # here "new_zealand" is "new-zealand" (see __call__)
146             if country.startswith(value.replace('-', ' ')):
147                 self._add_autocomplete(':' + country.replace(' ', '_'))
148 
149 
150 class ExternalBangParser(QueryPartParser):
151     @staticmethod
152     def check(raw_value):
153         return raw_value.startswith('!!')
154 
155     def __call__(self, raw_value):
156         value = raw_value[2:]
157         found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])
158         if self.enable_autocomplete:
159             self._autocomplete(bang_ac_list)
160         return found
161 
162     def _parse(self, value):
163         found = False
164         bang_definition, bang_ac_list = get_bang_definition_and_autocomplete(value)
165         if bang_definition is not None:
166             self.raw_text_query.external_bang = value
167             found = True
168         return found, bang_ac_list
169 
170     def _autocomplete(self, bang_ac_list):
171         if not bang_ac_list:
172             bang_ac_list = ['g', 'ddg', 'bing']
173         for external_bang in bang_ac_list:
174             self._add_autocomplete('!!' + external_bang)
175 
176 
177 class BangParser(QueryPartParser):
178     @staticmethod
179     def check(raw_value):
180         return raw_value[0] == '!' or raw_value[0] == '?'
181 
182     def __call__(self, raw_value):
183         value = raw_value[1:].replace('-', ' ').replace('_', ' ')
184         found = self._parse(value) if len(value) > 0 else False
185         if found and raw_value[0] == '!':
186             self.raw_text_query.specific = True
187         if self.enable_autocomplete:
188             self._autocomplete(raw_value[0], value)
189         return found
190 
191     def _parse(self, value):
192         # check if prefix is equal with engine shortcut
193         if value in engine_shortcuts:
194             value = engine_shortcuts[value]
195 
196         # check if prefix is equal with engine name
197         if value in engines:
198             self.raw_text_query.enginerefs.append(EngineRef(value, 'none'))
199             return True
200 
201         # check if prefix is equal with categorie name
202         if value in categories:
203             # using all engines for that search, which
204             # are declared under that categorie name
205             self.raw_text_query.enginerefs.extend(
206                 EngineRef(engine.name, value)
207                 for engine in categories[value]
208                 if (engine.name, value) not in self.raw_text_query.disabled_engines
209             )
210             return True
211 
212         return False
213 
214     def _autocomplete(self, first_char, value):
215         if not value:
216             # show some example queries
217             for suggestion in ['images', 'wikipedia', 'osm']:
218                 if suggestion not in self.raw_text_query.disabled_engines or suggestion in categories:
219                     self._add_autocomplete(first_char + suggestion)
220             return
221 
222         # check if query starts with categorie name
223         for category in categories:
224             if category.startswith(value):
225                 self._add_autocomplete(first_char + category.replace(' ', '_'))
226 
227         # check if query starts with engine name
228         for engine in engines:
229             if engine.startswith(value):
230                 self._add_autocomplete(first_char + engine.replace(' ', '_'))
231 
232         # check if query starts with engine shortcut
233         for engine_shortcut in engine_shortcuts:
234             if engine_shortcut.startswith(value):
235                 self._add_autocomplete(first_char + engine_shortcut)
236 
237 
238 class RawTextQuery:
239     """parse raw text query (the value from the html input)"""
240 
241     PARSER_CLASSES = [
242         TimeoutParser,  # this force the timeout
243         LanguageParser,  # this force a language
244         ExternalBangParser,  # external bang (must be before BangParser)
245         BangParser,  # this force a engine or category
246     ]
247 
248     def __init__(self, query, disabled_engines):
249         assert isinstance(query, str)
250         # input parameters
251         self.query = query
252         self.disabled_engines = disabled_engines if disabled_engines else []
253         # parsed values
254         self.enginerefs = []
255         self.languages = []
256         self.timeout_limit = None
257         self.external_bang = None
258         self.specific = False
259         self.autocomplete_list = []
260         # internal properties
261         self.query_parts = []  # use self.getFullQuery()
262         self.user_query_parts = []  # use self.getQuery()
263         self.autocomplete_location = None
264         self._parse_query()
265 
266     def _parse_query(self):
267         """
268         parse self.query, if tags are set, which
269         change the search engine or search-language
270         """
271 
272         # split query, including whitespaces
273         raw_query_parts = re.split(r'(\s+)', self.query)
274 
275         last_index_location = None
276         autocomplete_index = len(raw_query_parts) - 1
277 
278         for i, query_part in enumerate(raw_query_parts):
279             # part does only contain spaces, skip
280             if query_part.isspace() or query_part == '':
281                 continue
282 
283             # parse special commands
284             special_part = False
285             for parser_class in RawTextQuery.PARSER_CLASSES:
286                 if parser_class.check(query_part):
287                     special_part = parser_class(self, i == autocomplete_index)(query_part)
288                     break
289 
290             # append query part to query_part list
291             qlist = self.query_parts if special_part else self.user_query_parts
292             qlist.append(query_part)
293             last_index_location = (qlist, len(qlist) - 1)
294 
295         self.autocomplete_location = last_index_location
296 
297     def get_autocomplete_full_query(self, text):
298         qlist, position = self.autocomplete_location
299         qlist[position] = text
300         return self.getFullQuery()
301 
302     def changeQuery(self, query):
303         self.user_query_parts = query.strip().split()
304         self.query = self.getFullQuery()
305         self.autocomplete_location = (self.user_query_parts, len(self.user_query_parts) - 1)
306         self.autocomplete_list = []
307         return self
308 
309     def getQuery(self):
310         return ' '.join(self.user_query_parts)
311 
312     def getFullQuery(self):
313         """
314         get full querry including whitespaces
315         """
316         return '{0} {1}'.format(' '.join(self.query_parts), self.getQuery()).strip()
317 
318     def __str__(self):
319         return self.getFullQuery()
320 
321     def __repr__(self):
322         return (
323             f"<{self.__class__.__name__} "
324             + f"query={self.query!r} "
325             + f"disabled_engines={self.disabled_engines!r}\n  "
326             + f"languages={self.languages!r} "
327             + f"timeout_limit={self.timeout_limit!r} "
328             + f"external_bang={self.external_bang!r} "
329             + f"specific={self.specific!r} "
330             + f"enginerefs={self.enginerefs!r}\n  "
331             + f"autocomplete_list={self.autocomplete_list!r}\n  "
332             + f"query_parts={self.query_parts!r}\n  "
333             + f"user_query_parts={self.user_query_parts!r} >"
334         )
335
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/searx/query.py b/searx/query.py
--- a/searx/query.py
+++ b/searx/query.py
@@ -177,7 +177,7 @@
 class BangParser(QueryPartParser):
     @staticmethod
     def check(raw_value):
-        return raw_value[0] == '!' or raw_value[0] == '?'
+        return raw_value[0] == '!'
 
     def __call__(self, raw_value):
         value = raw_value[1:].replace('-', ' ').replace('_', ' ')

{"golden_diff": "diff --git a/searx/query.py b/searx/query.py\n--- a/searx/query.py\n+++ b/searx/query.py\n@@ -177,7 +177,7 @@\n class BangParser(QueryPartParser):\n     @staticmethod\n     def check(raw_value):\n-        return raw_value[0] == '!' or raw_value[0] == '?'\n+        return raw_value[0] == '!'\n \n     def __call__(self, raw_value):\n         value = raw_value[1:].replace('-', ' ').replace('_', ' ')\n", "issue": "`?` search syntax is broken\nThe [user documentation says](https://docs.searxng.org/user/search_syntax.html):\r\n  \r\n>**Prefix: `?`**\r\n>to add engines and categories to the currently selected categories\r\n\r\nBut to me it is unclear how it is different from the `!` bang syntax. The following queries all yield exactly the same results:\r\n\r\n* `!bing test !images`\r\n* `!bing test ?images`\r\n* `?bing test !images`\r\n\r\nOnly `?bing test ?images` for some reason only yields images??? :confused: \r\n\r\nCan somebody explain to me why somebody would want to use `?` over `!`?\r\n\r\nIf there is no satisfying answer to that question, I vote that we remove (or at least deprecate) the `?` syntax.\n", "before_files": [{"content": "# SPDX-License-Identifier: AGPL-3.0-or-later\n\nfrom abc import abstractmethod, ABC\nimport re\n\nfrom searx import settings\nfrom searx.languages import language_codes\nfrom searx.engines import categories, engines, engine_shortcuts\nfrom searx.external_bang import get_bang_definition_and_autocomplete\nfrom searx.search import EngineRef\nfrom searx.webutils import VALID_LANGUAGE_CODE\n\n\nclass QueryPartParser(ABC):\n\n    __slots__ = \"raw_text_query\", \"enable_autocomplete\"\n\n    @staticmethod\n    @abstractmethod\n    def check(raw_value):\n        \"\"\"Check if raw_value can be parsed\"\"\"\n\n    def __init__(self, raw_text_query, enable_autocomplete):\n        self.raw_text_query = raw_text_query\n        self.enable_autocomplete = enable_autocomplete\n\n    @abstractmethod\n    def __call__(self, raw_value):\n        \"\"\"Try to parse raw_value: set the self.raw_text_query properties\n\n        return True if raw_value has been parsed\n\n        self.raw_text_query.autocomplete_list is also modified\n        if self.enable_autocomplete is True\n        \"\"\"\n\n    def _add_autocomplete(self, value):\n        if value not in self.raw_text_query.autocomplete_list:\n            self.raw_text_query.autocomplete_list.append(value)\n\n\nclass TimeoutParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == '<'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:]\n        found = self._parse(value) if len(value) > 0 else False\n        if self.enable_autocomplete and not value:\n            self._autocomplete()\n        return found\n\n    def _parse(self, value):\n        if not value.isdigit():\n            return False\n        raw_timeout_limit = int(value)\n        if raw_timeout_limit < 100:\n            # below 100, the unit is the second ( <3 = 3 seconds timeout )\n            self.raw_text_query.timeout_limit = float(raw_timeout_limit)\n        else:\n            # 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )\n            self.raw_text_query.timeout_limit = raw_timeout_limit / 1000.0\n        return True\n\n    def _autocomplete(self):\n        for suggestion in ['<3', '<850']:\n            self._add_autocomplete(suggestion)\n\n\nclass LanguageParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == ':'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:].lower().replace('_', '-')\n        found = self._parse(value) if len(value) > 0 else False\n        if self.enable_autocomplete and not found:\n            self._autocomplete(value)\n        return found\n\n    def _parse(self, value):\n        found = False\n        # check if any language-code is equal with\n        # declared language-codes\n        for lc in language_codes:\n            lang_id, lang_name, country, english_name = map(str.lower, lc)\n\n            # if correct language-code is found\n            # set it as new search-language\n\n            if (\n                value == lang_id or value == lang_name or value == english_name or value.replace('-', ' ') == country\n            ) and value not in self.raw_text_query.languages:\n                found = True\n                lang_parts = lang_id.split('-')\n                if len(lang_parts) == 2:\n                    self.raw_text_query.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())\n                else:\n                    self.raw_text_query.languages.append(lang_id)\n                # to ensure best match (first match is not necessarily the best one)\n                if value == lang_id:\n                    break\n\n        # user may set a valid, yet not selectable language\n        if VALID_LANGUAGE_CODE.match(value):\n            lang_parts = value.split('-')\n            if len(lang_parts) > 1:\n                value = lang_parts[0].lower() + '-' + lang_parts[1].upper()\n            if value not in self.raw_text_query.languages:\n                self.raw_text_query.languages.append(value)\n                found = True\n\n        return found\n\n    def _autocomplete(self, value):\n        if not value:\n            # show some example queries\n            if len(settings['search']['languages']) < 10:\n                for lang in settings['search']['languages']:\n                    self.raw_text_query.autocomplete_list.append(':' + lang)\n            else:\n                for lang in [\":en\", \":en_us\", \":english\", \":united_kingdom\"]:\n                    self.raw_text_query.autocomplete_list.append(lang)\n            return\n\n        for lc in language_codes:\n            if lc[0] not in settings['search']['languages']:\n                continue\n            lang_id, lang_name, country, english_name = map(str.lower, lc)\n\n            # check if query starts with language-id\n            if lang_id.startswith(value):\n                if len(value) <= 2:\n                    self._add_autocomplete(':' + lang_id.split('-')[0])\n                else:\n                    self._add_autocomplete(':' + lang_id)\n\n            # check if query starts with language name\n            if lang_name.startswith(value) or english_name.startswith(value):\n                self._add_autocomplete(':' + lang_name)\n\n            # check if query starts with country\n            # here \"new_zealand\" is \"new-zealand\" (see __call__)\n            if country.startswith(value.replace('-', ' ')):\n                self._add_autocomplete(':' + country.replace(' ', '_'))\n\n\nclass ExternalBangParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value.startswith('!!')\n\n    def __call__(self, raw_value):\n        value = raw_value[2:]\n        found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])\n        if self.enable_autocomplete:\n            self._autocomplete(bang_ac_list)\n        return found\n\n    def _parse(self, value):\n        found = False\n        bang_definition, bang_ac_list = get_bang_definition_and_autocomplete(value)\n        if bang_definition is not None:\n            self.raw_text_query.external_bang = value\n            found = True\n        return found, bang_ac_list\n\n    def _autocomplete(self, bang_ac_list):\n        if not bang_ac_list:\n            bang_ac_list = ['g', 'ddg', 'bing']\n        for external_bang in bang_ac_list:\n            self._add_autocomplete('!!' + external_bang)\n\n\nclass BangParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == '!' or raw_value[0] == '?'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:].replace('-', ' ').replace('_', ' ')\n        found = self._parse(value) if len(value) > 0 else False\n        if found and raw_value[0] == '!':\n            self.raw_text_query.specific = True\n        if self.enable_autocomplete:\n            self._autocomplete(raw_value[0], value)\n        return found\n\n    def _parse(self, value):\n        # check if prefix is equal with engine shortcut\n        if value in engine_shortcuts:\n            value = engine_shortcuts[value]\n\n        # check if prefix is equal with engine name\n        if value in engines:\n            self.raw_text_query.enginerefs.append(EngineRef(value, 'none'))\n            return True\n\n        # check if prefix is equal with categorie name\n        if value in categories:\n            # using all engines for that search, which\n            # are declared under that categorie name\n            self.raw_text_query.enginerefs.extend(\n                EngineRef(engine.name, value)\n                for engine in categories[value]\n                if (engine.name, value) not in self.raw_text_query.disabled_engines\n            )\n            return True\n\n        return False\n\n    def _autocomplete(self, first_char, value):\n        if not value:\n            # show some example queries\n            for suggestion in ['images', 'wikipedia', 'osm']:\n                if suggestion not in self.raw_text_query.disabled_engines or suggestion in categories:\n                    self._add_autocomplete(first_char + suggestion)\n            return\n\n        # check if query starts with categorie name\n        for category in categories:\n            if category.startswith(value):\n                self._add_autocomplete(first_char + category.replace(' ', '_'))\n\n        # check if query starts with engine name\n        for engine in engines:\n            if engine.startswith(value):\n                self._add_autocomplete(first_char + engine.replace(' ', '_'))\n\n        # check if query starts with engine shortcut\n        for engine_shortcut in engine_shortcuts:\n            if engine_shortcut.startswith(value):\n                self._add_autocomplete(first_char + engine_shortcut)\n\n\nclass RawTextQuery:\n    \"\"\"parse raw text query (the value from the html input)\"\"\"\n\n    PARSER_CLASSES = [\n        TimeoutParser,  # this force the timeout\n        LanguageParser,  # this force a language\n        ExternalBangParser,  # external bang (must be before BangParser)\n        BangParser,  # this force a engine or category\n    ]\n\n    def __init__(self, query, disabled_engines):\n        assert isinstance(query, str)\n        # input parameters\n        self.query = query\n        self.disabled_engines = disabled_engines if disabled_engines else []\n        # parsed values\n        self.enginerefs = []\n        self.languages = []\n        self.timeout_limit = None\n        self.external_bang = None\n        self.specific = False\n        self.autocomplete_list = []\n        # internal properties\n        self.query_parts = []  # use self.getFullQuery()\n        self.user_query_parts = []  # use self.getQuery()\n        self.autocomplete_location = None\n        self._parse_query()\n\n    def _parse_query(self):\n        \"\"\"\n        parse self.query, if tags are set, which\n        change the search engine or search-language\n        \"\"\"\n\n        # split query, including whitespaces\n        raw_query_parts = re.split(r'(\\s+)', self.query)\n\n        last_index_location = None\n        autocomplete_index = len(raw_query_parts) - 1\n\n        for i, query_part in enumerate(raw_query_parts):\n            # part does only contain spaces, skip\n            if query_part.isspace() or query_part == '':\n                continue\n\n            # parse special commands\n            special_part = False\n            for parser_class in RawTextQuery.PARSER_CLASSES:\n                if parser_class.check(query_part):\n                    special_part = parser_class(self, i == autocomplete_index)(query_part)\n                    break\n\n            # append query part to query_part list\n            qlist = self.query_parts if special_part else self.user_query_parts\n            qlist.append(query_part)\n            last_index_location = (qlist, len(qlist) - 1)\n\n        self.autocomplete_location = last_index_location\n\n    def get_autocomplete_full_query(self, text):\n        qlist, position = self.autocomplete_location\n        qlist[position] = text\n        return self.getFullQuery()\n\n    def changeQuery(self, query):\n        self.user_query_parts = query.strip().split()\n        self.query = self.getFullQuery()\n        self.autocomplete_location = (self.user_query_parts, len(self.user_query_parts) - 1)\n        self.autocomplete_list = []\n        return self\n\n    def getQuery(self):\n        return ' '.join(self.user_query_parts)\n\n    def getFullQuery(self):\n        \"\"\"\n        get full querry including whitespaces\n        \"\"\"\n        return '{0} {1}'.format(' '.join(self.query_parts), self.getQuery()).strip()\n\n    def __str__(self):\n        return self.getFullQuery()\n\n    def __repr__(self):\n        return (\n            f\"<{self.__class__.__name__} \"\n            + f\"query={self.query!r} \"\n            + f\"disabled_engines={self.disabled_engines!r}\\n  \"\n            + f\"languages={self.languages!r} \"\n            + f\"timeout_limit={self.timeout_limit!r} \"\n            + f\"external_bang={self.external_bang!r} \"\n            + f\"specific={self.specific!r} \"\n            + f\"enginerefs={self.enginerefs!r}\\n  \"\n            + f\"autocomplete_list={self.autocomplete_list!r}\\n  \"\n            + f\"query_parts={self.query_parts!r}\\n  \"\n            + f\"user_query_parts={self.user_query_parts!r} >\"\n        )\n", "path": "searx/query.py"}], "after_files": [{"content": "# SPDX-License-Identifier: AGPL-3.0-or-later\n\nfrom abc import abstractmethod, ABC\nimport re\n\nfrom searx import settings\nfrom searx.languages import language_codes\nfrom searx.engines import categories, engines, engine_shortcuts\nfrom searx.external_bang import get_bang_definition_and_autocomplete\nfrom searx.search import EngineRef\nfrom searx.webutils import VALID_LANGUAGE_CODE\n\n\nclass QueryPartParser(ABC):\n\n    __slots__ = \"raw_text_query\", \"enable_autocomplete\"\n\n    @staticmethod\n    @abstractmethod\n    def check(raw_value):\n        \"\"\"Check if raw_value can be parsed\"\"\"\n\n    def __init__(self, raw_text_query, enable_autocomplete):\n        self.raw_text_query = raw_text_query\n        self.enable_autocomplete = enable_autocomplete\n\n    @abstractmethod\n    def __call__(self, raw_value):\n        \"\"\"Try to parse raw_value: set the self.raw_text_query properties\n\n        return True if raw_value has been parsed\n\n        self.raw_text_query.autocomplete_list is also modified\n        if self.enable_autocomplete is True\n        \"\"\"\n\n    def _add_autocomplete(self, value):\n        if value not in self.raw_text_query.autocomplete_list:\n            self.raw_text_query.autocomplete_list.append(value)\n\n\nclass TimeoutParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == '<'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:]\n        found = self._parse(value) if len(value) > 0 else False\n        if self.enable_autocomplete and not value:\n            self._autocomplete()\n        return found\n\n    def _parse(self, value):\n        if not value.isdigit():\n            return False\n        raw_timeout_limit = int(value)\n        if raw_timeout_limit < 100:\n            # below 100, the unit is the second ( <3 = 3 seconds timeout )\n            self.raw_text_query.timeout_limit = float(raw_timeout_limit)\n        else:\n            # 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )\n            self.raw_text_query.timeout_limit = raw_timeout_limit / 1000.0\n        return True\n\n    def _autocomplete(self):\n        for suggestion in ['<3', '<850']:\n            self._add_autocomplete(suggestion)\n\n\nclass LanguageParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == ':'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:].lower().replace('_', '-')\n        found = self._parse(value) if len(value) > 0 else False\n        if self.enable_autocomplete and not found:\n            self._autocomplete(value)\n        return found\n\n    def _parse(self, value):\n        found = False\n        # check if any language-code is equal with\n        # declared language-codes\n        for lc in language_codes:\n            lang_id, lang_name, country, english_name = map(str.lower, lc)\n\n            # if correct language-code is found\n            # set it as new search-language\n\n            if (\n                value == lang_id or value == lang_name or value == english_name or value.replace('-', ' ') == country\n            ) and value not in self.raw_text_query.languages:\n                found = True\n                lang_parts = lang_id.split('-')\n                if len(lang_parts) == 2:\n                    self.raw_text_query.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())\n                else:\n                    self.raw_text_query.languages.append(lang_id)\n                # to ensure best match (first match is not necessarily the best one)\n                if value == lang_id:\n                    break\n\n        # user may set a valid, yet not selectable language\n        if VALID_LANGUAGE_CODE.match(value):\n            lang_parts = value.split('-')\n            if len(lang_parts) > 1:\n                value = lang_parts[0].lower() + '-' + lang_parts[1].upper()\n            if value not in self.raw_text_query.languages:\n                self.raw_text_query.languages.append(value)\n                found = True\n\n        return found\n\n    def _autocomplete(self, value):\n        if not value:\n            # show some example queries\n            if len(settings['search']['languages']) < 10:\n                for lang in settings['search']['languages']:\n                    self.raw_text_query.autocomplete_list.append(':' + lang)\n            else:\n                for lang in [\":en\", \":en_us\", \":english\", \":united_kingdom\"]:\n                    self.raw_text_query.autocomplete_list.append(lang)\n            return\n\n        for lc in language_codes:\n            if lc[0] not in settings['search']['languages']:\n                continue\n            lang_id, lang_name, country, english_name = map(str.lower, lc)\n\n            # check if query starts with language-id\n            if lang_id.startswith(value):\n                if len(value) <= 2:\n                    self._add_autocomplete(':' + lang_id.split('-')[0])\n                else:\n                    self._add_autocomplete(':' + lang_id)\n\n            # check if query starts with language name\n            if lang_name.startswith(value) or english_name.startswith(value):\n                self._add_autocomplete(':' + lang_name)\n\n            # check if query starts with country\n            # here \"new_zealand\" is \"new-zealand\" (see __call__)\n            if country.startswith(value.replace('-', ' ')):\n                self._add_autocomplete(':' + country.replace(' ', '_'))\n\n\nclass ExternalBangParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value.startswith('!!')\n\n    def __call__(self, raw_value):\n        value = raw_value[2:]\n        found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])\n        if self.enable_autocomplete:\n            self._autocomplete(bang_ac_list)\n        return found\n\n    def _parse(self, value):\n        found = False\n        bang_definition, bang_ac_list = get_bang_definition_and_autocomplete(value)\n        if bang_definition is not None:\n            self.raw_text_query.external_bang = value\n            found = True\n        return found, bang_ac_list\n\n    def _autocomplete(self, bang_ac_list):\n        if not bang_ac_list:\n            bang_ac_list = ['g', 'ddg', 'bing']\n        for external_bang in bang_ac_list:\n            self._add_autocomplete('!!' + external_bang)\n\n\nclass BangParser(QueryPartParser):\n    @staticmethod\n    def check(raw_value):\n        return raw_value[0] == '!'\n\n    def __call__(self, raw_value):\n        value = raw_value[1:].replace('-', ' ').replace('_', ' ')\n        found = self._parse(value) if len(value) > 0 else False\n        if found and raw_value[0] == '!':\n            self.raw_text_query.specific = True\n        if self.enable_autocomplete:\n            self._autocomplete(raw_value[0], value)\n        return found\n\n    def _parse(self, value):\n        # check if prefix is equal with engine shortcut\n        if value in engine_shortcuts:\n            value = engine_shortcuts[value]\n\n        # check if prefix is equal with engine name\n        if value in engines:\n            self.raw_text_query.enginerefs.append(EngineRef(value, 'none'))\n            return True\n\n        # check if prefix is equal with categorie name\n        if value in categories:\n            # using all engines for that search, which\n            # are declared under that categorie name\n            self.raw_text_query.enginerefs.extend(\n                EngineRef(engine.name, value)\n                for engine in categories[value]\n                if (engine.name, value) not in self.raw_text_query.disabled_engines\n            )\n            return True\n\n        return False\n\n    def _autocomplete(self, first_char, value):\n        if not value:\n            # show some example queries\n            for suggestion in ['images', 'wikipedia', 'osm']:\n                if suggestion not in self.raw_text_query.disabled_engines or suggestion in categories:\n                    self._add_autocomplete(first_char + suggestion)\n            return\n\n        # check if query starts with categorie name\n        for category in categories:\n            if category.startswith(value):\n                self._add_autocomplete(first_char + category.replace(' ', '_'))\n\n        # check if query starts with engine name\n        for engine in engines:\n            if engine.startswith(value):\n                self._add_autocomplete(first_char + engine.replace(' ', '_'))\n\n        # check if query starts with engine shortcut\n        for engine_shortcut in engine_shortcuts:\n            if engine_shortcut.startswith(value):\n                self._add_autocomplete(first_char + engine_shortcut)\n\n\nclass RawTextQuery:\n    \"\"\"parse raw text query (the value from the html input)\"\"\"\n\n    PARSER_CLASSES = [\n        TimeoutParser,  # this force the timeout\n        LanguageParser,  # this force a language\n        ExternalBangParser,  # external bang (must be before BangParser)\n        BangParser,  # this force a engine or category\n    ]\n\n    def __init__(self, query, disabled_engines):\n        assert isinstance(query, str)\n        # input parameters\n        self.query = query\n        self.disabled_engines = disabled_engines if disabled_engines else []\n        # parsed values\n        self.enginerefs = []\n        self.languages = []\n        self.timeout_limit = None\n        self.external_bang = None\n        self.specific = False\n        self.autocomplete_list = []\n        # internal properties\n        self.query_parts = []  # use self.getFullQuery()\n        self.user_query_parts = []  # use self.getQuery()\n        self.autocomplete_location = None\n        self._parse_query()\n\n    def _parse_query(self):\n        \"\"\"\n        parse self.query, if tags are set, which\n        change the search engine or search-language\n        \"\"\"\n\n        # split query, including whitespaces\n        raw_query_parts = re.split(r'(\\s+)', self.query)\n\n        last_index_location = None\n        autocomplete_index = len(raw_query_parts) - 1\n\n        for i, query_part in enumerate(raw_query_parts):\n            # part does only contain spaces, skip\n            if query_part.isspace() or query_part == '':\n                continue\n\n            # parse special commands\n            special_part = False\n            for parser_class in RawTextQuery.PARSER_CLASSES:\n                if parser_class.check(query_part):\n                    special_part = parser_class(self, i == autocomplete_index)(query_part)\n                    break\n\n            # append query part to query_part list\n            qlist = self.query_parts if special_part else self.user_query_parts\n            qlist.append(query_part)\n            last_index_location = (qlist, len(qlist) - 1)\n\n        self.autocomplete_location = last_index_location\n\n    def get_autocomplete_full_query(self, text):\n        qlist, position = self.autocomplete_location\n        qlist[position] = text\n        return self.getFullQuery()\n\n    def changeQuery(self, query):\n        self.user_query_parts = query.strip().split()\n        self.query = self.getFullQuery()\n        self.autocomplete_location = (self.user_query_parts, len(self.user_query_parts) - 1)\n        self.autocomplete_list = []\n        return self\n\n    def getQuery(self):\n        return ' '.join(self.user_query_parts)\n\n    def getFullQuery(self):\n        \"\"\"\n        get full querry including whitespaces\n        \"\"\"\n        return '{0} {1}'.format(' '.join(self.query_parts), self.getQuery()).strip()\n\n    def __str__(self):\n        return self.getFullQuery()\n\n    def __repr__(self):\n        return (\n            f\"<{self.__class__.__name__} \"\n            + f\"query={self.query!r} \"\n            + f\"disabled_engines={self.disabled_engines!r}\\n  \"\n            + f\"languages={self.languages!r} \"\n            + f\"timeout_limit={self.timeout_limit!r} \"\n            + f\"external_bang={self.external_bang!r} \"\n            + f\"specific={self.specific!r} \"\n            + f\"enginerefs={self.enginerefs!r}\\n  \"\n            + f\"autocomplete_list={self.autocomplete_list!r}\\n  \"\n            + f\"query_parts={self.query_parts!r}\\n  \"\n            + f\"user_query_parts={self.user_query_parts!r} >\"\n        )\n", "path": "searx/query.py"}]}