mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from main branch
This commit is contained in:
commit
c3e3fc7656
@ -4,6 +4,7 @@ src/calibre/plugins
|
||||
resources/images.qrc
|
||||
src/calibre/manual/.build/
|
||||
src/calibre/manual/cli/
|
||||
src/calibre/manual/template_ref.rst
|
||||
build
|
||||
dist
|
||||
docs
|
||||
@ -31,4 +32,4 @@ nbproject/
|
||||
.pydevproject
|
||||
.settings/
|
||||
*.DS_Store
|
||||
calibre_plugins/
|
||||
calibre_plugins/
|
||||
|
@ -1,27 +1,30 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2010 - 2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
news.bbc.co.uk
|
||||
'''
|
||||
import re
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class BBC(BasicNewsRecipe):
|
||||
title = 'BBC News (fast)'
|
||||
__author__ = 'Darko Miletic, Starson17'
|
||||
description = 'News from UK. A much faster version that does not download pictures'
|
||||
description = 'Visit BBC News for up-to-the-minute news, breaking news, video, audio and feature stories. BBC News provides trusted World and UK news as well as local and regional perspectives. Also entertainment, business, science, technology and health news.'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
#delay = 1
|
||||
use_embedded_content = False
|
||||
encoding = 'utf8'
|
||||
publisher = 'BBC'
|
||||
category = 'news, UK, world'
|
||||
language = 'en_GB'
|
||||
publication_type = 'newsportal'
|
||||
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
masthead_url = 'http://news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif'
|
||||
extra_css = """
|
||||
body{ font-family: Verdana,Helvetica,Arial,sans-serif }
|
||||
.introduction{font-weight: bold}
|
||||
.story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small}
|
||||
.story-feature h2{text-align: center; text-transform: uppercase}
|
||||
"""
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
@ -31,31 +34,54 @@ class BBC(BasicNewsRecipe):
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['layout-block-a layout-block']})
|
||||
,dict(attrs={'class':['story-body','storybody']})
|
||||
dict(name='div', attrs={'class':['layout-block-a layout-block']})
|
||||
,dict(attrs={'class':['story-body','storybody']})
|
||||
,dict(attrs={'id':['meta-information','story-body']})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
|
||||
'story-feature wide ', 'story-feature narrow']})
|
||||
, dict(name=['img'])
|
||||
]
|
||||
dict(name='div', attrs={'class':['story-feature related narrow', \
|
||||
'share-help', 'embedded-hyper', \
|
||||
'story-feature wide ', \
|
||||
'story-feature narrow', \
|
||||
'hidden','story-actions', \
|
||||
'embedded-hyper']})
|
||||
,dict(name=['img','meta','link','object','embed','iframe','base'])
|
||||
,dict(attrs={'class':['hidden','videoInStoryC']})
|
||||
,dict(attrs={'id':['bbccom_sponsor_section','toggle-controls', \
|
||||
'toggle-images','toggle-title']})
|
||||
]
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
remove_attributes = ['width','height','xmlns:og','lang','clear']
|
||||
|
||||
feeds = [
|
||||
('News Front Page', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml'),
|
||||
('Science/Nature', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml'),
|
||||
('Technology', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/technology/rss.xml'),
|
||||
('Entertainment', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/entertainment/rss.xml'),
|
||||
('Magazine', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/uk_news/magazine/rss.xml'),
|
||||
('Business', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/business/rss.xml'),
|
||||
('Health', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/health/rss.xml'),
|
||||
('Americas', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/americas/rss.xml'),
|
||||
('Europe', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/europe/rss.xml'),
|
||||
('South Asia', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/south_asia/rss.xml'),
|
||||
('UK', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/uk_news/rss.xml'),
|
||||
('Asia-Pacific', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/asia-pacific/rss.xml'),
|
||||
('Africa', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/africa/rss.xml'),
|
||||
('Top Stories' , 'http://feeds.bbci.co.uk/news/rss.xml' ),
|
||||
('Science/Environment', 'http://feeds.bbci.co.uk/news/science_and_environment/rss.xml'),
|
||||
('Technology' , 'http://feeds.bbci.co.uk/news/technology/rss.xml' ),
|
||||
('Entertainment/Arts' , 'http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml' ),
|
||||
('Magazine' , 'http://feeds.bbci.co.uk/news/magazine/rss.xml' ),
|
||||
('Business' , 'http://feeds.bbci.co.uk/news/business/rss.xml' ),
|
||||
('Politics' , 'http://feeds.bbci.co.uk/news/politics/rss.xml' ),
|
||||
('Health' , 'http://feeds.bbci.co.uk/news/health/rss.xml' ),
|
||||
('US&Canada' , 'http://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml' ),
|
||||
('Latin America' , 'http://feeds.bbci.co.uk/news/world/latin_america/rss.xml' ),
|
||||
('Europe' , 'http://feeds.bbci.co.uk/news/world/europe/rss.xml' ),
|
||||
('South Asia' , 'http://feeds.bbci.co.uk/news/world/south_asia/rss.xml' ),
|
||||
('England' , 'http://feeds.bbci.co.uk/news/england/rss.xml' ),
|
||||
('Asia-Pacific' , 'http://feeds.bbci.co.uk/news/world/asia_pacific/rss.xml' ),
|
||||
('Africa' , 'http://feeds.bbci.co.uk/news/world/africa/rss.xml' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('left'):
|
||||
item.name='span'
|
||||
for item in soup.findAll('a'):
|
||||
if item.string is not None:
|
||||
str = item.string
|
||||
item.replaceWith(str)
|
||||
else:
|
||||
str = self.tag_to_string(item)
|
||||
item.replaceWith(str)
|
||||
return soup
|
||||
|
@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase):
|
||||
formats = ['EPUB', 'PDF']
|
||||
affiliate = True
|
||||
|
||||
class StoreEpubBudStore(StoreBase):
|
||||
name = 'ePub Bud'
|
||||
description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
|
||||
actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB']
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.'
|
||||
@ -1422,6 +1431,7 @@ plugins += [
|
||||
StoreEBookShoppeUKStore,
|
||||
StoreEPubBuyDEStore,
|
||||
StoreEHarlequinStore,
|
||||
StoreEpubBudStore,
|
||||
StoreFeedbooksStore,
|
||||
StoreFoylesUKStore,
|
||||
StoreGandalfStore,
|
||||
|
@ -44,11 +44,16 @@ class SafeFormat(TemplateFormatter):
|
||||
def get_value(self, orig_key, args, kwargs):
|
||||
if not orig_key:
|
||||
return ''
|
||||
key = orig_key.lower()
|
||||
orig_key = orig_key.lower()
|
||||
key = orig_key
|
||||
if key != 'title_sort' and key not in TOP_LEVEL_IDENTIFIERS:
|
||||
key = field_metadata.search_term_to_field_key(key)
|
||||
if key is None or (self.book and key not in self.book.all_field_keys()):
|
||||
raise ValueError(_('Value: unknown field ') + orig_key)
|
||||
if key is None or (self.book and
|
||||
key not in self.book.all_field_keys()):
|
||||
if hasattr(self.book, orig_key):
|
||||
key = orig_key
|
||||
else:
|
||||
raise ValueError(_('Value: unknown field ') + orig_key)
|
||||
b = self.book.get_user_metadata(key, False)
|
||||
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
||||
v = ''
|
||||
|
@ -408,6 +408,8 @@ def identify(log, abort, # {{{
|
||||
for f in plugin.prefs['ignore_fields']:
|
||||
if ':' not in f:
|
||||
setattr(result, f, getattr(dummy, f))
|
||||
if f == 'series':
|
||||
result.series_index = dummy.series_index
|
||||
result.relevance_in_source = i
|
||||
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
|
||||
and plugin.get_cached_cover_url(result.identifiers) is not
|
||||
|
@ -482,6 +482,8 @@ class EditMetadataAction(InterfaceAction):
|
||||
if mi.identifiers:
|
||||
idents.update(mi.identifiers)
|
||||
mi.identifiers = idents
|
||||
if mi.is_null('series'):
|
||||
mi.series_index = None
|
||||
db.set_metadata(i, mi, commit=False, set_title=set_title,
|
||||
set_authors=set_authors, notify=False)
|
||||
self.applied_ids.append(i)
|
||||
|
@ -95,25 +95,27 @@ class TemplateLineEditor(QLineEdit):
|
||||
|
||||
class TagWizard(QDialog):
|
||||
|
||||
text_template = " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')"
|
||||
text_empty_template = " test(field('{f}'), '{fv}', '{tv}')"
|
||||
text_re_template = " contains(field('{f}'), '{v}', '{tv}', '{fv}')"
|
||||
text_template = (" strcmp(field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True)
|
||||
text_empty_template = (" test(field('{f}'), '{fv}', '{tv}')", False)
|
||||
text_re_template = (" contains(field('{f}'), '{v}', '{tv}', '{fv}')", False)
|
||||
|
||||
templates = {
|
||||
'text.mult' : " str_in_list(field('{f}'), '{mult}', '{v}', '{tv}', '{fv}')",
|
||||
'text.mult.re' : " in_list(field('{f}'), '{mult}', '^{v}$', '{tv}', '{fv}')",
|
||||
'text.mult.empty' : " test(field('{f}'), '{fv}', '{tv}')",
|
||||
'text.mult' : (" str_in_list(field('{f}'), '{mult}', '{v}', '{tv}', '{fv}')", False),
|
||||
'text.mult.re' : (" in_list(field('{f}'), '{mult}', '^{v}$', '{tv}', '{fv}')", False),
|
||||
'text.mult.empty' : (" test(field('{f}'), '{fv}', '{tv}')", False),
|
||||
'text' : text_template,
|
||||
'text.re' : text_re_template,
|
||||
'text.empty' : text_empty_template,
|
||||
'rating' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')",
|
||||
'rating' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True),
|
||||
'rating.empty' : text_empty_template,
|
||||
'int' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')",
|
||||
'int' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True),
|
||||
'int.empty' : text_empty_template,
|
||||
'float' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')",
|
||||
'float' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True),
|
||||
'float.empty' : text_empty_template,
|
||||
'bool' : " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')",
|
||||
'bool' : (" strcmp(field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True),
|
||||
'bool.empty' : text_empty_template,
|
||||
'datetime' : (" strcmp(format_date(raw_field('{f}'), 'yyyyMMdd'), format_date('{v}', 'yyyyMMdd'), '{ltv}', '{eqv}', '{gtv}')", True),
|
||||
'datetime.empty' : text_empty_template,
|
||||
'series' : text_template,
|
||||
'series.re' : text_re_template,
|
||||
'series.empty' : text_empty_template,
|
||||
@ -128,6 +130,22 @@ class TagWizard(QDialog):
|
||||
'comments.empty' : text_empty_template,
|
||||
}
|
||||
|
||||
relationals = ('=', '!=', '<', '>', '<=', '>=')
|
||||
relational_truth_vals = {
|
||||
'=': ('', '1', ''),
|
||||
'!=': ('1', '', '1'),
|
||||
'<': ('1', '', ''),
|
||||
'>': ('', '', '1'),
|
||||
'<=': ('1', '1', ''),
|
||||
'>=': ('', '1', '1'),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def uses_this_wizard(txt):
|
||||
if not txt or txt.startswith('program:\n#tag wizard'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __init__(self, parent, db, txt, mi):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setWindowTitle(_('Coloring Wizard'))
|
||||
@ -141,8 +159,7 @@ class TagWizard(QDialog):
|
||||
m = db.metadata_for_field(k)
|
||||
if k.endswith('_index') or (
|
||||
m['kind'] == 'field' and m['name'] and
|
||||
k not in ('ondevice', 'path', 'size', 'sort') and
|
||||
m['datatype'] not in ('datetime')):
|
||||
k not in ('ondevice', 'path', 'size', 'sort')):
|
||||
self.columns.append(k)
|
||||
self.completion_values[k]['dt'] = m['datatype']
|
||||
if m['is_custom']:
|
||||
@ -203,11 +220,12 @@ class TagWizard(QDialog):
|
||||
h.setAlignment(Qt.AlignCenter)
|
||||
l.addWidget(h, 0, 2, 1, 1)
|
||||
|
||||
h = QLabel(_('not'))
|
||||
h = QLabel(_('op'))
|
||||
h.setToolTip('<p>' +
|
||||
_('Check this box to indicate that the value must <b>not</b> match '
|
||||
'to use the color. For example, you can check if a tag does '
|
||||
'not exist by entering that tag and checking this box.') + '</p>')
|
||||
_('Use this box to tell what comparison operation to use. Some '
|
||||
'comparisons cannot be used with certain options. For example, '
|
||||
'if regular expressions are used, only equals and not equals '
|
||||
'are valid.') + '</p>')
|
||||
h.setAlignment(Qt.AlignCenter)
|
||||
l.addWidget(h, 0, 3, 1, 1)
|
||||
|
||||
@ -246,7 +264,7 @@ class TagWizard(QDialog):
|
||||
l.addWidget(c, 0, 7, 1, 1)
|
||||
|
||||
self.andboxes = []
|
||||
self.notboxes = []
|
||||
self.opboxes = []
|
||||
self.tagboxes = []
|
||||
self.colorboxes = []
|
||||
self.reboxes = []
|
||||
@ -284,13 +302,17 @@ class TagWizard(QDialog):
|
||||
w.setText(_('is'))
|
||||
l.addWidget(w, i, 2, 1, 1)
|
||||
|
||||
create_widget(QCheckBox, self.notboxes, l, i, 3, None)
|
||||
w = create_widget(QComboBox, self.opboxes, l, i, 3, None)
|
||||
w.setMaximumWidth(40)
|
||||
|
||||
w = create_widget(QCheckBox, self.emptyboxes, l, i, 4, None)
|
||||
w.stateChanged.connect(partial(self.empty_box_changed, line=i-1))
|
||||
|
||||
create_widget(MultiCompleteLineEdit, self.tagboxes, l, i, 5, None, align=0)
|
||||
create_widget(QCheckBox, self.reboxes, l, i, 6, None)
|
||||
|
||||
w = create_widget(QCheckBox, self.reboxes, l, i, 6, None)
|
||||
w.stateChanged.connect(partial(self.re_box_changed, line=i-1))
|
||||
|
||||
create_widget(QComboBox, self.colorboxes, l, i, 7, self.colors)
|
||||
|
||||
w = create_widget(QLabel, None, l, maxlines+1, 5, None)
|
||||
@ -315,20 +337,23 @@ class TagWizard(QDialog):
|
||||
if len(vals) == 2:
|
||||
t, c = vals
|
||||
f = 'tags'
|
||||
a = n = e = re = False
|
||||
a = re = e = 0
|
||||
op = '='
|
||||
else:
|
||||
t,c,f,re,a,n,e = vals
|
||||
t,c,f,re,a,op,e = vals
|
||||
try:
|
||||
self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f))
|
||||
self.colorboxes[i].setCurrentIndex(
|
||||
self.colorboxes[i].findText(c))
|
||||
self.tagboxes[i].setText(t)
|
||||
self.reboxes[i].setChecked(re == '2')
|
||||
self.andboxes[i].setChecked(a == '2')
|
||||
self.notboxes[i].setChecked(n == '2')
|
||||
self.emptyboxes[i].setChecked(e == '2')
|
||||
self.andboxes[i].setChecked(a == '2')
|
||||
self.opboxes[i].setCurrentIndex(self.opboxes[i].findText(op))
|
||||
i += 1
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
w = QLabel(_('Preview'))
|
||||
@ -357,26 +382,6 @@ class TagWizard(QDialog):
|
||||
_('EXCEPTION'), self.mi)
|
||||
self.test_box.setText(t)
|
||||
|
||||
def column_changed(self, s, line=None):
|
||||
k = unicode(s)
|
||||
if k in self.completion_values:
|
||||
valbox = self.tagboxes[line]
|
||||
valbox.update_items_cache(self.completion_values[k]['v'])
|
||||
if self.completion_values[k]['m']:
|
||||
valbox.set_separator(', ')
|
||||
else:
|
||||
valbox.set_separator(None)
|
||||
|
||||
dt = self.completion_values[k]['dt']
|
||||
if dt in ('int', 'float', 'rating', 'bool'):
|
||||
self.reboxes[line].setChecked(0)
|
||||
self.reboxes[line].setEnabled(False)
|
||||
else:
|
||||
self.reboxes[line].setEnabled(True)
|
||||
else:
|
||||
valbox.update_items_cache([])
|
||||
valbox.set_separator(None)
|
||||
|
||||
def generate_program(self):
|
||||
res = ("program:\n#tag wizard -- do not directly edit\n"
|
||||
" first_non_empty(\n")
|
||||
@ -384,9 +389,9 @@ class TagWizard(QDialog):
|
||||
was_and = had_line = False
|
||||
|
||||
line = 0
|
||||
for tb, cb, fb, reb, ab, nb, eb in zip(
|
||||
for tb, cb, fb, reb, ab, ob, eb in zip(
|
||||
self.tagboxes, self.colorboxes, self.colboxes,
|
||||
self.reboxes, self.andboxes, self.notboxes, self.emptyboxes):
|
||||
self.reboxes, self.andboxes, self.opboxes, self.emptyboxes):
|
||||
f = unicode(fb.currentText())
|
||||
if not f:
|
||||
continue
|
||||
@ -394,14 +399,11 @@ class TagWizard(QDialog):
|
||||
dt = self.completion_values[f]['dt']
|
||||
c = unicode(cb.currentText()).strip()
|
||||
re = reb.checkState()
|
||||
a = ab.checkState()
|
||||
n = nb.checkState()
|
||||
e = eb.checkState()
|
||||
a = ab.checkState()
|
||||
op = unicode(ob.currentText())
|
||||
e = eb.checkState()
|
||||
line += 1
|
||||
|
||||
tval = '' if n == 2 else '1'
|
||||
fval = '1' if n == 2 else ''
|
||||
|
||||
if m:
|
||||
tags = [t.strip() for t in unicode(tb.text()).split(m) if t.strip()]
|
||||
if re == 2:
|
||||
@ -428,8 +430,15 @@ class TagWizard(QDialog):
|
||||
lines[-1] += ','
|
||||
|
||||
key = dt + ('.mult' if m else '') + ('.empty' if e else '') + ('.re' if re else '')
|
||||
template = self.templates[key]
|
||||
lines.append(template.format(v=tags, f=f, tv=tval, fv=fval, mult=m))
|
||||
tval = '1' if op == '=' else ''
|
||||
fval = '' if op == '=' else '1'
|
||||
template, is_relational = self.templates[key]
|
||||
if is_relational:
|
||||
ltv, eqv, gtv = self.relational_truth_vals[op]
|
||||
else:
|
||||
ltv, eqv, gtv = (None, None, None)
|
||||
lines.append(template.format(v=tags, f=f, tv=tval, fv=fval, mult=m,
|
||||
ltv=ltv, eqv=eqv, gtv=gtv))
|
||||
|
||||
if a == 2:
|
||||
was_and = True
|
||||
@ -444,9 +453,9 @@ class TagWizard(QDialog):
|
||||
res += ')\n'
|
||||
self.template = res
|
||||
res = ''
|
||||
for tb, cb, fb, reb, ab, nb, eb in zip(
|
||||
for tb, cb, fb, reb, ab, ob, eb in zip(
|
||||
self.tagboxes, self.colorboxes, self.colboxes,
|
||||
self.reboxes, self.andboxes, self.notboxes, self.emptyboxes):
|
||||
self.reboxes, self.andboxes, self.opboxes, self.emptyboxes):
|
||||
t = unicode(tb.text()).strip()
|
||||
if t.endswith(','):
|
||||
t = t[:-1]
|
||||
@ -454,15 +463,57 @@ class TagWizard(QDialog):
|
||||
f = unicode(fb.currentText())
|
||||
re = unicode(reb.checkState())
|
||||
a = unicode(ab.checkState())
|
||||
n = unicode(nb.checkState())
|
||||
op = unicode(ob.currentText())
|
||||
e = unicode(eb.checkState())
|
||||
if f and (t or e) and (a == '2' or c):
|
||||
res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + re + ':|:' + \
|
||||
a + ':|:' + n + ':|:' + e + '\n'
|
||||
a + ':|:' + op + ':|:' + e + '\n'
|
||||
res += '#else:' + else_txt + '\n'
|
||||
self.template += res
|
||||
return True
|
||||
|
||||
def column_changed(self, s, line=None):
|
||||
k = unicode(s)
|
||||
valbox = self.tagboxes[line]
|
||||
if k in self.completion_values:
|
||||
valbox.update_items_cache(self.completion_values[k]['v'])
|
||||
if self.completion_values[k]['m']:
|
||||
valbox.set_separator(', ')
|
||||
else:
|
||||
valbox.set_separator(None)
|
||||
|
||||
dt = self.completion_values[k]['dt']
|
||||
if dt in ('int', 'float', 'rating', 'bool'):
|
||||
self.reboxes[line].setChecked(0)
|
||||
self.reboxes[line].setEnabled(False)
|
||||
else:
|
||||
self.reboxes[line].setEnabled(True)
|
||||
self.fill_in_opbox(line)
|
||||
else:
|
||||
valbox.update_items_cache([])
|
||||
valbox.set_separator(None)
|
||||
|
||||
def fill_in_opbox(self, line):
|
||||
opbox = self.opboxes[line]
|
||||
opbox.clear()
|
||||
k = unicode(self.colboxes[line].currentText())
|
||||
if not k:
|
||||
return
|
||||
if k in self.completion_values:
|
||||
rebox = self.reboxes[line]
|
||||
ebox = self.emptyboxes[line]
|
||||
idx = opbox.currentIndex()
|
||||
if self.completion_values[k]['m'] or \
|
||||
rebox.checkState() == 2 or ebox.checkState() == 2:
|
||||
opbox.addItems(self.relationals[0:2])
|
||||
idx = idx if idx < 2 else 0
|
||||
else:
|
||||
opbox.addItems(self.relationals)
|
||||
opbox.setCurrentIndex(max(idx, 0))
|
||||
|
||||
def re_box_changed(self, state, line=None):
|
||||
self.fill_in_opbox(line)
|
||||
|
||||
def empty_box_changed(self, state, line=None):
|
||||
if state == 2:
|
||||
self.tagboxes[line].setText('')
|
||||
@ -472,6 +523,7 @@ class TagWizard(QDialog):
|
||||
else:
|
||||
self.reboxes[line].setEnabled(True)
|
||||
self.tagboxes[line].setEnabled(True)
|
||||
self.fill_in_opbox(line)
|
||||
|
||||
def and_box_changed(self, state, line=None):
|
||||
if state == 2:
|
||||
|
340
src/calibre/gui2/preferences/coloring.py
Normal file
340
src/calibre/gui2/preferences/coloring.py
Normal file
@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox,
|
||||
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
|
||||
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox)
|
||||
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.library.coloring import (Rule, conditionable_columns,
|
||||
displayable_columns)
|
||||
|
||||
class ConditionEditor(QWidget): # {{{
|
||||
|
||||
def __init__(self, fm, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.fm = fm
|
||||
|
||||
self.action_map = {
|
||||
'bool' : (
|
||||
(_('is true'), 'is true',),
|
||||
(_('is false'), 'is false'),
|
||||
(_('is undefined'), 'is undefined')
|
||||
),
|
||||
'int' : (
|
||||
(_('is equal to'), 'eq'),
|
||||
(_('is less than'), 'lt'),
|
||||
(_('is greater than'), 'gt')
|
||||
),
|
||||
'multiple' : (
|
||||
(_('has'), 'has'),
|
||||
(_('does not have'), 'does not have'),
|
||||
(_('has pattern'), 'has pattern'),
|
||||
(_('does not have pattern'), 'does not have pattern'),
|
||||
(_('is set'), 'is set'),
|
||||
(_('is not set'), 'is not set'),
|
||||
),
|
||||
'single' : (
|
||||
(_('is'), 'is'),
|
||||
(_('is not'), 'is not'),
|
||||
(_('matches pattern'), 'matches pattern'),
|
||||
(_('does not match pattern'), 'does not match pattern'),
|
||||
(_('is set'), 'is set'),
|
||||
(_('is not set'), 'is not set'),
|
||||
),
|
||||
}
|
||||
|
||||
for x in ('float', 'rating', 'datetime'):
|
||||
self.action_map[x] = self.action_map['int']
|
||||
|
||||
self.l = l = QGridLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.l1 = l1 = QLabel(_('If the '))
|
||||
l.addWidget(l1, 0, 0)
|
||||
|
||||
self.column_box = QComboBox(self)
|
||||
l.addWidget(self.column_box, 0, 1)
|
||||
|
||||
self.l2 = l2 = QLabel(_(' column '))
|
||||
l.addWidget(l2, 0, 2)
|
||||
|
||||
self.action_box = QComboBox(self)
|
||||
l.addWidget(self.action_box, 0, 3)
|
||||
|
||||
self.l3 = l3 = QLabel(_(' the value '))
|
||||
l.addWidget(l3, 0, 4)
|
||||
|
||||
self.value_box = QLineEdit(self)
|
||||
l.addWidget(self.value_box, 0, 5)
|
||||
|
||||
self.column_box.addItem('', '')
|
||||
for key in sorted(
|
||||
conditionable_columns(fm),
|
||||
key=lambda x:sort_key(fm[x]['name'])):
|
||||
self.column_box.addItem(key, key)
|
||||
self.column_box.setCurrentIndex(0)
|
||||
|
||||
self.column_box.currentIndexChanged.connect(self.init_action_box)
|
||||
self.action_box.currentIndexChanged.connect(self.init_value_box)
|
||||
|
||||
for b in (self.column_box, self.action_box):
|
||||
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
|
||||
b.setMinimumContentsLength(15)
|
||||
|
||||
@dynamic_property
|
||||
def current_col(self):
|
||||
def fget(self):
|
||||
idx = self.column_box.currentIndex()
|
||||
return unicode(self.column_box.itemData(idx).toString())
|
||||
def fset(self, val):
|
||||
for idx in range(self.column_box.count()):
|
||||
c = unicode(self.column_box.itemData(idx).toString())
|
||||
if c == val:
|
||||
self.column_box.setCurrentIndex(idx)
|
||||
return
|
||||
raise ValueError('Column %r not found'%val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@dynamic_property
|
||||
def current_action(self):
|
||||
def fget(self):
|
||||
idx = self.action_box.currentIndex()
|
||||
return unicode(self.action_box.itemData(idx).toString())
|
||||
def fset(self, val):
|
||||
for idx in range(self.action_box.count()):
|
||||
c = unicode(self.action_box.itemData(idx).toString())
|
||||
if c == val:
|
||||
self.action_box.setCurrentIndex(idx)
|
||||
return
|
||||
raise ValueError('Action %r not valid for current column'%val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@property
|
||||
def current_val(self):
|
||||
return unicode(self.value_box.text()).strip()
|
||||
|
||||
@dynamic_property
|
||||
def condition(self):
|
||||
|
||||
def fget(self):
|
||||
c, a, v = (self.current_col, self.current_action,
|
||||
self.current_val)
|
||||
if not c or not a:
|
||||
return None
|
||||
return (c, a, v)
|
||||
|
||||
def fset(self, condition):
|
||||
c, a, v = condition
|
||||
if not v:
|
||||
v = ''
|
||||
v = v.strip()
|
||||
self.current_col = c
|
||||
self.current_action = a
|
||||
self.value_box.setText(v)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def init_action_box(self):
|
||||
self.action_box.blockSignals(True)
|
||||
self.action_box.clear()
|
||||
self.action_box.addItem('', '')
|
||||
col = self.current_col
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
if dt in self.action_map:
|
||||
actions = self.action_map[dt]
|
||||
else:
|
||||
k = 'multiple' if m['is_multiple'] else 'single'
|
||||
actions = self.action_map[k]
|
||||
|
||||
for text, key in actions:
|
||||
self.action_box.addItem(text, key)
|
||||
self.action_box.setCurrentIndex(0)
|
||||
self.action_box.blockSignals(False)
|
||||
self.init_value_box()
|
||||
|
||||
def init_value_box(self):
|
||||
self.value_box.setEnabled(True)
|
||||
self.value_box.setText('')
|
||||
self.value_box.setInputMask('')
|
||||
self.value_box.setValidator(None)
|
||||
col = self.current_col
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
action = self.current_action
|
||||
if not col or not action:
|
||||
return
|
||||
tt = ''
|
||||
if dt in ('int', 'float', 'rating'):
|
||||
tt = _('Enter a number')
|
||||
v = QIntValidator if dt == 'int' else QDoubleValidator
|
||||
self.value_box.setValidator(v(self.value_box))
|
||||
elif dt == 'datetime':
|
||||
self.value_box.setInputMask('9999-99-99')
|
||||
tt = _('Enter a date in the format YYYY-MM-DD')
|
||||
else:
|
||||
tt = _('Enter a string')
|
||||
if 'pattern' in action:
|
||||
tt = _('Enter a regular expression')
|
||||
self.value_box.setToolTip(tt)
|
||||
if action in ('is set', 'is not set', 'is true', 'is false',
|
||||
'is undefined'):
|
||||
self.value_box.setEnabled(False)
|
||||
# }}}
|
||||
|
||||
class RuleEditor(QDialog): # {{{
|
||||
|
||||
def __init__(self, fm, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.fm = fm
|
||||
|
||||
self.setWindowIcon(QIcon(I('format-fill-color.png')))
|
||||
self.setWindowTitle(_('Create/edit a column coloring rule'))
|
||||
|
||||
self.l = l = QGridLayout(self)
|
||||
self.setLayout(l)
|
||||
|
||||
self.l1 = l1 = QLabel(_('Create a coloring rule by'
|
||||
' filling in the boxes below'))
|
||||
l.addWidget(l1, 0, 0, 1, 4)
|
||||
|
||||
self.f1 = QFrame(self)
|
||||
self.f1.setFrameShape(QFrame.HLine)
|
||||
l.addWidget(self.f1, 1, 0, 1, 4)
|
||||
|
||||
self.l2 = l2 = QLabel(_('Set the color of the column:'))
|
||||
l.addWidget(l2, 2, 0)
|
||||
|
||||
self.column_box = QComboBox(self)
|
||||
l.addWidget(self.column_box, 2, 1)
|
||||
|
||||
self.l3 = l3 = QLabel(_('to'))
|
||||
l3.setAlignment(Qt.AlignHCenter)
|
||||
l.addWidget(l3, 2, 2)
|
||||
|
||||
self.color_box = QComboBox(self)
|
||||
l.addWidget(self.color_box, 2, 3)
|
||||
|
||||
self.l4 = l4 = QLabel(
|
||||
_('Only if the following conditions are all satisfied:'))
|
||||
l4.setAlignment(Qt.AlignHCenter)
|
||||
l.addWidget(l4, 3, 0, 1, 4)
|
||||
|
||||
self.scroll_area = sa = QScrollArea(self)
|
||||
sa.setMinimumHeight(300)
|
||||
sa.setMinimumWidth(950)
|
||||
sa.setWidgetResizable(True)
|
||||
l.addWidget(sa, 4, 0, 1, 4)
|
||||
|
||||
self.add_button = b = QPushButton(QIcon(I('plus.png')),
|
||||
_('Add another condition'))
|
||||
l.addWidget(b, 5, 0, 1, 4)
|
||||
b.clicked.connect(self.add_blank_condition)
|
||||
|
||||
self.l5 = l5 = QLabel(_('You can disable a condition by'
|
||||
' blanking all of its boxes'))
|
||||
l.addWidget(l5, 6, 0, 1, 4)
|
||||
|
||||
self.bb = bb = QDialogButtonBox(
|
||||
QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
l.addWidget(bb, 7, 0, 1, 4)
|
||||
|
||||
self.conditions_widget = QWidget(self)
|
||||
sa.setWidget(self.conditions_widget)
|
||||
self.conditions_widget.setLayout(QVBoxLayout())
|
||||
self.conditions_widget.layout().setAlignment(Qt.AlignTop)
|
||||
self.conditions = []
|
||||
|
||||
for b in (self.column_box, self.color_box):
|
||||
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
|
||||
b.setMinimumContentsLength(15)
|
||||
|
||||
for key in sorted(
|
||||
displayable_columns(fm),
|
||||
key=lambda x:sort_key(fm[x]['name'])):
|
||||
name = fm[key]['name']
|
||||
if name:
|
||||
self.column_box.addItem(key, key)
|
||||
self.column_box.setCurrentIndex(0)
|
||||
|
||||
self.color_box.addItems(QColor.colorNames())
|
||||
self.color_box.setCurrentIndex(0)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def add_blank_condition(self):
|
||||
c = ConditionEditor(self.fm, parent=self.conditions_widget)
|
||||
self.conditions.append(c)
|
||||
self.conditions_widget.layout().addWidget(c)
|
||||
|
||||
def accept(self):
|
||||
if self.validate():
|
||||
QDialog.accept(self)
|
||||
|
||||
def validate(self):
|
||||
r = Rule(self.fm)
|
||||
for c in self.conditions:
|
||||
condition = c.condition
|
||||
if condition is not None:
|
||||
try:
|
||||
r.add_condition(*condition)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_dialog(self, _('Invalid condition'),
|
||||
_('One of the conditions for this rule is'
|
||||
' invalid: <b>%s</b>')%e,
|
||||
det_msg=traceback.format_exc(), show=True)
|
||||
return False
|
||||
if len(r.conditions) < 1:
|
||||
error_dialog(self, _('No conditions'),
|
||||
_('You must specify at least one non-empty condition'
|
||||
' for this rule'), show=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def rule(self):
|
||||
r = Rule(self.fm)
|
||||
r.color = unicode(self.color_box.currentText())
|
||||
idx = self.column_box.currentIndex()
|
||||
col = unicode(self.column_box.itemData(idx).toString())
|
||||
for c in self.conditions:
|
||||
condition = c.condition
|
||||
if condition is not None:
|
||||
r.add_condition(*condition)
|
||||
|
||||
return col, r
|
||||
# }}}
|
||||
|
||||
class EditRules(QWidget):
|
||||
|
||||
def __init__(self, db, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.db = db
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
|
||||
from calibre.library import db
|
||||
|
||||
d = RuleEditor(db().field_metadata)
|
||||
d.add_blank_condition()
|
||||
d.exec_()
|
||||
|
||||
col, r = d.rule
|
||||
|
||||
print ('Column to be colored:', col)
|
||||
print ('Template:')
|
||||
print (r.template)
|
||||
|
@ -7,7 +7,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import json, traceback
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from PyQt4.Qt import QDialogButtonBox
|
||||
|
||||
from calibre.gui2 import error_dialog, warning_dialog
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences.template_functions_ui import Ui_Form
|
||||
from calibre.gui2.widgets import PythonHighlighter
|
||||
@ -152,10 +154,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
_('Name already used'), show=True)
|
||||
return
|
||||
if self.argument_count.value() == 0:
|
||||
error_dialog(self.gui, _('Template functions'),
|
||||
_('Argument count must be -1 or greater than zero'),
|
||||
show=True)
|
||||
return
|
||||
box = warning_dialog(self.gui, _('Template functions'),
|
||||
_('Argument count should be -1 or greater than zero.'
|
||||
'Setting it to zero means that this function cannot '
|
||||
'be used in single function mode.'), det_msg = '',
|
||||
show=False)
|
||||
box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.Cancel)
|
||||
box.det_msg_toggle.setVisible(False)
|
||||
if not box.exec_():
|
||||
return
|
||||
try:
|
||||
prog = unicode(self.program.toPlainText())
|
||||
cls = compile_user_function(name, unicode(self.documentation.toPlainText()),
|
||||
|
78
src/calibre/gui2/store/epubbud_plugin.py
Normal file
78
src/calibre/gui2/store/epubbud_plugin.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class EpubBudStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://epubbud.com/'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
OPDS based search.
|
||||
|
||||
We really should get the catelog from http://pragprog.com/catalog.opds
|
||||
and look for the application/opensearchdescription+xml entry.
|
||||
Then get the opensearch description to get the search url and
|
||||
format. However, we are going to be lazy and hard code it.
|
||||
'''
|
||||
url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
# Use html instead of etree as html allows us
|
||||
# to ignore the namespace easily.
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//entry'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//id/text()'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href'))
|
||||
|
||||
title = u''.join(data.xpath('.//title/text()'))
|
||||
author = u''.join(data.xpath('.//author/name/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = '$0.00'
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB'
|
||||
|
||||
yield s
|
178
src/calibre/library/coloring.py
Normal file
178
src/calibre/library/coloring.py
Normal file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
from future_builtins import map
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import binascii, re, json
|
||||
from textwrap import dedent
|
||||
|
||||
class Rule(object): # {{{
|
||||
|
||||
SIGNATURE = '# BasicColorRule():'
|
||||
|
||||
def __init__(self, fm, color=None):
|
||||
self.color = color
|
||||
self.fm = fm
|
||||
self.conditions = []
|
||||
|
||||
def add_condition(self, col, action, val):
|
||||
if col not in self.fm:
|
||||
raise ValueError('%r is not a valid column name'%col)
|
||||
v = self.validate_condition(col, action, val)
|
||||
if v:
|
||||
raise ValueError(v)
|
||||
self.conditions.append((col, action, val))
|
||||
|
||||
def validate_condition(self, col, action, val):
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
|
||||
try:
|
||||
int(val) if dt == 'int' else float(val)
|
||||
except:
|
||||
return '%r is not a valid numerical value'%val
|
||||
|
||||
if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
|
||||
in action):
|
||||
try:
|
||||
re.compile(val)
|
||||
except:
|
||||
return '%r is not a valid regular expression'%val
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
args = (self.color, self.conditions)
|
||||
sig = json.dumps(args, ensure_ascii=False)
|
||||
return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
if not self.color or not self.conditions:
|
||||
return None
|
||||
conditions = map(self.apply_condition, self.conditions)
|
||||
conditions = (',\n' + ' '*9).join(conditions)
|
||||
return dedent('''\
|
||||
program:
|
||||
{sig}
|
||||
test(and(
|
||||
{conditions}
|
||||
), {color}, '');
|
||||
''').format(sig=self.signature, conditions=conditions,
|
||||
color=self.color)
|
||||
|
||||
def apply_condition(self, condition):
|
||||
col, action, val = condition
|
||||
m = self.fm[col]
|
||||
dt = m['datatype']
|
||||
|
||||
if dt == 'bool':
|
||||
return self.bool_condition(col, action, val)
|
||||
|
||||
if dt in ('int', 'float', 'rating'):
|
||||
return self.number_condition(col, action, val)
|
||||
|
||||
if dt == 'datetime':
|
||||
return self.date_condition(col, action, val)
|
||||
|
||||
if dt in ('comments', 'series', 'text', 'enumeration'):
|
||||
ism = m.get('is_multiple', False)
|
||||
if ism:
|
||||
return self.multiple_condition(col, action, val, ism)
|
||||
return self.text_condition(col, action, val)
|
||||
|
||||
def bool_condition(self, col, action, val):
|
||||
test = {'is true': 'True',
|
||||
'is false': 'False',
|
||||
'is undefined': 'None'}[action]
|
||||
return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
|
||||
|
||||
def number_condition(self, col, action, val):
|
||||
lt, eq, gt = {
|
||||
'eq': ('', '1', ''),
|
||||
'lt': ('1', '', ''),
|
||||
'gt': ('', '', '1')
|
||||
}[action]
|
||||
lt, eq, gt = '', '1', ''
|
||||
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
|
||||
|
||||
def date_condition(self, col, action, val):
|
||||
lt, eq, gt = {
|
||||
'eq': ('', '1', ''),
|
||||
'lt': ('1', '', ''),
|
||||
'gt': ('', '', '1')
|
||||
}[action]
|
||||
return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
|
||||
val, lt, eq, gt)
|
||||
|
||||
def multiple_condition(self, col, action, val, sep):
|
||||
if action == 'is set':
|
||||
return "test('%s', '1', '')"%col
|
||||
if action == 'is not set':
|
||||
return "test('%s', '', '1')"%col
|
||||
if action == 'has':
|
||||
return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
|
||||
if action == 'does not have':
|
||||
return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
|
||||
if action == 'has pattern':
|
||||
return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
|
||||
if action == 'does not have pattern':
|
||||
return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
|
||||
|
||||
def text_condition(self, col, action, val):
|
||||
if action == 'is set':
|
||||
return "test('%s', '1', '')"%col
|
||||
if action == 'is not set':
|
||||
return "test('%s', '', '1')"%col
|
||||
if action == 'is':
|
||||
return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
|
||||
if action == 'is not':
|
||||
return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
|
||||
if action == 'matches pattern':
|
||||
return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
|
||||
if action == 'does not match pattern':
|
||||
return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
|
||||
|
||||
# }}}
|
||||
|
||||
def rule_from_template(fm, template):
|
||||
ok_lines = []
|
||||
for line in template.splitlines():
|
||||
if line.startswith(Rule.SIGNATURE):
|
||||
raw = line[len(Rule.SIGNATURE):].strip()
|
||||
try:
|
||||
color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
|
||||
except:
|
||||
continue
|
||||
r = Rule(fm)
|
||||
r.color = color
|
||||
for c in conditions:
|
||||
try:
|
||||
r.add_condition(*c)
|
||||
except:
|
||||
continue
|
||||
if r.color and r.conditions:
|
||||
return r
|
||||
else:
|
||||
ok_lines.append(line)
|
||||
return '\n'.join(ok_lines)
|
||||
|
||||
def conditionable_columns(fm):
|
||||
for key in fm:
|
||||
m = fm[key]
|
||||
dt = m['datatype']
|
||||
if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
|
||||
'comments', 'text', 'enumeration', 'datetime'):
|
||||
yield key
|
||||
|
||||
|
||||
def displayable_columns(fm):
|
||||
for key in fm.displayable_field_keys():
|
||||
if key not in ('sort', 'author_sort', 'comments', 'formats',
|
||||
'identifiers', 'path'):
|
||||
yield key
|
||||
|
@ -240,11 +240,21 @@ def cli_docs(app):
|
||||
raw += '\n'+'\n'.join(lines)
|
||||
update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info)
|
||||
|
||||
def generate_docs(app):
|
||||
cli_docs(app)
|
||||
template_docs(app)
|
||||
|
||||
def template_docs(app):
|
||||
from template_ref_generate import generate_template_language_help
|
||||
info = app.builder.info
|
||||
raw = generate_template_language_help()
|
||||
update_cli_doc('template_ref.rst', raw, info)
|
||||
|
||||
def setup(app):
|
||||
app.add_config_value('epub_cover', None, False)
|
||||
app.add_builder(EPUBHelpBuilder)
|
||||
app.connect('doctree-read', substitute)
|
||||
app.connect('builder-inited', cli_docs)
|
||||
app.connect('builder-inited', generate_docs)
|
||||
app.connect('build-finished', finished)
|
||||
|
||||
def finished(app, exception):
|
||||
|
@ -1,266 +0,0 @@
|
||||
.. include:: global.rst
|
||||
|
||||
.. _templaterefcalibre:
|
||||
|
||||
Reference for all builtin template language functions
|
||||
========================================================
|
||||
|
||||
Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
|
||||
.. module:: calibre.utils.formatter_functions
|
||||
|
||||
Get values from metadata
|
||||
--------------------------
|
||||
|
||||
field(name)
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinField
|
||||
|
||||
raw_field(name)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinRaw_field
|
||||
|
||||
booksize()
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinBooksize
|
||||
|
||||
format_date(val, format_string)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinFormat_date
|
||||
|
||||
ondevice()
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinOndevice
|
||||
|
||||
Arithmetic
|
||||
-------------
|
||||
|
||||
add(x, y)
|
||||
^^^^^^^^^^^^^
|
||||
.. autoclass:: BuiltinAdd
|
||||
|
||||
subtract(x, y)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubtract
|
||||
|
||||
multiply(x, y)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinMultiply
|
||||
|
||||
divide(x, y)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinDivide
|
||||
|
||||
Boolean
|
||||
------------
|
||||
|
||||
and(value1, value2, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinAnd
|
||||
|
||||
or(value1, value2, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinOr
|
||||
|
||||
not(value)
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinNot
|
||||
|
||||
If-then-else
|
||||
-----------------
|
||||
|
||||
contains(val, pattern, text if match, text if not match)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinContains
|
||||
|
||||
test(val, text if not empty, text if empty)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTest
|
||||
|
||||
ifempty(val, text if empty)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinIfempty
|
||||
|
||||
Iterating over values
|
||||
------------------------
|
||||
|
||||
first_non_empty(value, value, ...)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinFirstNonEmpty
|
||||
|
||||
lookup(val, pattern, field, pattern, field, ..., else_field)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinLookup
|
||||
|
||||
switch(val, pattern, value, pattern, value, ..., else_value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSwitch
|
||||
|
||||
List Lookup
|
||||
---------------
|
||||
|
||||
in_list(val, separator, pattern, found_val, not_found_val)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinInList
|
||||
|
||||
str_in_list(val, separator, string, found_val, not_found_val)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinStrInList
|
||||
|
||||
list_item(val, index, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinListitem
|
||||
|
||||
select(val, key)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSelect
|
||||
|
||||
|
||||
List Manipulation
|
||||
-------------------
|
||||
|
||||
count(val, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCount
|
||||
|
||||
merge_lists(list1, list2, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinMergeLists
|
||||
|
||||
sublist(val, start_index, end_index, separator)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSublist
|
||||
|
||||
subitems(val, start_index, end_index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubitems
|
||||
|
||||
Recursion
|
||||
-------------
|
||||
|
||||
eval(template)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinEval
|
||||
|
||||
template(x)
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTemplate
|
||||
|
||||
Relational
|
||||
-----------
|
||||
|
||||
cmp(x, y, lt, eq, gt)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCmp
|
||||
|
||||
strcmp(x, y, lt, eq, gt)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinStrcmp
|
||||
|
||||
String case changes
|
||||
---------------------
|
||||
|
||||
lowercase(val)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinLowercase
|
||||
|
||||
uppercase(val)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinUppercase
|
||||
|
||||
titlecase(val)
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinTitlecase
|
||||
|
||||
capitalize(val)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinCapitalize
|
||||
|
||||
String Manipulation
|
||||
---------------------
|
||||
|
||||
re(val, pattern, replacement)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinRe
|
||||
|
||||
shorten(val, left chars, middle text, right chars)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinShorten
|
||||
|
||||
substr(str, start, end)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinSubstr
|
||||
|
||||
|
||||
Other
|
||||
--------
|
||||
|
||||
assign(id, val)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinAssign
|
||||
|
||||
print(a, b, ...)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: BuiltinPrint
|
||||
|
||||
|
||||
API of the Metadata objects
|
||||
----------------------------
|
||||
|
||||
The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions.
|
||||
|
||||
.. module:: calibre.ebooks.metadata.book.base
|
||||
|
||||
.. autoclass:: Metadata
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. data:: STANDARD_METADATA_FIELDS
|
||||
|
||||
The set of standard metadata fields.
|
||||
|
||||
.. literalinclude:: ../ebooks/metadata/book/__init__.py
|
||||
:lines: 7-
|
||||
|
92
src/calibre/manual/template_ref_generate.py
Normal file
92
src/calibre/manual/template_ref_generate.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
PREAMBLE = '''\
|
||||
.. include:: global.rst
|
||||
|
||||
.. _templaterefcalibre:
|
||||
|
||||
Reference for all builtin template language functions
|
||||
========================================================
|
||||
|
||||
Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
|
||||
.. module:: calibre.utils.formatter_functions
|
||||
|
||||
'''
|
||||
|
||||
CATEGORY_TEMPLATE = '''\
|
||||
{category}
|
||||
{dashes}
|
||||
|
||||
'''
|
||||
|
||||
FUNCTION_TEMPLATE = '''\
|
||||
{fs}
|
||||
{hats}
|
||||
|
||||
.. autoclass:: {cn}
|
||||
|
||||
'''
|
||||
|
||||
POSTAMBLE = '''\
|
||||
|
||||
API of the Metadata objects
|
||||
----------------------------
|
||||
|
||||
The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions.
|
||||
|
||||
.. module:: calibre.ebooks.metadata.book.base
|
||||
|
||||
.. autoclass:: Metadata
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. data:: STANDARD_METADATA_FIELDS
|
||||
|
||||
The set of standard metadata fields.
|
||||
|
||||
.. literalinclude:: ../ebooks/metadata/book/__init__.py
|
||||
:lines: 7-
|
||||
'''
|
||||
|
||||
|
||||
def generate_template_language_help():
|
||||
from calibre.utils.formatter_functions import all_builtin_functions
|
||||
|
||||
funcs = defaultdict(dict)
|
||||
|
||||
for func in all_builtin_functions:
|
||||
class_name = func.__class__.__name__
|
||||
func_sig = getattr(func, 'doc')
|
||||
x = func_sig.find(' -- ')
|
||||
if x < 0:
|
||||
print 'No sig for ', class_name
|
||||
continue
|
||||
func_sig = func_sig[:x]
|
||||
func_cat = getattr(func, 'category')
|
||||
funcs[func_cat][func_sig] = class_name
|
||||
|
||||
output = PREAMBLE
|
||||
cats = sorted(funcs.keys())
|
||||
for cat in cats:
|
||||
output += CATEGORY_TEMPLATE.format(category=cat, dashes='-'*len(cat))
|
||||
entries = [k for k in sorted(funcs[cat].keys())]
|
||||
for entry in entries:
|
||||
output += FUNCTION_TEMPLATE.format(fs = entry, cn=funcs[cat][entry],
|
||||
hats='^'*len(entry))
|
||||
|
||||
output += POSTAMBLE
|
||||
return output
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_template_language_help()
|
@ -57,6 +57,7 @@ class FormatterFunction(object):
|
||||
|
||||
doc = _('No documentation provided')
|
||||
name = 'no name provided'
|
||||
category = 'Unknown'
|
||||
arg_count = 0
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
||||
@ -87,6 +88,7 @@ class BuiltinFormatterFunction(FormatterFunction):
|
||||
class BuiltinStrcmp(BuiltinFormatterFunction):
|
||||
name = 'strcmp'
|
||||
arg_count = 5
|
||||
category = 'Relational'
|
||||
__doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x '
|
||||
'and y as strings. Returns lt if x < y. Returns eq if x == y. '
|
||||
'Otherwise returns gt.')
|
||||
@ -101,6 +103,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction):
|
||||
|
||||
class BuiltinCmp(BuiltinFormatterFunction):
|
||||
name = 'cmp'
|
||||
category = 'Relational'
|
||||
arg_count = 5
|
||||
__doc__ = doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to '
|
||||
'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt.')
|
||||
@ -117,6 +120,7 @@ class BuiltinCmp(BuiltinFormatterFunction):
|
||||
class BuiltinStrcat(BuiltinFormatterFunction):
|
||||
name = 'strcat'
|
||||
arg_count = -1
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a '
|
||||
'string formed by concatenating all the arguments')
|
||||
|
||||
@ -130,6 +134,7 @@ class BuiltinStrcat(BuiltinFormatterFunction):
|
||||
class BuiltinAdd(BuiltinFormatterFunction):
|
||||
name = 'add'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -140,6 +145,7 @@ class BuiltinAdd(BuiltinFormatterFunction):
|
||||
class BuiltinSubtract(BuiltinFormatterFunction):
|
||||
name = 'subtract'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -150,6 +156,7 @@ class BuiltinSubtract(BuiltinFormatterFunction):
|
||||
class BuiltinMultiply(BuiltinFormatterFunction):
|
||||
name = 'multiply'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -160,6 +167,7 @@ class BuiltinMultiply(BuiltinFormatterFunction):
|
||||
class BuiltinDivide(BuiltinFormatterFunction):
|
||||
name = 'divide'
|
||||
arg_count = 2
|
||||
category = 'Arithmetic'
|
||||
__doc__ = doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, x, y):
|
||||
@ -170,6 +178,8 @@ class BuiltinDivide(BuiltinFormatterFunction):
|
||||
class BuiltinTemplate(BuiltinFormatterFunction):
|
||||
name = 'template'
|
||||
arg_count = 1
|
||||
category = 'Recursion'
|
||||
|
||||
__doc__ = doc = _('template(x) -- evaluates x as a template. The evaluation is done '
|
||||
'in its own context, meaning that variables are not shared between '
|
||||
'the caller and the template evaluation. Because the { and } '
|
||||
@ -185,6 +195,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
|
||||
class BuiltinEval(BuiltinFormatterFunction):
|
||||
name = 'eval'
|
||||
arg_count = 1
|
||||
category = 'Recursion'
|
||||
__doc__ = doc = _('eval(template) -- evaluates the template, passing the local '
|
||||
'variables (those \'assign\'ed to) instead of the book metadata. '
|
||||
' This permits using the template processor to construct complex '
|
||||
@ -198,6 +209,7 @@ class BuiltinEval(BuiltinFormatterFunction):
|
||||
class BuiltinAssign(BuiltinFormatterFunction):
|
||||
name = 'assign'
|
||||
arg_count = 2
|
||||
category = 'Other'
|
||||
__doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. '
|
||||
'id must be an identifier, not an expression')
|
||||
|
||||
@ -208,6 +220,7 @@ class BuiltinAssign(BuiltinFormatterFunction):
|
||||
class BuiltinPrint(BuiltinFormatterFunction):
|
||||
name = 'print'
|
||||
arg_count = -1
|
||||
category = 'Other'
|
||||
__doc__ = doc = _('print(a, b, ...) -- prints the arguments to standard output. '
|
||||
'Unless you start calibre from the command line (calibre-debug -g), '
|
||||
'the output will go to a black hole.')
|
||||
@ -219,14 +232,16 @@ class BuiltinPrint(BuiltinFormatterFunction):
|
||||
class BuiltinField(BuiltinFormatterFunction):
|
||||
name = 'field'
|
||||
arg_count = 1
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('field(name) -- returns the metadata field named by name')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, name):
|
||||
return formatter.get_value(name, [], kwargs)
|
||||
|
||||
class BuiltinRaw_field(BuiltinFormatterFunction):
|
||||
class BuiltinRawField(BuiltinFormatterFunction):
|
||||
name = 'raw_field'
|
||||
arg_count = 1
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('raw_field(name) -- returns the metadata field named by name '
|
||||
'without applying any formatting.')
|
||||
|
||||
@ -236,6 +251,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction):
|
||||
class BuiltinSubstr(BuiltinFormatterFunction):
|
||||
name = 'substr'
|
||||
arg_count = 3
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('substr(str, start, end) -- returns the start\'th through the end\'th '
|
||||
'characters of str. The first character in str is the zero\'th '
|
||||
'character. If end is negative, then it indicates that many '
|
||||
@ -249,6 +265,7 @@ class BuiltinSubstr(BuiltinFormatterFunction):
|
||||
class BuiltinLookup(BuiltinFormatterFunction):
|
||||
name = 'lookup'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- '
|
||||
'like switch, except the arguments are field (metadata) names, not '
|
||||
'text. The value of the appropriate field will be fetched and used. '
|
||||
@ -276,6 +293,7 @@ class BuiltinLookup(BuiltinFormatterFunction):
|
||||
class BuiltinTest(BuiltinFormatterFunction):
|
||||
name = 'test'
|
||||
arg_count = 3
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not '
|
||||
'empty` if the field is not empty, otherwise return `text if empty`')
|
||||
|
||||
@ -288,6 +306,7 @@ class BuiltinTest(BuiltinFormatterFunction):
|
||||
class BuiltinContains(BuiltinFormatterFunction):
|
||||
name = 'contains'
|
||||
arg_count = 4
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('contains(val, pattern, text if match, text if not match) -- checks '
|
||||
'if field contains matches for the regular expression `pattern`. '
|
||||
'Returns `text if match` if matches are found, otherwise it returns '
|
||||
@ -303,6 +322,7 @@ class BuiltinContains(BuiltinFormatterFunction):
|
||||
class BuiltinSwitch(BuiltinFormatterFunction):
|
||||
name = 'switch'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- '
|
||||
'for each `pattern, value` pair, checks if the field matches '
|
||||
'the regular expression `pattern` and if so, returns that '
|
||||
@ -323,6 +343,7 @@ class BuiltinSwitch(BuiltinFormatterFunction):
|
||||
class BuiltinInList(BuiltinFormatterFunction):
|
||||
name = 'in_list'
|
||||
arg_count = 5
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- '
|
||||
'treat val as a list of items separated by separator, '
|
||||
'comparing the pattern against each value in the list. If the '
|
||||
@ -340,6 +361,8 @@ class BuiltinInList(BuiltinFormatterFunction):
|
||||
class BuiltinStrInList(BuiltinFormatterFunction):
|
||||
name = 'str_in_list'
|
||||
arg_count = 5
|
||||
category = 'List Lookup'
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('str_in_list(val, separator, string, found_val, not_found_val) -- '
|
||||
'treat val as a list of items separated by separator, '
|
||||
'comparing the string against each value in the list. If the '
|
||||
@ -360,6 +383,7 @@ class BuiltinStrInList(BuiltinFormatterFunction):
|
||||
class BuiltinRe(BuiltinFormatterFunction):
|
||||
name = 're'
|
||||
arg_count = 3
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('re(val, pattern, replacement) -- return the field after applying '
|
||||
'the regular expression. All instances of `pattern` are replaced '
|
||||
'with `replacement`. As in all of calibre, these are '
|
||||
@ -371,6 +395,7 @@ class BuiltinRe(BuiltinFormatterFunction):
|
||||
class BuiltinIfempty(BuiltinFormatterFunction):
|
||||
name = 'ifempty'
|
||||
arg_count = 2
|
||||
category = 'If-then-else'
|
||||
__doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, '
|
||||
'otherwise return `text if empty`')
|
||||
|
||||
@ -383,6 +408,7 @@ class BuiltinIfempty(BuiltinFormatterFunction):
|
||||
class BuiltinShorten(BuiltinFormatterFunction):
|
||||
name = 'shorten'
|
||||
arg_count = 4
|
||||
category = 'String Manipulation'
|
||||
__doc__ = doc = _('shorten(val, left chars, middle text, right chars) -- Return a '
|
||||
'shortened version of the field, consisting of `left chars` '
|
||||
'characters from the beginning of the field, followed by '
|
||||
@ -408,6 +434,7 @@ class BuiltinShorten(BuiltinFormatterFunction):
|
||||
class BuiltinCount(BuiltinFormatterFunction):
|
||||
name = 'count'
|
||||
arg_count = 2
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('count(val, separator) -- interprets the value as a list of items '
|
||||
'separated by `separator`, returning the number of items in the '
|
||||
'list. Most lists use a comma as the separator, but authors '
|
||||
@ -419,6 +446,7 @@ class BuiltinCount(BuiltinFormatterFunction):
|
||||
class BuiltinListitem(BuiltinFormatterFunction):
|
||||
name = 'list_item'
|
||||
arg_count = 3
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('list_item(val, index, separator) -- interpret the value as a list of '
|
||||
'items separated by `separator`, returning the `index`th item. '
|
||||
'The first item is number zero. The last item can be returned '
|
||||
@ -439,6 +467,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
|
||||
class BuiltinSelect(BuiltinFormatterFunction):
|
||||
name = 'select'
|
||||
arg_count = 2
|
||||
category = 'List Lookup'
|
||||
__doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list '
|
||||
'of items, with the items being "id:value". Find the pair with the'
|
||||
'id equal to key, and return the corresponding value.'
|
||||
@ -456,6 +485,7 @@ class BuiltinSelect(BuiltinFormatterFunction):
|
||||
class BuiltinSublist(BuiltinFormatterFunction):
|
||||
name = 'sublist'
|
||||
arg_count = 4
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the '
|
||||
'value as a list of items separated by `separator`, returning a '
|
||||
'new list made from the `start_index` to the `end_index` item. '
|
||||
@ -486,6 +516,7 @@ class BuiltinSublist(BuiltinFormatterFunction):
|
||||
class BuiltinSubitems(BuiltinFormatterFunction):
|
||||
name = 'subitems'
|
||||
arg_count = 3
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to '
|
||||
'break apart lists of items such as genres. It interprets the value '
|
||||
'as a comma-separated list of items, where each item is a period-'
|
||||
@ -520,11 +551,12 @@ class BuiltinSubitems(BuiltinFormatterFunction):
|
||||
pass
|
||||
return ', '.join(sorted(rv, key=sort_key))
|
||||
|
||||
class BuiltinFormat_date(BuiltinFormatterFunction):
|
||||
class BuiltinFormatDate(BuiltinFormatterFunction):
|
||||
name = 'format_date'
|
||||
arg_count = 2
|
||||
__doc__ = doc = _('format_date(val, format_string) -- format the value, which must '
|
||||
'be a date field, using the format_string, returning a string. '
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('format_date(val, format_string) -- format the value, '
|
||||
'which must be a date, using the format_string, returning a string. '
|
||||
'The formatting codes are: '
|
||||
'd : the day as number without a leading zero (1 to 31) '
|
||||
'dd : the day as number with a leading zero (01 to 31) '
|
||||
@ -539,7 +571,7 @@ class BuiltinFormat_date(BuiltinFormatterFunction):
|
||||
'iso : the date with time and timezone. Must be the only format present')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val, format_string):
|
||||
if not val:
|
||||
if not val or val == 'None':
|
||||
return ''
|
||||
try:
|
||||
dt = parse_date(val)
|
||||
@ -551,6 +583,7 @@ class BuiltinFormat_date(BuiltinFormatterFunction):
|
||||
class BuiltinUppercase(BuiltinFormatterFunction):
|
||||
name = 'uppercase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('uppercase(val) -- return value of the field in upper case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -559,6 +592,7 @@ class BuiltinUppercase(BuiltinFormatterFunction):
|
||||
class BuiltinLowercase(BuiltinFormatterFunction):
|
||||
name = 'lowercase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('lowercase(val) -- return value of the field in lower case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -567,6 +601,7 @@ class BuiltinLowercase(BuiltinFormatterFunction):
|
||||
class BuiltinTitlecase(BuiltinFormatterFunction):
|
||||
name = 'titlecase'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('titlecase(val) -- return value of the field in title case')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -575,6 +610,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction):
|
||||
class BuiltinCapitalize(BuiltinFormatterFunction):
|
||||
name = 'capitalize'
|
||||
arg_count = 1
|
||||
category = 'String case changes'
|
||||
__doc__ = doc = _('capitalize(val) -- return value of the field capitalized')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||
@ -583,6 +619,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction):
|
||||
class BuiltinBooksize(BuiltinFormatterFunction):
|
||||
name = 'booksize'
|
||||
arg_count = 0
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('booksize() -- return value of the size field')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals):
|
||||
@ -596,6 +633,7 @@ class BuiltinBooksize(BuiltinFormatterFunction):
|
||||
class BuiltinOndevice(BuiltinFormatterFunction):
|
||||
name = 'ondevice'
|
||||
arg_count = 0
|
||||
category = 'Get values from metadata'
|
||||
__doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return '
|
||||
'the empty string')
|
||||
|
||||
@ -607,6 +645,7 @@ class BuiltinOndevice(BuiltinFormatterFunction):
|
||||
class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
|
||||
name = 'first_non_empty'
|
||||
arg_count = -1
|
||||
category = 'Iterating over values'
|
||||
__doc__ = doc = _('first_non_empty(value, value, ...) -- '
|
||||
'returns the first value that is not empty. If all values are '
|
||||
'empty, then the empty value is returned.'
|
||||
@ -623,6 +662,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
|
||||
class BuiltinAnd(BuiltinFormatterFunction):
|
||||
name = 'and'
|
||||
arg_count = -1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('and(value, value, ...) -- '
|
||||
'returns the string "1" if all values are not empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -639,6 +679,7 @@ class BuiltinAnd(BuiltinFormatterFunction):
|
||||
class BuiltinOr(BuiltinFormatterFunction):
|
||||
name = 'or'
|
||||
arg_count = -1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('or(value, value, ...) -- '
|
||||
'returns the string "1" if any value is not empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -655,6 +696,7 @@ class BuiltinOr(BuiltinFormatterFunction):
|
||||
class BuiltinNot(BuiltinFormatterFunction):
|
||||
name = 'not'
|
||||
arg_count = 1
|
||||
category = 'Boolean'
|
||||
__doc__ = doc = _('not(value) -- '
|
||||
'returns the string "1" if the value is empty, otherwise '
|
||||
'returns the empty string. This function works well with test or '
|
||||
@ -671,6 +713,7 @@ class BuiltinNot(BuiltinFormatterFunction):
|
||||
class BuiltinMergeLists(BuiltinFormatterFunction):
|
||||
name = 'merge_lists'
|
||||
arg_count = 3
|
||||
category = 'List Manipulation'
|
||||
__doc__ = doc = _('merge_lists(list1, list2, separator) -- '
|
||||
'return a list made by merging the items in list1 and list2, '
|
||||
'removing duplicate items using a case-insensitive compare. If '
|
||||
@ -704,7 +747,7 @@ builtin_divide = BuiltinDivide()
|
||||
builtin_eval = BuiltinEval()
|
||||
builtin_first_non_empty = BuiltinFirstNonEmpty()
|
||||
builtin_field = BuiltinField()
|
||||
builtin_format_date = BuiltinFormat_date()
|
||||
builtin_format_date = BuiltinFormatDate()
|
||||
builtin_ifempty = BuiltinIfempty()
|
||||
builtin_in_list = BuiltinInList()
|
||||
builtin_list_item = BuiltinListitem()
|
||||
@ -716,7 +759,7 @@ builtin_not = BuiltinNot()
|
||||
builtin_ondevice = BuiltinOndevice()
|
||||
builtin_or = BuiltinOr()
|
||||
builtin_print = BuiltinPrint()
|
||||
builtin_raw_field = BuiltinRaw_field()
|
||||
builtin_raw_field = BuiltinRawField()
|
||||
builtin_re = BuiltinRe()
|
||||
builtin_select = BuiltinSelect()
|
||||
builtin_shorten = BuiltinShorten()
|
||||
|
Loading…
x
Reference in New Issue
Block a user