diff --git a/API.md b/API.md index abdc8425..79c1e6eb 100644 --- a/API.md +++ b/API.md @@ -2258,6 +2258,9 @@ Date/time values can be of following form: Some fields, such as user names, can take wildcards (`*`). +You can escape special characters such as `:` and `-` by prepending them with a +backslash: `\\`. + **Example** Searching for posts with following query: @@ -2266,3 +2269,8 @@ Searching for posts with following query: will show flash files tagged as sea, that were liked by seven people at most, uploaded by user Pirate. + +Searching for posts with `re:zero` will show an error message about unknown +named token. + +Searching for posts with `re\:zero` will show posts tagged with `re:zero`. diff --git a/client/html/help_search_general.tpl b/client/html/help_search_general.tpl index a7b05d83..6a5cfd58 100644 --- a/client/html/help_search_general.tpl +++ b/client/html/help_search_general.tpl @@ -80,6 +80,9 @@ take following form:

,desc to control the sort direction, which can be also controlled by negating the whole token.

+

You can escape special characters such as : and - +by prepending them with a backslash: \\.

+

Example

Searching for posts with following query:

@@ -89,3 +92,8 @@ by negating the whole token.

will show flash files tagged as sea, that were liked by seven people at most, uploaded by user Pirate.

+

Searching for posts with re:zero will show an error message +about unknown named token.

+ +

Searching for posts with re\:zero will show posts tagged with +re:zero.

diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 34e86537..1dd85149 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -10,15 +10,6 @@ from szurubooru.search.configs.base_search_config import ( BaseSearchConfig, Filter) -def _enum_transformer(available_values: Dict[str, Any], value: str) -> str: - try: - return available_values[value.lower()] - except KeyError: - raise errors.SearchError( - 'Invalid value: %r. Possible values: %r.' % ( - value, list(sorted(available_values.keys())))) - - def _type_transformer(value: str) -> str: available_values = { 'image': model.Post.TYPE_IMAGE, @@ -31,7 +22,7 @@ def _type_transformer(value: str) -> str: 'flash': model.Post.TYPE_FLASH, 'swf': model.Post.TYPE_FLASH, } - return _enum_transformer(available_values, value) + return search_util.enum_transformer(available_values, value) def _safety_transformer(value: str) -> str: @@ -41,7 +32,7 @@ def _safety_transformer(value: str) -> str: 'questionable': model.Post.SAFETY_SKETCHY, 'unsafe': model.Post.SAFETY_UNSAFE, } - return _enum_transformer(available_values, value) + return search_util.enum_transformer(available_values, value) def _create_score_filter(score: int) -> Filter: diff --git a/server/szurubooru/search/configs/util.py b/server/szurubooru/search/configs/util.py index c6b9b783..ac4bd8ee 100644 --- a/server/szurubooru/search/configs/util.py +++ b/server/szurubooru/search/configs/util.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union, Callable +from typing import Any, Optional, Union, Dict, Callable import sqlalchemy as sa from szurubooru import db, errors from szurubooru.func import util @@ -8,27 +8,62 @@ from szurubooru.search.configs.base_search_config import Filter Number = Union[int, float] +WILDCARD = '(--wildcard--)' # something unlikely to be used by the users + + +def unescape(text: str, make_wildcards_special: bool = False) -> str: + output = '' + i = 0 + while i < len(text): + if text[i] == '\\': + try: + char = text[i+1] + i += 1 + except IndexError: + raise errors.SearchError( + 'Unterminated escape sequence (did you forget to escape ' + 'the ending backslash?)') + if char not in '*\\:-.,': + raise errors.SearchError( + 'Unknown escape sequence (did you forget to escape ' + 'the backslash?)') + elif text[i] == '*' and make_wildcards_special: + char = WILDCARD + else: + char = text[i] + output += char + i += 1 + return output def wildcard_transformer(value: str) -> str: return ( - value + unescape(value, make_wildcards_special=True) .replace('\\', '\\\\') .replace('%', '\\%') .replace('_', '\\_') - .replace('*', '%')) + .replace(WILDCARD, '%')) + + +def enum_transformer(available_values: Dict[str, Any], value: str) -> str: + try: + return available_values[unescape(value.lower())] + except KeyError: + raise errors.SearchError( + 'Invalid value: %r. Possible values: %r.' % ( + value, list(sorted(available_values.keys())))) def integer_transformer(value: str) -> int: - return int(value) + return int(unescape(value)) def float_transformer(value: str) -> float: for sep in list('/:'): if sep in value: a, b = value.split(sep, 1) - return float(a) / float(b) - return float(value) + return float(unescape(a)) / float(unescape(b)) + return float(unescape(value)) def apply_num_criterion_to_column( @@ -84,23 +119,23 @@ def apply_str_criterion_to_column( for value in criterion.values: expr = expr | column.ilike(transformer(value)) elif isinstance(criterion, criteria.RangedCriterion): - expr = column.ilike(transformer(criterion.original_text)) + raise errors.SearchError( + 'Ranged criterion is invalid in this context. ' + 'Did you forget to escape the dots?') else: assert False return expr def create_str_filter( - column: SaColumn, - transformer: Callable[[str], str]=wildcard_transformer + column: SaColumn, transformer: Callable[[str], str]=wildcard_transformer ) -> Filter: def wrapper( query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool) -> SaQuery: assert criterion - expr = apply_str_criterion_to_column( - column, criterion, transformer) + expr = apply_str_criterion_to_column(column, criterion, transformer) if negated: expr = ~expr return query.filter(expr) diff --git a/server/szurubooru/search/parser.py b/server/szurubooru/search/parser.py index 6369bc6e..50d8fb0e 100644 --- a/server/szurubooru/search/parser.py +++ b/server/szurubooru/search/parser.py @@ -1,17 +1,20 @@ import re -from typing import List +from typing import Match, List from szurubooru import errors from szurubooru.search import criteria, tokens from szurubooru.search.query import SearchQuery +from szurubooru.search.configs import util def _create_criterion( original_value: str, value: str) -> criteria.BaseCriterion: - if ',' in value: - return criteria.ArrayCriterion( - original_value, value.split(',')) - if '..' in value: - low, high = value.split('..', 1) + if re.search(r'(?