mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
084e9cd755
@ -245,7 +245,8 @@ General Program Mode
|
||||
not_expression ::= [ '!' not_expression ]* | concatenate_expr
|
||||
concatenate_expr::= compare_expr [ '&' compare_expr ]*
|
||||
compare_expr ::= add_sub_expr [ compare_op add_sub_expr ]
|
||||
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | 'inlist' |
|
||||
compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' |
|
||||
'in' | 'inlist' | 'field_inlist' |
|
||||
'==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
|
||||
add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]*
|
||||
add_sub_op ::= '+' | '-'
|
||||
@ -406,8 +407,10 @@ Relational operators return ``'1'`` if the comparison is true, otherwise the emp
|
||||
|
||||
There are two forms of relational operators: string comparisons and numeric comparisons.
|
||||
|
||||
String comparisons do case-insensitive string comparison using lexical order. The supported string comparison operators are ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``in``, and ``inlist``.
|
||||
For the ``in`` operator, the result of the left hand expression is interpreted as a regular expression pattern. The ``in`` operator is True if the value of left-hand regular expression matches the value of the right hand expression. The ``inlist`` operator is true if the left hand regular expression matches any one of the items in the right hand list where the items in the list are separated by commas. The matches are case-insensitive.
|
||||
String comparisons do case-insensitive string comparison using lexical order. The supported string comparison operators are ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``in``, ``inlist``, and ``field_inlist``.
|
||||
For the ``in`` operator, the result of the left hand expression is interpreted as a regular expression pattern. The ``in`` operator is True if the value of left-hand regular expression matches the value of the right hand expression.
|
||||
|
||||
The ``inlist`` operator is true if the left hand regular expression matches any one of the items in the right hand list where the items in the list are separated by commas. The ``field_inlist`` operator is true if the left hand regular expression matches any of the items in the field (column) named by the right hand expression, using the separator defined for the field. NB: the ``field_inlist`` operator requires the right hand expression to evaluate to a field name, while the ``inlist`` operator requires the right hand expression to evaluate to a string containing a comma-separated list. Because of this difference, ``field_inlist`` is substantially faster than ``inlist`` because no string conversions or list constructions are done. The regular expressions are case-insensitive.
|
||||
|
||||
The numeric comparison operators are ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. The left and right expressions must evaluate to numeric values with two exceptions: both the string value "None" (undefined field) and the empty string evaluate to the value zero.
|
||||
|
||||
@ -415,8 +418,10 @@ Examples:
|
||||
|
||||
* ``program: field('series') == 'foo'`` returns ``'1'`` if the book's series is 'foo', otherwise ``''``.
|
||||
* ``program: 'f.o' in field('series')`` returns ``'1'`` if the book's series matches the regular expression ``f.o`` (e.g., `foo`, `Off Onyx`, etc.), otherwise ``''``.
|
||||
* ``program: 'science' inlist field('#genre')`` returns ``'1'`` if any of the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc., otherwise ``''``.
|
||||
* ``program: '^science$' inlist field('#genre')`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`. The genres `History of Science` and `Science Fiction` don't match. If there isn't a match then returns ``''``.
|
||||
* ``program: 'science' inlist field('#genre')`` returns ``'1'`` if any of the values retrieved from the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc., otherwise ``''``.
|
||||
* ``program: '^science$' inlist $#genre`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`. The genres `History of Science` and `Science Fiction` don't match. If there isn't a match then it returns ``''``.
|
||||
* ``program: 'asimov' field_inlist 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov``, e.g., `Asimov, Isaac` or `Isaac Asimov`, otherwise ``''``.
|
||||
* ``program: 'asimov$' field_inlist 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov$``, e.g., `Isaac Asimov`, otherwise ``''``. It doesn't match `Asimov, Isaac` because of the ``$`` anchor in the regular expression.
|
||||
* ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not ``foo``. Otherwise it returns ``'mumble'``.
|
||||
* ``program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi`` returns ``'yes'`` if series is either ``'foo'`` or ``'1632'``, otherwise ``'no'``.
|
||||
* ``program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi`` returns ``'yes'`` if series is either ``'foo'`` or ``'1632'``, otherwise ``'no'``.
|
||||
@ -553,10 +558,10 @@ In `GPM` the functions described in `Single Function Mode` all require an additi
|
||||
|
||||
format_date(raw_field('pubdate'), 'yyyy')
|
||||
|
||||
* ``format_date_field(field_name, format_string)`` -- format the value in the field ``field_name``, which must be the lookup name of date field, either standard or custom. See ``format_date()`` for the formatting codes. This function is much faster than format_date and should be used when you are formatting the value in a field (column). It can't be used for computed dates or dates in string variables. Examples::
|
||||
* ``field_format_date(field_name, format_string)`` -- format the value in the field ``field_name``, which must be the lookup name of date field, either standard or custom. See ``format_date()`` for the formatting codes. This function is much faster than format_date and should be used when you are formatting the value in a field (column). It can't be used for computed dates or dates in string variables. Alias: format_date_field. Examples::
|
||||
|
||||
format_date_field('pubdate', 'yyyy.MM.dd')
|
||||
format_date_field('#date_read', 'MMM dd, yyyy')
|
||||
field_format_date('pubdate', 'yyyy.MM.dd')
|
||||
field_format_date('#date_read', 'MMM dd, yyyy')
|
||||
|
||||
* ``formats_modtimes(date_format_string)`` -- return a comma-separated list of colon-separated items ``FMT:DATE`` representing modification times for the formats of a book. The ``date_format_string`` parameter specifies how the date is to be formatted. See the ``format_date()`` function for details. You can use the ``select`` function to get the modification time for a specific format. Note that format names are always uppercase, as in EPUB.
|
||||
* ``formats_paths()`` -- return a comma-separated list of colon-separated items ``FMT:PATH`` giving the full path to the formats of a book. You can use the select function to get the path for a specific format. Note that format names are always uppercase, as in EPUB.
|
||||
|
@ -57,10 +57,33 @@ class TableItem(QTableWidgetItem):
|
||||
return self.sort_key < other.sort_key
|
||||
|
||||
|
||||
class CountTableItem(QTableWidgetItem):
|
||||
|
||||
def __init__(self, val):
|
||||
QTableWidgetItem.__init__(self, str(val))
|
||||
self.val = val
|
||||
self.setTextAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
||||
|
||||
def setText(self, val):
|
||||
self.val = val
|
||||
QTableWidgetItem.setText(self, str(val))
|
||||
|
||||
def set_sort_key(self):
|
||||
pass
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.val >= other.val
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.val < other.val
|
||||
|
||||
|
||||
AUTHOR_COLUMN = 0
|
||||
AUTHOR_SORT_COLUMN = 1
|
||||
LINK_COLUMN = 2
|
||||
NOTES_COLUMN = 3
|
||||
COUNTS_COLUMN = 2
|
||||
LINK_COLUMN = 3
|
||||
NOTES_COLUMN = 4
|
||||
|
||||
|
||||
class EditColumnDelegate(QStyledItemDelegate):
|
||||
@ -83,7 +106,8 @@ class EditColumnDelegate(QStyledItemDelegate):
|
||||
if index.column() == NOTES_COLUMN:
|
||||
self.notes_utilities.edit_note(self.table.itemFromIndex(index))
|
||||
return None
|
||||
|
||||
if index.column() == COUNTS_COLUMN:
|
||||
return None
|
||||
from calibre.gui2.widgets import EnLineEdit
|
||||
editor = EnLineEdit(parent)
|
||||
editor.setClearButtonEnabled(True)
|
||||
@ -193,13 +217,14 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
self.original_authors = {}
|
||||
auts = db.new_api.author_data()
|
||||
self.completion_data = []
|
||||
counts = db.new_api.get_usage_count_by_id('authors')
|
||||
for id_, v in auts.items():
|
||||
name = v['name']
|
||||
name = name.replace('|', ',')
|
||||
self.completion_data.append(name)
|
||||
self.authors[id_] = {'name': name, 'sort': v['sort'], 'link': v['link']}
|
||||
self.original_authors[id_] = {'name': name, 'sort': v['sort'],
|
||||
'link': v['link']}
|
||||
vals = {'name': name, 'sort': v['sort'], 'link': v['link'], 'count':counts[id_]}
|
||||
self.authors[id_] = vals
|
||||
self.original_authors[id_] = vals.copy()
|
||||
|
||||
if prefs['use_primary_find_in_search']:
|
||||
self.string_contains = primary_contains
|
||||
@ -260,7 +285,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
|
||||
self.table.blockSignals(True)
|
||||
self.table.clear()
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setColumnCount(5)
|
||||
|
||||
self.table.setRowCount(len(auts_to_show))
|
||||
row = 0
|
||||
@ -269,19 +294,21 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
for id_, v in self.authors.items():
|
||||
if id_ not in auts_to_show:
|
||||
continue
|
||||
name, sort, link = (v['name'], v['sort'], v['link'])
|
||||
name, sort, link, count = (v['name'], v['sort'], v['link'], v['count'])
|
||||
name = name.replace('|', ',')
|
||||
|
||||
name_item = TableItem(name)
|
||||
name_item.setData(Qt.ItemDataRole.UserRole, id_)
|
||||
sort_item = TableItem(sort)
|
||||
link_item = TableItem(link)
|
||||
count_item = CountTableItem(count)
|
||||
|
||||
self.table.setItem(row, AUTHOR_COLUMN, name_item)
|
||||
self.table.setItem(row, AUTHOR_SORT_COLUMN, sort_item)
|
||||
self.table.setItem(row, LINK_COLUMN, link_item)
|
||||
note_item = NotesTableWidgetItem()
|
||||
self.table.setItem(row, NOTES_COLUMN, note_item)
|
||||
self.table.setItem(row, COUNTS_COLUMN, count_item)
|
||||
|
||||
self.set_icon(name_item, id_)
|
||||
self.set_icon(sort_item, id_)
|
||||
@ -289,7 +316,17 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
self.notes_utilities.set_icon(note_item, id_, id_ in all_items_that_have_notes)
|
||||
row += 1
|
||||
|
||||
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort'), _('Link'), _('Notes')])
|
||||
headers = { # this depends on the dict being ordered, which is true from python 3.7
|
||||
_('Author'): _('Name of the author'),
|
||||
_('Author sort'): _('Value used to sort this author'),
|
||||
_('Count'): _('Count of books with this author'),
|
||||
_('Link'): _('Link (URL) for this author'),
|
||||
_('Notes'): _('Whether this author has a note attached. The icon changes if the note was created or edited'),
|
||||
}
|
||||
self.table.setHorizontalHeaderLabels(headers.keys())
|
||||
for i,tt in enumerate(headers.values()):
|
||||
header_item = self.table.horizontalHeaderItem(i)
|
||||
header_item.setToolTip(tt)
|
||||
|
||||
if self.last_sorted_by == 'sort':
|
||||
self.author_sort_order = 1 - self.author_sort_order
|
||||
@ -371,7 +408,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
self.save_state()
|
||||
|
||||
def get_column_name(self, column):
|
||||
return ('name', 'sort', 'link', 'notes')[column]
|
||||
return ('name', 'sort', 'count', 'link', 'notes')[column]
|
||||
|
||||
def item_is_modified(self, item, id_):
|
||||
sub = self.get_column_name(item.column())
|
||||
@ -381,7 +418,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||
|
||||
def show_context_menu(self, point):
|
||||
self.context_item = self.table.itemAt(point)
|
||||
if self.context_item is None:
|
||||
if self.context_item is None or self.context_item.column() == COUNTS_COLUMN:
|
||||
return
|
||||
case_menu = QMenu(_('Change case'))
|
||||
case_menu.setIcon(QIcon.cached_icon('font_size_larger.png'))
|
||||
|
@ -730,6 +730,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
tags = self.ordered_tags
|
||||
|
||||
select_item = None
|
||||
tooltips = ( # must be in the same order as the columns in the table
|
||||
_('Name of the item'),
|
||||
_('Count of books with this item'),
|
||||
_('Value of the item before it was edited'),
|
||||
_('The link (URL) associated with this item'),
|
||||
_('Whether the item has a note. The icon changes if the note was created or edited')
|
||||
)
|
||||
with block_signals(self.table):
|
||||
self.name_col = QTableWidgetItem(self.category_name)
|
||||
self.table.setHorizontalHeaderItem(VALUE_COLUMN, self.name_col)
|
||||
@ -742,6 +749,9 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
if self.supports_notes:
|
||||
self.notes_col = QTableWidgetItem(_('Notes'))
|
||||
self.table.setHorizontalHeaderItem(4, self.notes_col)
|
||||
for i,tt in enumerate(tooltips):
|
||||
header_item = self.table.horizontalHeaderItem(i)
|
||||
header_item.setToolTip(tt)
|
||||
|
||||
self.table.setRowCount(len(tags))
|
||||
if self.supports_notes:
|
||||
|
@ -75,7 +75,7 @@ class TemplateHighlighter(QSyntaxHighlighter):
|
||||
|
||||
KEYWORDS_GPM = ['if', 'then', 'else', 'elif', 'fi', 'for', 'rof',
|
||||
'separator', 'break', 'continue', 'return', 'in', 'inlist',
|
||||
'def', 'fed', 'limit']
|
||||
'field_inlist', 'def', 'fed', 'limit']
|
||||
|
||||
KEYWORDS_PYTHON = ["and", "as", "assert", "break", "class", "continue", "def",
|
||||
"del", "elif", "else", "except", "exec", "finally", "for", "from",
|
||||
|
@ -652,7 +652,7 @@ class _Parser:
|
||||
def compare_expr(self):
|
||||
left = self.add_subtract_expr()
|
||||
if (self.token_op_is_string_infix_compare() or
|
||||
self.token_is('in') or self.token_is('inlist')):
|
||||
self.token_is('in') or self.token_is('inlist') or self.token_is('field_inlist')):
|
||||
operator = self.token()
|
||||
return StringCompareNode(self.line_number, operator, left, self.add_subtract_expr())
|
||||
if self.token_op_is_numeric_infix_compare():
|
||||
@ -1398,11 +1398,27 @@ class _Interpreter:
|
||||
[v.strip() for v in y.split(',') if v.strip()]))
|
||||
}
|
||||
|
||||
def do_field_inlist(self, left, right, prog):
|
||||
res = getattr(self.parent_book, right, None)
|
||||
if res is None or not isinstance(res, (list, tuple, set, dict)):
|
||||
self.error(_("Field '{0}' is either not a field or not a list").format(right), prog.line_number)
|
||||
pat = re.compile(left, flags=re.I)
|
||||
for v in res:
|
||||
if re.search(pat, v):
|
||||
return '1'
|
||||
return ''
|
||||
|
||||
def do_node_string_infix(self, prog):
|
||||
try:
|
||||
left = self.expr(prog.left)
|
||||
right = self.expr(prog.right)
|
||||
res = '1' if self.INFIX_STRING_COMPARE_OPS[prog.operator](left, right) else ''
|
||||
try:
|
||||
res = '1' if self.INFIX_STRING_COMPARE_OPS[prog.operator](left, right) else ''
|
||||
except KeyError:
|
||||
if prog.operator == 'field_inlist':
|
||||
res = self.do_field_inlist(left, right, prog)
|
||||
else:
|
||||
raise
|
||||
if (self.break_reporter):
|
||||
self.break_reporter(prog.node_name, res, prog.line_number)
|
||||
return res
|
||||
@ -1684,6 +1700,7 @@ class TemplateFormatter(string.Formatter):
|
||||
(r'(separator|limit)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(def|fed|continue)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(return|inlist|break)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(field_inlist)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(\|\||&&|!|{|})', lambda x,t: (_Parser.LEX_OP, t)), # noqa
|
||||
(r'[(),=;:\+\-*/&]', lambda x,t: (_Parser.LEX_OP, t)), # noqa
|
||||
(r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa
|
||||
|
@ -1290,21 +1290,23 @@ class BuiltinFormatDate(BuiltinFormatterFunction):
|
||||
|
||||
|
||||
class BuiltinFormatDateField(BuiltinFormatterFunction):
|
||||
name = 'format_date_field'
|
||||
name = 'field_format_date'
|
||||
arg_count = 2
|
||||
category = 'Formatting values'
|
||||
__doc__ = doc = _("format_date_field(field_name, format_string) -- format "
|
||||
__doc__ = doc = _("field_format_date(field_name, format_string) -- format "
|
||||
"the value in the field 'field_name', which must be the lookup name "
|
||||
"of date field, either standard or custom. See 'format_date' for "
|
||||
"the formatting codes. This function is much faster than format_date "
|
||||
"and should be used when you are formatting the value in a field "
|
||||
"(column). It can't be used for computed dates or dates in string "
|
||||
"variables. Example: format_date_field('pubdate', 'yyyy.MM.dd')")
|
||||
"variables. Example: format_date_field('pubdate', 'yyyy.MM.dd'). "
|
||||
"Alias: format_date_field")
|
||||
aliases = ['format_date_field']
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, field, format_string):
|
||||
try:
|
||||
if field not in mi.all_field_keys():
|
||||
return _('Unknown field %s passed to function %s')%(field, 'format_date_field')
|
||||
return _('Unknown field %s passed to function %s')%(field, 'field_format_date')
|
||||
val = mi.get(field, None)
|
||||
if val is None:
|
||||
s = ''
|
||||
|
Loading…
x
Reference in New Issue
Block a user