diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py index 55292ba0c8..96ec60dd11 100644 --- a/src/calibre/ebooks/metadata/book/render.py +++ b/src/calibre/ebooks/metadata/book/render.py @@ -70,7 +70,8 @@ def search_action_with_data(search_term, value, book_id, field=None, **k): def web_search_link(template, mi, value): formatter = SafeFormat() iv = str(value) - mi.set('item_value', qquote(iv, True)) + mi.set('item_value', iv) + mi.set('item_value_quoted', qquote(iv, True)) mi.set('item_value_no_plus', qquote(iv, False)) u = formatter.safe_format(template, mi, 'BOOK DETAILS WEB LINK', mi) if u: diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 203cba2000..4e872a843d 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -542,12 +542,17 @@ class CreateCustomColumn(QDialog): l.addWidget(self.web_search_label) wst = self.web_search_template = QLineEdit() wst.setToolTip('
' + _( - 'Fill in this box if you want clicking on the value in book details to do a ' + "Fill in this box if you want clicking on the value in book details to do a " "web search instead of searching your calibre library. The book's metadata is " - "available to the template. Additional fields '{0}' and '{1}' are also available to the " - 'template. For multiple-valued (tags-like) columns they are the value being examined, ' - 'telling you which value to use to generate the link. These two values are automatically escaped for use in URLs.').format( - 'item_value', 'item_value_no_plus') + '
') + "available to the template.Additional fields '{0}', `{1}`, and '{2}' are also available " + "to the template. For multiple-valued (tags-like) columns they are the value being examined, " + "telling you which value to use to generate the link. The two values '{1}' and '{2}' are " + "automatically escaped for use in URLs. In '{1}', spaces are replaced by plus signs. In '{2}' " + "spaces are replaced by '%20'.
The template functions '{3}' (the easiest to use), " + "'{4}', '{5}', and '{6}' are useful for constructing the desired URL. There are examples in " + "the template function documentation.").format( + 'item_value', 'item_value_quoted', 'item_value_no_plus', 'make_url()', 'make_url_extended()', + 'query_string()', 'quote_for_url()') + '
') l.addWidget(wst) self.web_search_label.setBuddy(wst) wst_tb = self.web_search_toolbutton = QToolButton() @@ -563,17 +568,21 @@ class CreateCustomColumn(QDialog): db = self.gui.current_db.new_api lv = self.gui.library_view rows = lv.selectionModel().selectedRows() + from calibre.ebooks.metadata.search_internet import qquote if not self.editing_col or not rows: - vals = [{'value': _('Value'), 'lookup_name': _('Lookup name'), 'author': _('Author'), + vals = [{'item_value': _('Item Value'), + 'item_value_quoted': qquote(_('Item Value'), True), + 'item_value_no_plus': qquote(_('Item Value'), False), + 'lookup_name': _('Lookup name'),'author': _('Author'), 'title': _('Title'), 'author_sort': _('Author sort')}] else: - from calibre.ebooks.metadata.search_internet import qquote vals = [] for row in rows: book_id = lv.model().id(row) mi = db.new_api.get_metadata(book_id) - mi.set('item_value', qquote('Item Value', True)) - mi.set('item_value_no_plus', qquote('Item Value', False)) + mi.set('item_value', _('Item Value')) + mi.set('item_value_quoted', qquote(_('Item Value'), True)) + mi.set('item_value_no_plus', qquote(_('Item Value'), False)) vals.append(mi) d = TemplateDialog(parent=self, text=self.web_search_template.text(), mi=vals) if d.exec() == QDialog.DialogCode.Accepted: @@ -682,8 +691,7 @@ class CreateCustomColumn(QDialog): self.comments_type.setVisible(is_comments) self.comments_type_label.setVisible(is_comments) - has_url_template = not is_comments and col_type in ('text', '*text', 'composite', '*composite', - 'series', 'enumeration') + has_url_template = col_type in ('text', '*text', 'composite', '*composite', 'series', 'enumeration') self.web_search_label.setVisible(has_url_template) self.web_search_template.setVisible(has_url_template) self.web_search_toolbutton.setVisible(has_url_template) @@ -962,21 +970,26 @@ class CreateNewCustomColumn: 'make_category': True or False -- whether the column is shown in the tag browser 'contains_html': True or False -- whether the column is interpreted as HTML 'use_decorations': True or False -- should check marks be displayed + 'search_template': a template used to construct a search URL for book details datetime columns: 'date_format': a string specifying the display format enumerated columns 'enum_values': a string containing comma-separated valid values for an enumeration 'enum_colors': a string containing comma-separated colors for an enumeration 'use_decorations': True or False -- should check marks be displayed + 'search_template': a template used to construct a search URL for book details float columns: 'decimals': the number of decimal digits to allow when editing (int). Range: 1 - 9 float and int columns: 'number_format': the format to apply when displaying the column rating columns: 'allow_half_stars': True or False -- are half-stars allowed + series columns: + 'search_template': a template used to construct a search URL for book details text columns: 'is_names': True or False -- whether the items are comma or ampersand separated 'use_decorations': True or False -- should check marks be displayed + 'search_template': a template used to construct a search URL for book details This method returns a tuple (Result.enum_value, message). If tuple[0] is Result.COLUMN_ADDED then the message is the lookup name including the '#'. diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 9882694433..d690f7494d 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -30,6 +30,7 @@ from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN from calibre.db.notes.exim import expand_note_resources, parse_html from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata.book.base import field_metadata +from calibre.ebooks.metadata.search_internet import qquote from calibre.utils.config import tweaks from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date from calibre.utils.icu import capitalize, sort_key, strcmp @@ -55,6 +56,7 @@ FORMATTING_VALUES = _('Formatting values') CASE_CHANGES = _('Case changes') DATE_FUNCTIONS = _('Date functions') DB_FUNCS = _('Database functions') +URL_FUNCTIONS = _('URL functions') # Class and method to save an untranslated copy of translated strings @@ -2780,7 +2782,7 @@ of templates. class BuiltinToHex(BuiltinFormatterFunction): name = 'to_hex' arg_count = 1 - category = STRING_MANIPULATION + category = URL_FUNCTIONS __doc__ = doc = _( r''' ``to_hex(val)`` -- returns the string ``val`` encoded into hex.[/] This is useful @@ -2794,7 +2796,7 @@ when constructing calibre URLs. class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction): name = 'urls_from_identifiers' arg_count = 2 - category = FORMATTING_VALUES + category = URL_FUNCTIONS __doc__ = doc = _( r''' ``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated @@ -3213,6 +3215,203 @@ data without converting it to a string first. Example: ``list_count_field('tags' raise NotImplementedError() +class BuiltinMakeUrl(BuiltinFormatterFunction): + name = 'make_url' + arg_count = -1 + category = URL_FUNCTIONS + __doc__ = doc = _( +r''' +``make_url(path, [query_name, query_value]+)`` -- this function is the easiest way +to construct a query URL. It uses a ``path``, the web site and page you want to +query, and ``query_name``, ``query_value`` pairs from which the query is built. +In general, the ``query_value`` must be URL-encoded. With this function it is always +encoded and spaces are always replaced with ``'+'`` signs. + +At least one ``query_name, query_value`` pair must be provided. + +Example: constructing a Wikipedia search URL for the author `Niccolò Machiavelli`: +[CODE] +make_url('https://en.wikipedia.org/w/index.php', 'search', 'Niccolò Machiavelli') +[/CODE] +returns +[CODE] +https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli +[/CODE] + +If you are writing a custom column book details URL template then use ``$item_name`` or +``field('item_name')`` to obtain the value of the field that was clicked on. +Example: if `Niccolò Machiavelli` was clicked then you can construct the URL using: +[CODE] +make_url('https://en.wikipedia.org/w/index.php', 'search', $item_name) +[/CODE] + +See also the functions :ref:`make_url_extended`, :ref:`query_string` and :ref:`encode_for_url`. +''') + + def evaluate(self, formatter, kwargs, mi, locals, path, *args): + if (len(args) % 2) != 0: + raise ValueError(_('{} requires an odd number of arguments').format('make_url')) + if len(args) < 2: + raise ValueError(_('{} requires at least 3 arguments').format('make_url')) + query_args = [] + for i in range(0, len(args), 2): + query_args.append(f'{args[i]}={qquote(args[i+1].strip())}') + return f'{path}?{"&".join(query_args)}' + + +class BuiltinMakeUrlExtended(BuiltinFormatterFunction): + name = 'make_url_extended' + arg_count = -1 + category = URL_FUNCTIONS + __doc__ = doc = _( +r''' +``make_url_extended(...)`` -- this function is similar to :ref:`make_url` but +gives you more control over the URL components. The components of a URL are + +[B]scheme[/B]:://[B]authority[/B]/[B]path[/B]?[B]query string[/B]. + +See [URL href="https://en.wikipedia.org/wiki/URL"]Uniform Resource Locater[/URL] on Wikipedia for more detail. + +The function has two variants: +[CODE] +make_url_extended(scheme, authority, path, [query_name, query_value]+) +[/CODE] +and +[CODE] +make_url_extended(scheme, authority, path, query_string) +[/CODE] +[/] +This function returns a URL constructed from the ``scheme``, ``authority``, ``path``, +and either the ``query_string`` or a query string constructed from the query argument pairs. +You must supply either a ``query_string`` or at least one ``query_name, query_value`` pair. +If you supply ``query_string`` and it is empty then the resulting URL will not have a query string section. + +Example 1: constructing a Wikipedia search URL for the author `Niccolò Machiavelli`: +[CODE] +make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', 'Niccolò Machiavelli') +[/CODE] +returns +[CODE] +https://en.wikipedia.org/w/index.php?search=Niccol%C3%B2+Machiavelli +[/CODE] + +See the :ref:`query_string`() function for an example using ``make_url_extended()`` with a ``query_string``. + +If you are writing a custom column book details URL template then use ``$item_name`` or +``field('item_name')`` to obtain the value of the field that was clicked on. +Example: if `Niccolò Machiavelli` was clicked on then you can construct the URL using : +[CODE] +make_url_extended('https', 'en.wikipedia.org', '/w/index.php', 'search', $item_name') +[/CODE] + +See also the functions :ref:`make_url`, :ref:`query_string` and :ref:`encode_for_url`. +''') + + def evaluate(self, formatter, kwargs, mi, locals, scheme, host, path, *args): + if len(args) != 1: + if (len(args) % 2) != 0: + raise ValueError(_('{} requires an odd number of arguments').format('make_url_extended')) + if len(args) < 2: + raise ValueError(_('{} requires at least 5 arguments').format('make_url_extended')) + query_args = [] + for i in range(0, len(args), 2): + query_args.append(f'{args[i]}={qquote(args[i+1].strip())}') + qs = '&'.join(query_args) + else: + qs = args[0] + if qs: + qs = '?' + qs + return f"{scheme}://{host}/{path[1:] if path.startswith('/') else path}{qs}" + + +class BuiltinQueryString(BuiltinFormatterFunction): + name = 'query_string' + arg_count = -1 + category = URL_FUNCTIONS + __doc__ = doc = _( +r''' +``query_string([query_name, query_value, how_to_encode]+)``-- returns a URL query string +constructed from the ``query_name, query_value, how_to_encode`` triads. +A query string is a series of items where each item looks like ``query_name=query_value`` +where ``query_value`` is URL-encoded as instructed. The query items are separated by +``'&'`` (ampersand) characters. + +If ``how_to_encode`` is ``0`` then ``query_value`` is encoded and spaces are replaced +with ``'+'`` (plus) signs. If ``how_to_encode`` is ``1`` then ``query_value`` is +encoded with spaces replaced by ``%20``. If ``how_to_encode`` is ``2`` then ``query_value`` +is returned unchanged; no encoding is done and spaces are not replaced. If you want +``query_value`` not to be encoded but spaces to be replaced then use the :ref:`re` +function, as in ``re($series, ' ', '%20')`` + +You use this function if you need specific control over how the parts of the +query string are constructed. You could then use the resultingquery string in +:ref:`make_url_extended`, as in +[CODE] +make_url_extended( + 'https', 'your_host', 'your_path', + query_string('encoded', 'Hendrik Bäßler', 0, 'unencoded', 'Hendrik Bäßler', 2)) +[/CODE] +giving you +[CODE] +https://your_host/your_path?encoded=Hendrik+B%C3%A4%C3%9Fler&unencoded=Hendrik Bäßler +[/CODE] + +You must have at least one ``query_name, query_value, how_to_encode`` triad, but can +have as many as you wish. + +The returned value is a URL query string with all the specified items, for example: +``name1=val1[&nameN=valN]*``. Note that the ``'?'`` `path` / `query string` separator +is not included in the returned result. + +If you are writing a custom column book details URL template then use ``$item_name`` or +``field('item_name')`` to obtain the unencoded value of the field that was clicked. +You also have ``item_value_quoted`` where the value is already encoded with plus signs +replacing spaces, and ``item_value_no_plus`` where the value is already encoded +with ``%20`` replacing spaces. + +See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`encode_for_url`. +''') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + if (len(args) % 3) != 0 or len(args) < 3: + raise ValueError(_('{} requires at least one group of 3 arguments').format('query_string')) + funcs = [ + partial(qquote, use_plus=True), + partial(qquote, use_plus=False), + lambda x:x, + ] + query_args = [] + for i in range(0, len(args), 3): + if (f := args[i+2]) not in ('0', '1', '2'): + raise ValueError( + _('In {} the third argument of a group must be 0, 1, or 2, not {}').format('query_string', f)) + query_args.append(f'{args[i]}={funcs[int(f)](args[i+1].strip())}') + return "&".join(query_args) + + +class BuiltinEncodeForURL(BuiltinFormatterFunction): + name = 'encode_for_url' + arg_count = 2 + category = URL_FUNCTIONS + __doc__ = doc = _( +r''' +``encode_for_url(value, use_plus)`` -- returns the ``value`` encoded for use in a URL as +specified by ``use_plus``. The value is first URL-encoded. Next, if ``use_plus`` is ``0`` then +spaces are replaced by ``'+'`` (plus) signs. If it is ``1`` then spaces are replaced by ``%20``. + +If you do not want the value to be encoding but to have spaces replaced then use the +:ref:`re` function, as in ``re($series, ' ', '%20')`` + +See also the functions :ref:`make_url`, :ref:`make_url_extended` and :ref:`query_string`. +''') + + def evaluate(self, formatter, kwargs, mi, locals, value, use_plus): + if use_plus not in ('0', '1'): + raise ValueError( + _('In {} the second argument must be 0, or 1, not {}').format('quote_for_url', use_plus)) + return qquote(value, use_plus=use_plus=='0') + + _formatter_builtins = [ BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(), BuiltinAssign(), @@ -3222,7 +3421,7 @@ _formatter_builtins = [ BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(), - BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), + BuiltinDaysBetween(), BuiltinDivide(), BuiltinEncodeForURL(), BuiltinEval(), BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(), BuiltinFieldListCount(), BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(), BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(), @@ -3237,9 +3436,10 @@ _formatter_builtins = [ BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(), BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(), BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(), - BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(), - BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRange(), - BuiltinRawField(), BuiltinRawList(), + BuiltinLowercase(), BuiltinMakeUrl(), BuiltinMakeUrlExtended(), BuiltinMod(), + BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(), + BuiltinOr(), BuiltinPrint(), BuiltinQueryString(), BuiltinRatingToStars(), + BuiltinRange(), BuiltinRawField(), BuiltinRawList(), BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(), BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(), BuiltinStrcmp(), BuiltinStrcmpcase(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),