Merge from trunk

This commit is contained in:
Charles Haley 2010-04-23 21:21:06 +01:00
commit e4b514d2e6
59 changed files with 20859 additions and 8667 deletions

View File

@ -4,6 +4,55 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.6.49
date: 2010-04-23
new features:
- title: "Support for the SpringDesign Alex and the Nokia 5800XM"
tickets: [5215]
- title: "Justification control is now more sophisticated. You can choose to have either un-justified text,
justified text or leave the justification specified in the input document as is."
tickets: [4921]
bug fixes:
- title: "Fix regression that broke database integrity checking in 0.6.48"
tickets: [5329]
- title: "Conversion pipeline: Ignore links in the HTML that have quoted non-ASCII characters, since there is no way to decode them correctly."
tickets: [5354]
- title: "Make title casing more intelligent, based on the guidelines for the New York Times style manual"
tickets: [3086]
- title: "MOBI Input: Handle hexadecimal entities used to specify angle brackets"
tickets: [5336]
- title: "Fix rendering of ratings column in linux when using a 'fancy' style."
- title: "MOBI Input: Don't fail when the MOBI metadata species a cover that does not exist."
tickets: [5333]
- title: "Fix display of covers in the ebook viewer from MOBI and LIT files"
tickets: [5342]
- title: "MOBI Input: Fix regression that broke detection of covers in MOBI files when converting"
- title: "Restore blank lines in text only comments when displaying the detailed view for a book"
new recipes:
- title: The West Australian, Kurier, Virtual Shackles
author: Darko Miletic
- title: NPR Music Blogs
author: cix3
improved recipes:
- New York Review of Books
- USA Today
- Guardian
- La Republica
- version: 0.6.48 - version: 0.6.48
date: 2010-04-18 date: 2010-04-18

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -119,5 +119,7 @@ class Guardian(BasicNewsRecipe):
raise NotImplementedError raise NotImplementedError
def postprocess_html(self,soup,first):
return soup.findAll('html')[0]

View File

@ -0,0 +1,50 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
kurier.at
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Kurier(BasicNewsRecipe):
title = 'Kurier'
__author__ = 'Darko Miletic'
description = 'News from Austria'
publisher = 'KURIER'
category = 'news, politics, Austria'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
use_embedded_content = False
language = 'de_AT'
remove_empty_feeds = True
publication_type = 'newspaper'
extra_css = ' body{font-family: Verdana,Helvetica,sans-serif } img{margin-bottom: 0.4em} .bild_us{font-size: x-small} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(attrs={'class':['contenttabs','drucken','versenden','leserbrief','kommentieren','addthis_button']})]
keep_only_tags = [dict(attrs={'id':'content'})]
remove_tags_after = dict(attrs={'id':'author'})
remove_attributes = ['width','height']
feeds = [
(u'Nachrichten', u'http://kurier.at/rss/nachrichten_nachrichten_rss.xml' )
,(u'Techno' , u'http://kurier.at/rss/techno_techno_rss.xml' )
,(u'Wirtschaft' , u'http://kurier.at/rss/wirtschaft_wirtschaft_rss.xml' )
,(u'Kultur' , u'http://kurier.at/rss/kultur_kultur_rss.xml' )
,(u'Freizeit' , u'http://kurier.at/rss/freizeit_freizeit_rss.xml' )
,(u'Wetter' , u'http://kurier.at/rss/oewetter_rss.xml' )
,(u'Verkehr' , u'http://kurier.at/rss/verkehr_rss.xml' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -22,20 +22,35 @@ class LaRepublica(BasicNewsRecipe):
language = 'it' language = 'it'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
oldest_article = 1 oldest_article = 5
max_articles_per_feed = 100 max_articles_per_feed = 100
use_embedded_content = False use_embedded_content = False
recursion = 10 recursion = 10
remove_javascript = True remove_javascript = True
def get_article_url(self, article):
link = article.get('id', article.get('guid', None))
if link is None:
return article
return link
keep_only_tags = [dict(name='div', attrs={'class':'articolo'}),
dict(name='div', attrs={'class':'body-text'}),
dict(name='div', attrs={'class':'page-content'}),
dict(name='div', attrs={'id':'contA'})
]
keep_only_tags = [dict(name='div', attrs={'class':'articolo'})]
remove_tags = [ remove_tags = [
dict(name=['object','link']), dict(name=['object','link']),
dict(name='span',attrs={'class':'linkindice'}), dict(name='span',attrs={'class':'linkindice'}),
dict(name='div',attrs={'class':'bottom-mobile'}), dict(name='div', attrs={'class':'bottom-mobile'}),
dict(name='div',attrs={'id':['rssdiv','blocco']}) dict(name='div', attrs={'id':['rssdiv','blocco']}),
dict(name='div', attrs={'class':'utility'}),
dict(name='div', attrs={'class':'generalbox'})
]
remove_tags_after = [
dict(name='div',attrs={'id':'ugc_linkUpload'})
] ]
feeds = [ feeds = [
@ -51,5 +66,9 @@ class LaRepublica(BasicNewsRecipe):
(u'Repubblica Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'), (u'Repubblica Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'),
(u'Repubblica Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'), (u'Repubblica Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'),
(u'Repubblica Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'), (u'Repubblica Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'),
(u'Repubblica Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml') (u'Repubblica Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml'),
(u'Repubblica Motori', u'http://www.repubblica.it/rss/motori/rss2.0.xml'),
(u'Repubblica Roma', u'http://roma.repubblica.it/rss/rss2.0.xml'),
(u'Repubblica Torino', u'http://torino.repubblica.it/rss/rss2.0.xml')
] ]

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
@ -6,51 +7,81 @@ __docformat__ = 'restructuredtext en'
''' '''
nybooks.com nybooks.com
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from lxml import html
from calibre.constants import preferred_encoding
class NewYorkReviewOfBooks(BasicNewsRecipe): class NewYorkReviewOfBooks(BasicNewsRecipe):
title = u'New York Review of Books' title = u'New York Review of Books (no subscription)'
description = u'Book reviews' description = u'Book reviews'
language = 'en' language = 'en'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal'
no_stylesheets = True
no_javascript = True
needs_subscription = True needs_subscription = True
remove_tags_before = {'id':'container'}
remove_tags = [{'class':['noprint', 'ad', 'footer']}, {'id':'right-content'}] keep_only_tags = [dict(id='article-body')]
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
'center advertisement']})]
preprocess_regexps = [(re.compile(r'<head>.*?</head>', re.DOTALL), lambda
m:'<head></head>')]
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None: br.open('http://www.nybooks.com/account/signin/')
br.open('http://www.nybooks.com/register/') br.select_form(nr = 1)
br.select_form(name='login') br['username'] = self.username
br['email'] = self.username
br['password'] = self.password br['password'] = self.password
br.submit() br.submit()
return br return br
def print_version(self, url):
return url+'?pagination=false'
def parse_index(self): def parse_index(self):
root = html.fromstring(self.browser.open('http://www.nybooks.com/current-issue').read()) soup = self.index_to_soup('http://www.nybooks.com/current-issue')
date = root.xpath('//h4[@class = "date"]')[0]
self.timefmt = ' ['+date.text.encode(preferred_encoding)+']' # Find cover
sidebar = soup.find(id='sidebar')
if sidebar is not None:
a = sidebar.find('a', href=lambda x: x and 'view-photo' in x)
if a is not None:
psoup = self.index_to_soup('http://www.nybooks.com'+a['href'])
cover = psoup.find('img', src=True)
self.cover_url = cover['src']
self.log('Found cover at:', self.cover_url)
# Find date
div = soup.find(id='page-title')
if div is not None:
h5 = div.find('h5')
if h5 is not None:
text = self.tag_to_string(h5)
date = text.partition(u'\u2022')[0].strip()
self.timefmt = u' [%s]'%date
self.log('Issue date:', date)
# Find TOC
toc = soup.find('ul', attrs={'class':'issue-article-list'})
articles = [] articles = []
for tag in date.itersiblings(): for li in toc.findAll('li'):
if tag.tag == 'h4': break h3 = li.find('h3')
if tag.tag == 'p': title = self.tag_to_string(h3)
if tag.get('class') == 'indented': author = self.tag_to_string(li.find('h4'))
articles[-1]['description'] += html.tostring(tag) title = title + u' (%s)'%author
else: url = 'http://www.nybooks.com'+h3.find('a', href=True)['href']
href = tag.xpath('descendant::a[@href]')[0].get('href') desc = ''
article = { for p in li.findAll('p'):
'title': u''.join(tag.xpath('descendant::text()')), desc += self.tag_to_string(p)
'date' : '', self.log('Found article:', title)
'url' : 'http://www.nybooks.com'+href, self.log('\t', url)
'description': '', self.log('\t', desc)
} articles.append({'title':title, 'url':url, 'date':'',
articles.append(article) 'description':desc})
return [('Current Issue', articles)] return [('Current Issue', articles)]

View File

@ -6,10 +6,9 @@ __docformat__ = 'restructuredtext en'
''' '''
nybooks.com nybooks.com
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from lxml import html
from calibre.constants import preferred_encoding
class NewYorkReviewOfBooks(BasicNewsRecipe): class NewYorkReviewOfBooks(BasicNewsRecipe):
@ -17,57 +16,61 @@ class NewYorkReviewOfBooks(BasicNewsRecipe):
description = u'Book reviews' description = u'Book reviews'
language = 'en' language = 'en'
__author__ = 'Kovid Goyal and Sujata Raman' __author__ = 'Kovid Goyal'
no_stylesheets = True no_stylesheets = True
no_javascript = True no_javascript = True
remove_tags_before = {'id':'container'}
remove_tags = [{'class':['noprint', 'ad', 'footer']}, {'id':'right-content'},
dict(name='img', attrs={'src':"/images/1x1-clear.gif"}),
] keep_only_tags = [dict(id='article-body')]
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
'center advertisement']})]
extra_css = ''' preprocess_regexps = [(re.compile(r'<head>.*?</head>', re.DOTALL), lambda
p{font-family:"Times New Roman",Georgia,serif; font-size: 60%;} m:'<head></head>')]
.caption{ font-family:"Times New Roman",Georgia,serif; font-size:40%;}
h2{font-family:"Times New Roman",Georgia,serif; font-size:90%;}
a{ color:#003399; }
.reviewed-title{font-family:"Times New Roman",Georgia,serif;font-size : 50%; font-style:italic;}
.reviewed-author{font-family:"Times New Roman",Georgia,serif;font-size : 50%;}
.reviewed-info{font-family:"Times New Roman",Georgia,serif;font-size : 50%;}
h5{font-family:"Times New Roman",Georgia,serif;font-size : 50%;}
.date{font-family:"Times New Roman",Georgia,serif;font-variant:small-caps;font-size : 50%;}
h4{font-family:"Times New Roman",Georgia,serif;font-size : 50%;}
'''
def preprocess_html(self, soup): def print_version(self, url):
return url+'?pagination=false'
for tag in soup.findAll(name=['span',]):
tag.name = 'div'
for tag in soup.findAll(name=['blockquote',]):
tag.name = 'p'
return soup
def parse_index(self): def parse_index(self):
root = html.fromstring(self.browser.open('http://www.nybooks.com/current-issue').read()) soup = self.index_to_soup('http://www.nybooks.com/current-issue')
date = root.xpath('//h4[@class = "date"]')[0]
self.timefmt = ' ['+date.text.encode(preferred_encoding)+']' # Find cover
sidebar = soup.find(id='sidebar')
if sidebar is not None:
a = sidebar.find('a', href=lambda x: x and 'view-photo' in x)
if a is not None:
psoup = self.index_to_soup('http://www.nybooks.com'+a['href'])
cover = psoup.find('img', src=True)
self.cover_url = cover['src']
self.log('Found cover at:', self.cover_url)
# Find date
div = soup.find(id='page-title')
if div is not None:
h5 = div.find('h5')
if h5 is not None:
text = self.tag_to_string(h5)
date = text.partition(u'\u2022')[0].strip()
self.timefmt = u' [%s]'%date
self.log('Issue date:', date)
# Find TOC
toc = soup.find('ul', attrs={'class':'issue-article-list'})
articles = [] articles = []
for tag in date.itersiblings(): for li in toc.findAll('li'):
if tag.tag == 'h4': break h3 = li.find('h3')
if tag.tag == 'p': title = self.tag_to_string(h3)
if tag.get('class') == 'indented': author = self.tag_to_string(li.find('h4'))
articles[-1]['description'] += html.tostring(tag) title = title + u' (%s)'%author
else: url = 'http://www.nybooks.com'+h3.find('a', href=True)['href']
href = tag.xpath('descendant::a[@href]')[0].get('href') desc = ''
article = { for p in li.findAll('p'):
'title': u''.join(tag.xpath('descendant::text()')), desc += self.tag_to_string(p)
'date' : '', self.log('Found article:', title)
'url' : 'http://www.nybooks.com'+href, self.log('\t', url)
'description': '', self.log('\t', desc)
} articles.append({'title':title, 'url':url, 'date':'',
articles.append(article) 'description':desc})
return [('Current Issue', articles)] return [('Current Issue', articles)]

View File

@ -0,0 +1,63 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
thewest.com.au
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class TheWest(BasicNewsRecipe):
title = 'The West Australian'
__author__ = 'Darko Miletic'
description = 'News from Australia'
publisher = 'thewest.com.au'
category = 'news, politics, Australia'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en_AU'
remove_empty_feeds = True
publication_type = 'newspaper'
masthead_url = 'http://l.yimg.com/ao/i/mp/properties/news/02/wan/img/wan-logo-h49.png'
extra_css = ' .article{font-family: Arial,Helvetica,sans-serif } .image{font-size: x-small} '
preprocess_regexps = [
(re.compile(r'</title>.*?</head>', re.DOTALL|re.IGNORECASE),lambda match: '</title></head>')
]
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(attrs={'class':['tools','lhs']})
,dict(attrs={'id' :'tools-bottom'})
,dict(attrs={'href' :'http://twitter.com/thewest_com_au'})
]
keep_only_tags = [dict(attrs={'class':'mod article'})]
remove_attributes = ['width','height']
feeds = [
(u'WA News' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/wa.xml' )
,(u'National' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/national.xml' )
,(u'World' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/world.xml' )
,(u'Offbeat' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/offbeat.xml' )
,(u'Business' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/business.xml' )
,(u'Sport' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/sport.xml' )
,(u'Entertainment' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/entertainment.xml' )
,(u'Travel' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/travel.xml' )
,(u'Life+Style' , u'http://d.yimg.com/au.rss.news.yahoo.com/thewest/lifestyle.xml' )
]
def get_article_url(self, article):
return article.get('guid', None)
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -377,8 +377,9 @@ class USAToday(BasicNewsRecipe):
if byline: if byline:
byline['class'] = 'byline' byline['class'] = 'byline'
# Replace comma with middot # Replace comma with middot
byline.contents[0].replaceWith(re.sub(","," &middot;", byline.renderContents())) byline.contents[0].replaceWith(re.sub(u",", u" &middot;",
return byline.renderContents() byline.renderContents(encoding=None)))
return byline.renderContents(encoding=None)
else : else :
paras = soup.findAll(text=True) paras = soup.findAll(text=True)
for para in paras: for para in paras:

View File

@ -0,0 +1,33 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.virtualshackles.com
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class Virtualshackles(BasicNewsRecipe):
title = 'Virtual Shackles'
__author__ = 'Darko Miletic'
description = "The adventures of Orion and Jack, making games they'd never play for people they don't like."
category = 'virtual shackles, virtualshackles, games, webcomic, comic, video game, orion, jack'
oldest_article = 10
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = True
encoding = 'cp1252'
publisher = 'Virtual Shackles'
language = 'en'
publication_type = 'comic'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
feeds = [(u'Virtual Shackles', u'http://feeds2.feedburner.com/virtualshackles' )]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -399,38 +399,49 @@ def my_unichr(num):
except ValueError: except ValueError:
return u'?' return u'?'
def entity_to_unicode(match, exceptions=[], encoding='cp1252'): def entity_to_unicode(match, exceptions=[], encoding='cp1252',
result_exceptions={}):
''' '''
@param match: A match object such that '&'+match.group(1)';' is the entity. :param match: A match object such that '&'+match.group(1)';' is the entity.
@param exceptions: A list of entities to not convert (Each entry is the name of the entity, for e.g. 'apos' or '#1234'
@param encoding: The encoding to use to decode numeric entities between 128 and 256. :param exceptions: A list of entities to not convert (Each entry is the name of the entity, for e.g. 'apos' or '#1234'
:param encoding: The encoding to use to decode numeric entities between 128 and 256.
If None, the Unicode UCS encoding is used. A common encoding is cp1252. If None, the Unicode UCS encoding is used. A common encoding is cp1252.
:param result_exceptions: A mapping of characters to entities. If the result
is in result_exceptions, result_exception[result] is returned instead.
Convenient way to specify exception for things like < or > that can be
specified by various actual entities.
''' '''
def check(ch):
return result_exceptions.get(ch, ch)
ent = match.group(1) ent = match.group(1)
if ent in exceptions: if ent in exceptions:
return '&'+ent+';' return '&'+ent+';'
if ent == 'apos': if ent == 'apos':
return "'" return check("'")
if ent == 'hellips': if ent == 'hellips':
ent = 'hellip' ent = 'hellip'
if ent.startswith(u'#x'): if ent.lower().startswith(u'#x'):
num = int(ent[2:], 16) num = int(ent[2:], 16)
if encoding is None or num > 255: if encoding is None or num > 255:
return my_unichr(num) return check(my_unichr(num))
return chr(num).decode(encoding) return check(chr(num).decode(encoding))
if ent.startswith(u'#'): if ent.startswith(u'#'):
try: try:
num = int(ent[1:]) num = int(ent[1:])
except ValueError: except ValueError:
return '&'+ent+';' return '&'+ent+';'
if encoding is None or num > 255: if encoding is None or num > 255:
return my_unichr(num) return check(my_unichr(num))
try: try:
return chr(num).decode(encoding) return check(chr(num).decode(encoding))
except UnicodeDecodeError: except UnicodeDecodeError:
return my_unichr(num) return check(my_unichr(num))
try: try:
return my_unichr(name2codepoint[ent]) return check(my_unichr(name2codepoint[ent]))
except KeyError: except KeyError:
return '&'+ent+';' return '&'+ent+';'

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.6.48' __version__ = '0.6.49'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -438,13 +438,13 @@ from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK from calibre.devices.nook.driver import NOOK
from calibre.devices.prs500.driver import PRS500 from calibre.devices.prs500.driver import PRS500
from calibre.devices.prs505.driver import PRS505, PRS700 from calibre.devices.prs505.driver import PRS505, PRS700
from calibre.devices.android.driver import ANDROID from calibre.devices.android.driver import ANDROID, S60
from calibre.devices.nokia.driver import N770, N810 from calibre.devices.nokia.driver import N770, N810
from calibre.devices.eslick.driver import ESLICK from calibre.devices.eslick.driver import ESLICK
from calibre.devices.nuut2.driver import NUUT2 from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511 from calibre.devices.hanvon.driver import N516, EB511, ALEX
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3 from calibre.devices.teclast.driver import TECLAST_K3
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
@ -506,6 +506,7 @@ plugins += [
PRS700, PRS700,
PRS500, PRS500,
ANDROID, ANDROID,
S60,
N770, N770,
N810, N810,
COOL_ER, COOL_ER,
@ -526,7 +527,8 @@ plugins += [
ELONEX, ELONEX,
TECLAST_K3, TECLAST_K3,
EDGE, EDGE,
SNE SNE,
ALEX
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')] x.__name__.endswith('MetadataReader')]

View File

@ -56,3 +56,21 @@ class ANDROID(USBMS):
dirs = [x.strip() for x in dirs.split(',')] dirs = [x.strip() for x in dirs.split(',')]
self.EBOOK_DIR_MAIN = dirs self.EBOOK_DIR_MAIN = dirs
class S60(USBMS):
name = 'S60 driver'
gui_name = 'S60 phone'
description = _('Communicate with S60 phones.')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
VENDOR_ID = [0x421]
PRODUCT_ID = [0x156]
BCD = [0x100]
# For use with zxreader
FORMATS = ['fb2']
EBOOK_DIR_MAIN = 'FB2 Books'
VENDOR_NAME = 'NOKIA'
WINDOWS_MAIN_MEM = 'S60'

View File

@ -34,6 +34,22 @@ class N516(USBMS):
EBOOK_DIR_MAIN = 'e_book' EBOOK_DIR_MAIN = 'e_book'
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
class ALEX(N516):
name = 'Alex driver'
gui_name = 'SpringDesign Alex'
description = _('Communicate with the SpringDesign Alex eBook reader.')
author = 'Kovid Goyal'
FORMATS = ['epub', 'pdf']
VENDOR_NAME = 'ALEX'
WINDOWS_MAIN_MEM = 'READER'
MAIN_MEMORY_VOLUME_LABEL = 'Alex Internal Memory'
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = True
class EB511(USBMS): class EB511(USBMS):
name = 'Elonex EB 511 driver' name = 'Elonex EB 511 driver'
gui_name = 'EB 511' gui_name = 'EB 511'

View File

@ -322,7 +322,7 @@ class ComicInput(InputFormatPlugin):
('margin_bottom', 0, OptionRecommendation.HIGH), ('margin_bottom', 0, OptionRecommendation.HIGH),
('insert_blank_line', False, OptionRecommendation.HIGH), ('insert_blank_line', False, OptionRecommendation.HIGH),
('remove_paragraph_spacing', False, OptionRecommendation.HIGH), ('remove_paragraph_spacing', False, OptionRecommendation.HIGH),
('dont_justify', True, OptionRecommendation.HIGH), ('change_justification', 'left', OptionRecommendation.HIGH),
('dont_split_on_pagebreaks', True, OptionRecommendation.HIGH), ('dont_split_on_pagebreaks', True, OptionRecommendation.HIGH),
('chapter', None, OptionRecommendation.HIGH), ('chapter', None, OptionRecommendation.HIGH),
('page_breaks_brefore', None, OptionRecommendation.HIGH), ('page_breaks_brefore', None, OptionRecommendation.HIGH),

View File

@ -124,7 +124,7 @@ def add_pipeline_options(parser, plumber):
'linearize_tables', 'linearize_tables',
'extra_css', 'extra_css',
'margin_top', 'margin_left', 'margin_right', 'margin_top', 'margin_left', 'margin_right',
'margin_bottom', 'dont_justify', 'margin_bottom', 'change_justification',
'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size', 'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size',
'asciiize', 'remove_header', 'header_regex', 'asciiize', 'remove_header', 'header_regex',
'remove_footer', 'footer_regex', 'remove_footer', 'footer_regex',

View File

@ -299,12 +299,16 @@ OptionRecommendation(name='margin_right',
help=_('Set the right margin in pts. Default is %default. ' help=_('Set the right margin in pts. Default is %default. '
'Note: 72 pts equals 1 inch')), 'Note: 72 pts equals 1 inch')),
OptionRecommendation(name='dont_justify', OptionRecommendation(name='change_justification',
recommended_value=False, level=OptionRecommendation.LOW, recommended_value='original', level=OptionRecommendation.LOW,
help=_('Do not force text to be justified in output. Whether text ' choices=['left','justify','original'],
'is actually displayed justified or not depends on whether ' help=_('Change text justification. A value of "left" converts all'
'the ebook format and reading device support justification.') ' justified text in the source to left aligned (i.e. '
), 'unjustified) text. A value of "justify" converts all '
'unjustified text to justified. A value of "original" '
'(the default) does not change justification in the '
'source file. Note that only some output formats support '
'justification.')),
OptionRecommendation(name='remove_paragraph_spacing', OptionRecommendation(name='remove_paragraph_spacing',
recommended_value=False, level=OptionRecommendation.LOW, recommended_value=False, level=OptionRecommendation.LOW,

View File

@ -130,7 +130,7 @@ class LRFOutput(OutputFormatPlugin):
]) ])
recommendations = set([ recommendations = set([
('dont_justify', True, OptionRecommendation.HIGH), ('change_justification', 'original', OptionRecommendation.HIGH),
]) ])
def convert_images(self, pages, opts, wide): def convert_images(self, pages, opts, wide):

View File

@ -303,7 +303,12 @@ class MobiReader(object):
for pat in ENCODING_PATS: for pat in ENCODING_PATS:
self.processed_html = pat.sub('', self.processed_html) self.processed_html = pat.sub('', self.processed_html)
e2u = functools.partial(entity_to_unicode, e2u = functools.partial(entity_to_unicode,
exceptions=['lt', 'gt', 'amp', 'apos', 'quot', '#60', '#62']) result_exceptions={
'<' : u'&lt;',
'>' : u'&gt;',
'&' : u'&amp;',
'"' : u'&quot;',
"'" : u'&apos;'})
self.processed_html = re.sub(r'&(\S+?);', e2u, self.processed_html = re.sub(r'&(\S+?);', e2u,
self.processed_html) self.processed_html)
self.extract_images(processed_records, output_dir) self.extract_images(processed_records, output_dir)
@ -619,6 +624,7 @@ class MobiReader(object):
opf.cover = None opf.cover = None
cover = opf.cover cover = opf.cover
cover_copied = None
if cover is not None: if cover is not None:
cover = cover.replace('/', os.sep) cover = cover.replace('/', os.sep)
if os.path.exists(cover): if os.path.exists(cover):
@ -626,13 +632,19 @@ class MobiReader(object):
if os.path.exists(ncover): if os.path.exists(ncover):
os.remove(ncover) os.remove(ncover)
shutil.copyfile(cover, ncover) shutil.copyfile(cover, ncover)
cover_copied = os.path.abspath(ncover)
opf.cover = ncover.replace(os.sep, '/') opf.cover = ncover.replace(os.sep, '/')
manifest = [(htmlfile, 'application/xhtml+xml'), manifest = [(htmlfile, 'application/xhtml+xml'),
(os.path.abspath('styles.css'), 'text/css')] (os.path.abspath('styles.css'), 'text/css')]
bp = os.path.dirname(htmlfile) bp = os.path.dirname(htmlfile)
added = set([])
for i in getattr(self, 'image_names', []): for i in getattr(self, 'image_names', []):
manifest.append((os.path.join(bp, 'images/', i), 'image/jpeg')) path = os.path.join(bp, 'images', i)
added.add(path)
manifest.append((path, 'image/jpeg'))
if cover_copied is not None:
manifest.append((cover_copied, 'image/jpeg'))
opf.create_manifest(manifest) opf.create_manifest(manifest)
opf.create_spine([os.path.basename(htmlfile)]) opf.create_spine([os.path.basename(htmlfile)])

View File

@ -430,7 +430,10 @@ class DirContainer(object):
return f.write(data) return f.write(data)
def exists(self, path): def exists(self, path):
try:
path = os.path.join(self.rootdir, urlunquote(path)) path = os.path.join(self.rootdir, urlunquote(path))
except ValueError: #Happens if path contains quoted special chars
return False
return os.path.isfile(path) return os.path.isfile(path)
def namelist(self): def namelist(self):

View File

@ -212,11 +212,12 @@ class EbookIterator(object):
cover = self.opf.cover cover = self.opf.cover
if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover: if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover:
cfile = os.path.join(os.path.dirname(self.spine[0]), cfile = os.path.join(self.base, 'calibre_iterator_cover.html')
'calibre_iterator_cover.html') chtml = (TITLEPAGE%os.path.relpath(cover, self.base).replace(os.sep,
chtml = (TITLEPAGE%cover).encode('utf-8') '/')).encode('utf-8')
open(cfile, 'wb').write(chtml) open(cfile, 'wb').write(chtml)
self.spine[0:0] = [SpineItem(cfile)] self.spine[0:0] = [SpineItem(cfile,
mime_type='application/xhtml+xml')]
self.delete_on_exit.append(cfile) self.delete_on_exit.append(cfile)
if self.opf.path_to_html_toc is not None and \ if self.opf.path_to_html_toc is not None and \

View File

@ -318,8 +318,8 @@ class Stylizer(object):
if text == 'inherit': if text == 'inherit':
style['text-align'] = 'inherit' style['text-align'] = 'inherit'
else: else:
if text in ('left', 'justify'): if text in ('left', 'justify') and self.opts.change_justification in ('left', 'justify'):
val = 'left' if self.opts.dont_justify else 'justify' val = self.opts.change_justification
style['text-align'] = val style['text-align'] = val
else: else:
style['text-align'] = text style['text-align'] = text

View File

@ -138,8 +138,8 @@ class CSSFlattener(object):
float(self.context.margin_left)) float(self.context.margin_left))
bs.append('margin-right : %fpt'%\ bs.append('margin-right : %fpt'%\
float(self.context.margin_right)) float(self.context.margin_right))
bs.append('text-align: '+ \ if self.context.change_justification != 'original':
('left' if self.context.dont_justify else 'justify')) bs.append('text-align: '+ self.context.change_justification)
body.set('style', '; '.join(bs)) body.set('style', '; '.join(bs))
stylizer = Stylizer(html, item.href, self.oeb, self.context, profile, stylizer = Stylizer(html, item.href, self.oeb, self.context, profile,
user_css=self.context.extra_css, user_css=self.context.extra_css,

View File

@ -19,7 +19,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, 'look_and_feel', Widget.__init__(self, parent, 'look_and_feel',
['dont_justify', 'extra_css', 'base_font_size', ['change_justification', 'extra_css', 'base_font_size',
'font_size_mapping', 'line_height', 'font_size_mapping', 'line_height',
'linearize_tables', 'linearize_tables',
'disable_font_rescaling', 'insert_blank_line', 'disable_font_rescaling', 'insert_blank_line',

View File

@ -84,7 +84,7 @@
<string>...</string> <string>...</string>
</property> </property>
<property name="icon"> <property name="icon">
<iconset resource="../../../../resources/images.qrc"> <iconset>
<normaloff>:/images/wizard.svg</normaloff>:/images/wizard.svg</iconset> <normaloff>:/images/wizard.svg</normaloff>:/images/wizard.svg</iconset>
</property> </property>
<property name="iconSize"> <property name="iconSize">
@ -181,21 +181,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0">
<widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text">
<string>Insert &amp;blank line</string>
</property>
</widget>
</item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QCheckBox" name="opt_dont_justify">
<property name="text">
<string>No text &amp;justification</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="opt_linearize_tables"> <widget class="QCheckBox" name="opt_linearize_tables">
<property name="text"> <property name="text">
<string>&amp;Linearize tables</string> <string>&amp;Linearize tables</string>
@ -221,6 +207,42 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="8" column="0">
<widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text">
<string>Insert &amp;blank line</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Text justification:</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QComboBox" name="opt_change_justification">
<property name="currentIndex">
<number>2</number>
</property>
<item>
<property name="text">
<string>justify</string>
</property>
</item>
<item>
<property name="text">
<string>left</string>
</property>
</item>
<item>
<property name="text">
<string>original</string>
</property>
</item>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -3,9 +3,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
''' import textwrap, os, re
'''
import textwrap, os
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
@ -97,7 +95,12 @@ class BookInfo(QDialog, Ui_BookInfo):
info = self.view.model().get_book_info(row) info = self.view.model().get_book_info(row)
self.setWindowTitle(info[_('Title')]) self.setWindowTitle(info[_('Title')])
self.title.setText('<b>'+info.pop(_('Title'))) self.title.setText('<b>'+info.pop(_('Title')))
self.comments.setText('<div>%s</div>' % info.pop(_('Comments'), '')) comments = info.pop(_('Comments'), '')
if re.search(r'<[a-zA-Z]+>', comments) is None:
lines = comments.splitlines()
lines = [x if x.strip() else '<br><br>' for x in lines]
comments = '\n'.join(lines)
self.comments.setText('<div>%s</div>' % comments)
cdata = info.pop('cover', '') cdata = info.pop('cover', '')
self.cover_pixmap = QPixmap.fromImage(cdata) self.cover_pixmap = QPixmap.fromImage(cdata)
self.resize_cover() self.resize_cover()

View File

@ -29,6 +29,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.do_refresh() self.recipe_model.do_refresh()
self.search = SearchBox2(self) self.search = SearchBox2(self)
self.search.setMinimumContentsLength(25)
self.search.initialize('scheduler_search_history') self.search.initialize('scheduler_search_history')
self.recipe_box.layout().insertWidget(0, self.search) self.recipe_box.layout().insertWidget(0, self.search)
self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'), self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'),

View File

@ -14,7 +14,7 @@
<string>Schedule news download</string> <string>Schedule news download</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="../../../../resources/images.qrc"> <iconset>
<normaloff>:/images/scheduler.svg</normaloff>:/images/scheduler.svg</iconset> <normaloff>:/images/scheduler.svg</normaloff>:/images/scheduler.svg</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
@ -79,7 +79,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>375</width> <width>375</width>
<height>500</height> <height>502</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout_5">
@ -371,7 +371,7 @@
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="standardButtons"> <property name="standardButtons">
<set>QDialogButtonBox::Ok</set> <set>QDialogButtonBox::Save</set>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -9,9 +9,9 @@ from contextlib import closing
from datetime import date from datetime import date
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QIcon,\ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QImage, QApplication, QMenu, \ QImage, QMenu, \
QStyledItemDelegate, QCompleter, QIntValidator, \ QStyledItemDelegate, QCompleter, QIntValidator, \
QPlainTextEdit, QDoubleValidator, QCheckBox, QMessageBox QPlainTextEdit, QDoubleValidator, QCheckBox, QMessageBox
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
@ -30,14 +30,15 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
class RatingDelegate(QItemDelegate): class LibraryDelegate(QStyledItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
SIZE = 16 SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def __init__(self, parent): def __init__(self, parent):
QItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
self._parent = parent self._parent = parent
self.dummy = QModelIndex()
self.star_path = QPainterPath() self.star_path = QPainterPath()
self.star_path.moveTo(90, 50) self.star_path.moveTo(90, 50)
for i in range(1, 5): for i in range(1, 5):
@ -56,9 +57,9 @@ class RatingDelegate(QItemDelegate):
return QSize(5*(self.SIZE), self.SIZE+4) return QSize(5*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index): def paint(self, painter, option, index):
if index.model().data(index, Qt.DisplayRole) is None: style = self._parent.style()
num = 0 option = QStyleOptionViewItemV4(option)
else: self.initStyleOption(option, self.dummy)
num = index.model().data(index, Qt.DisplayRole).toInt()[0] num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star(): def draw_star():
painter.save() painter.save()
@ -71,11 +72,10 @@ class RatingDelegate(QItemDelegate):
painter.save() painter.save()
if hasattr(QStyle, 'CE_ItemViewItem'): if hasattr(QStyle, 'CE_ItemViewItem'):
QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, style.drawControl(QStyle.CE_ItemViewItem, option,
painter, self._parent) painter, self._parent)
elif option.state & QStyle.State_Selected: elif option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight()) painter.fillRect(option.rect, option.palette.highlight())
self.drawFocus(painter, option, option.rect)
try: try:
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
painter.setClipRect(option.rect) painter.setClipRect(option.rect)
@ -94,7 +94,7 @@ class RatingDelegate(QItemDelegate):
painter.restore() painter.restore()
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
sb = QItemDelegate.createEditor(self, parent, option, index) sb = QStyledItemDelegate.createEditor(self, parent, option, index)
sb.setMinimum(0) sb.setMinimum(0)
sb.setMaximum(5) sb.setMaximum(5)
return sb return sb
@ -897,7 +897,7 @@ class BooksView(TableView):
def __init__(self, parent, modelcls=BooksModel): def __init__(self, parent, modelcls=BooksModel):
TableView.__init__(self, parent) TableView.__init__(self, parent)
self.rating_delegate = RatingDelegate(self) self.rating_delegate = LibraryDelegate(self)
self.timestamp_delegate = DateDelegate(self) self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self) self.pubdate_delegate = PubDateDelegate(self)
self.tags_delegate = TagsDelegate(self) self.tags_delegate = TagsDelegate(self)

View File

@ -194,6 +194,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.tool_bar2.insertSeparator(self.action_find_next) self.tool_bar2.insertSeparator(self.action_find_next)
self.setFocusPolicy(Qt.StrongFocus) self.setFocusPolicy(Qt.StrongFocus)
self.search = SearchBox2(self) self.search = SearchBox2(self)
self.search.setMinimumContentsLength(20)
self.search.initialize('viewer_search_history') self.search.initialize('viewer_search_history')
self.search.setToolTip(_('Search for text in book')) self.search.setToolTip(_('Search for text in book'))
self.search.setMinimumWidth(200) self.search.setMinimumWidth(200)

View File

@ -551,7 +551,8 @@ class LineEditECM(object):
self.setText(unicode(self.text()).swapcase()) self.setText(unicode(self.text()).swapcase())
def title_case(self): def title_case(self):
self.setText(unicode(self.text()).title()) from calibre.utils.titlecase import titlecase
self.setText(titlecase(unicode(self.text())))
class EnLineEdit(LineEditECM, QLineEdit): class EnLineEdit(LineEditECM, QLineEdit):

View File

@ -122,10 +122,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.is_case_sensitive = not iswindows and not isosx and \ self.is_case_sensitive = not iswindows and not isosx and \
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB'))
SchemaUpgrade.__init__(self) SchemaUpgrade.__init__(self)
CustomColumns.__init__(self)
self.initialize_dynamic() self.initialize_dynamic()
def initialize_dynamic(self): def initialize_dynamic(self):
CustomColumns.__init__(self)
template = '''\ template = '''\
(SELECT {query} FROM books_{table}_link AS link INNER JOIN (SELECT {query} FROM books_{table}_link AS link INNER JOIN
{table} ON(link.{link_col}={table}.id) WHERE link.book=books.id) {table} ON(link.{link_col}={table}.id) WHERE link.book=books.id)
@ -1490,6 +1490,7 @@ books_series_link feeds
os.remove(self.dbpath) os.remove(self.dbpath)
shutil.copyfile(dest, self.dbpath) shutil.copyfile(dest, self.dbpath)
self.connect() self.connect()
self.initialize_dynamic()
self.refresh() self.refresh()
if os.path.exists(dest): if os.path.exists(dest):
os.remove(dest) os.remove(dest)

View File

@ -81,7 +81,7 @@ Device Integration
What devices does |app| support? What devices does |app| support?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
At the moment |app| has full support for the SONY PRS 300/500/505/600/700/900, Barnes & Noble Nook, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3, various Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk. At the moment |app| has full support for the SONY PRS 300/500/505/600/700/900, Barnes & Noble Nook, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3, SpringDesign Alex, various Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk.
How can I help get my device supported in |app|? How can I help get my device supported in |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

94
src/calibre/utils/titlecase.py Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Original Perl version by: John Gruber http://daringfireball.net/ 10 May 2008
Python version by Stuart Colville http://muffinresearch.co.uk
License: http://www.opensource.org/licenses/mit-license.php
"""
import re
__all__ = ['titlecase']
__version__ = '0.5'
SMALL = 'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?'
PUNCT = r"""!"#$%&'()*+,\-./:;?@[\\\]_`{|}~"""
SMALL_WORDS = re.compile(r'^(%s)$' % SMALL, re.I)
INLINE_PERIOD = re.compile(r'[a-z][.][a-z]', re.I)
UC_ELSEWHERE = re.compile(r'[%s]*?[a-zA-Z]+[A-Z]+?' % PUNCT)
CAPFIRST = re.compile(r"^[%s]*?([A-Za-z])" % PUNCT)
SMALL_FIRST = re.compile(r'^([%s]*)(%s)\b' % (PUNCT, SMALL), re.I)
SMALL_LAST = re.compile(r'\b(%s)[%s]?$' % (SMALL, PUNCT), re.I)
SUBPHRASE = re.compile(r'([:.;?!][ ])(%s)' % SMALL)
APOS_SECOND = re.compile(r"^[dol]{1}[']{1}[a-z]+$", re.I)
ALL_CAPS = re.compile(r'^[A-Z\s%s]+$' % PUNCT)
UC_INITIALS = re.compile(r"^(?:[A-Z]{1}\.{1}|[A-Z]{1}\.{1}[A-Z]{1})+$")
MAC_MC = re.compile(r"^([Mm]a?c)(\w+)")
def titlecase(text):
"""
Titlecases input text
This filter changes all words to Title Caps, and attempts to be clever
about *un*capitalizing SMALL words like a/an/the in the input.
The list of "SMALL words" which are not capped comes from
the New York Times Manual of Style, plus 'vs' and 'v'.
"""
all_caps = ALL_CAPS.match(text)
words = re.split('\s', text)
line = []
for word in words:
if all_caps:
if UC_INITIALS.match(word):
line.append(word)
continue
else:
word = word.lower()
if APOS_SECOND.match(word):
word = word.replace(word[0], word[0].upper())
word = word.replace(word[2], word[2].upper())
line.append(word)
continue
if INLINE_PERIOD.search(word) or UC_ELSEWHERE.match(word):
line.append(word)
continue
if SMALL_WORDS.match(word):
line.append(word.lower())
continue
match = MAC_MC.match(word)
if match:
line.append("%s%s" % (match.group(1).capitalize(),
match.group(2).capitalize()))
continue
hyphenated = []
for item in word.split('-'):
hyphenated.append(CAPFIRST.sub(lambda m: m.group(0).upper(), item))
line.append("-".join(hyphenated))
result = " ".join(line)
result = SMALL_FIRST.sub(lambda m: '%s%s' % (
m.group(1),
m.group(2).capitalize()
), result)
result = SMALL_LAST.sub(lambda m: m.group(0).capitalize(), result)
result = SUBPHRASE.sub(lambda m: '%s%s' % (
m.group(1),
m.group(2).capitalize()
), result)
return result

View File

@ -113,7 +113,7 @@ class NewsItem(NewsTreeItem):
return NONE return NONE
def __cmp__(self, other): def __cmp__(self, other):
return cmp(self.title, getattr(other, 'title', '')) return cmp(self.title.lower(), getattr(other, 'title', '').lower())
class RecipeModel(QAbstractItemModel, SearchQueryParser): class RecipeModel(QAbstractItemModel, SearchQueryParser):