From b808b74add46c5beee2cdb09d680f8f17fcc2e09 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Sep 2012 18:12:15 +0530 Subject: [PATCH 1/8] Ebook-viewer: When displaying amthematics, reflow equations that dont fit on a single line --- resources/compiled_coffeescript.zip | Bin 57017 -> 57014 bytes .../jax/output/SVG/autoload/multiline.js | 2 +- src/calibre/ebooks/oeb/display/mathjax.coffee | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 0f6b8c3e7a522839ce146a7c6211d81b23512310..8e45c8fa6b584c61f35ac0446cc4f880a5b24df1 100644 GIT binary patch delta 147 zcmdnFmwDS>X6XQLW)=|!5b&I+B)cZ z#4tVPnXDKhI=SGk5ff7gkSRO);9V`Ijfr4UrhBGLe+t1&gCe%cA@{VHcrJsv4ww1F V0=!w-Kz6bL;eJL2hKF}RJOBuBEE50# delta 130 zcmdnCmwD%2W|;tQW)=|!5ODOlGMH(5nQ!ufdjf1v I?tm-=040_%)c^nh diff --git a/resources/viewer/mathjax/jax/output/SVG/autoload/multiline.js b/resources/viewer/mathjax/jax/output/SVG/autoload/multiline.js index 30bbe2c45c..4ee0ce4f7a 100644 --- a/resources/viewer/mathjax/jax/output/SVG/autoload/multiline.js +++ b/resources/viewer/mathjax/jax/output/SVG/autoload/multiline.js @@ -187,7 +187,7 @@ MathJax.Hub.Register.StartupHook("SVG Jax Ready",function () { // fill it with the proper elements, // and clean up the bbox // - line = BBOX(); + var line = BBOX(); state.first = broken; state.last = true; this.SVGmoveLine(start,end,line,state,values); line.Clean(); diff --git a/src/calibre/ebooks/oeb/display/mathjax.coffee b/src/calibre/ebooks/oeb/display/mathjax.coffee index cd130c85c8..14633b8fbc 100644 --- a/src/calibre/ebooks/oeb/display/mathjax.coffee +++ b/src/calibre/ebooks/oeb/display/mathjax.coffee @@ -39,7 +39,7 @@ class MathJax showMathMenu: false, extensions: ["tex2jax.js", "asciimath2jax.js", "mml2jax.js"], jax: ["input/TeX","input/MathML","input/AsciiMath","output/SVG"], - // SVG : { linebreaks : { automatic : true } }, + SVG : { linebreaks : { automatic : true } }, TeX: { extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"] } From a6be50425ad235d9e781407dbd99d0d922ad3837 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Sep 2012 18:32:55 +0530 Subject: [PATCH 2/8] ... --- src/calibre/ebooks/conversion/plumber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index dcb5add27e..8f7ab10e0e 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -1120,7 +1120,7 @@ OptionRecommendation(name='search_replace', self.log.info('Creating %s...'%self.output_plugin.name) our = CompositeProgressReporter(0.67, 1., self.ui_reporter) self.output_plugin.report_progress = our - our(0., _('Creating')+' %s'%self.output_plugin.name) + our(0., _('Running %s plugin')%self.output_plugin.name) with self.output_plugin: self.output_plugin.convert(self.oeb, self.output, self.input_plugin, self.opts, self.log) From 9b988d50372a3a3bf44ba8ea7419b6c9ecfa795b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Sep 2012 21:31:05 +0530 Subject: [PATCH 3/8] ... --- src/calibre/ebooks/conversion/plugins/recipe_input.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/ebooks/conversion/plugins/recipe_input.py b/src/calibre/ebooks/conversion/plugins/recipe_input.py index 790c63badd..9bd4077528 100644 --- a/src/calibre/ebooks/conversion/plugins/recipe_input.py +++ b/src/calibre/ebooks/conversion/plugins/recipe_input.py @@ -66,6 +66,7 @@ class RecipeInput(InputFormatPlugin): if os.access(recipe_or_file, os.R_OK): self.recipe_source = open(recipe_or_file, 'rb').read() recipe = compile_recipe(self.recipe_source) + log('Using custom recipe') else: from calibre.web.feeds.recipes.collection import \ get_builtin_recipe_by_title @@ -87,12 +88,15 @@ class RecipeInput(InputFormatPlugin): 'back to builtin one') builtin = True if builtin: + log('Using bundled builtin recipe') raw = get_builtin_recipe_by_title(title, log=log, download_recipe=False) if raw is None: raise ValueError('Failed to find builtin recipe: '+title) recipe = compile_recipe(raw) self.recipe_source = raw + else: + log('Using downloaded builtin recipe') if recipe is None: raise ValueError('%r is not a valid recipe file or builtin recipe' % From 042048e93ac2bca4914bd79ab159c776f09553b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Sep 2012 21:35:09 +0530 Subject: [PATCH 4/8] ... --- recipes/houston_chronicle.recipe | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recipes/houston_chronicle.recipe b/recipes/houston_chronicle.recipe index 639d5c2042..ed430aa45a 100644 --- a/recipes/houston_chronicle.recipe +++ b/recipes/houston_chronicle.recipe @@ -15,11 +15,11 @@ class HoustonChronicle(BasicNewsRecipe): remove_attributes = ['style'] auto_cleanup = True - oldest_article = 2.0 + oldest_article = 3.0 #keep_only_tags = {'class':lambda x: x and ('hst-articletitle' in x or #'hst-articletext' in x or 'hst-galleryitem' in x)} - #remove_attributes = ['xmlns'] + remove_attributes = ['xmlns'] feeds = [ ('News', "http://www.chron.com/rss/feed/News-270.php"), @@ -38,4 +38,4 @@ class HoustonChronicle(BasicNewsRecipe): ] - + From 052ed1010a929bfca14f271b2a9846c9d17c0448 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 11 Sep 2012 10:49:08 +0530 Subject: [PATCH 5/8] ... --- recipes/wsj.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/wsj.recipe b/recipes/wsj.recipe index dc6ec83e60..057da7adf7 100644 --- a/recipes/wsj.recipe +++ b/recipes/wsj.recipe @@ -65,7 +65,7 @@ class WallStreetJournal(BasicNewsRecipe): br['password'] = self.password res = br.submit() raw = res.read() - if '>Log Out<' not in raw: + if 'Welcome,' not in raw and '>Logout<' not in raw and '>Log Out<' not in raw: raise ValueError('Failed to log in to wsj.com, check your ' 'username and password') return br From 9ea57154066bb139d288252ba22a92d66edde35d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 11 Sep 2012 12:49:32 +0530 Subject: [PATCH 6/8] Conversion: Add support for CSS pseudo classes :hover, :link, :visited, :first-line, :focus, :active, :first-letter --- src/calibre/ebooks/conversion/plumber.py | 2 + src/calibre/ebooks/oeb/stylizer.py | 73 +++++++++++++------- src/calibre/ebooks/oeb/transforms/flatcss.py | 70 ++++++++++++++----- 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 8f7ab10e0e..60cce24121 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -1009,6 +1009,8 @@ OptionRecommendation(name='search_replace', pr(0., _('Running transforms on ebook...')) + self.oeb.plumber_output_format = self.output_fmt or '' + from calibre.ebooks.oeb.transforms.guide import Clean Clean()(self.oeb, self.opts) pr(0.1) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 969f7c763a..d558f7f49b 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -268,33 +268,41 @@ class Stylizer(object): self.rules = rules self._styles = {} for _, _, cssdict, text, _ in rules: - fl = ':first-letter' in text - if fl: - text = text.replace(':first-letter', '') + fl = re.search(ur':(first-letter|first-line|link|hover|visited|active|focus)', text) + if fl is not None: + text = text.replace(fl.group(), '') selector = get_css_selector(text) matches = selector(tree, self.logger) - if fl: - from lxml.builder import ElementMaker - E = ElementMaker(namespace=XHTML_NS) - for elem in matches: - for x in elem.iter(): - if x.text: - punctuation_chars = [] - text = unicode(x.text) - while text: - if not unicodedata.category(text[0]).startswith('P'): - break - punctuation_chars.append(text[0]) - text = text[1:] + if fl is not None: + fl = fl.group(1) + if fl == 'first-letter' and getattr(self.oeb, + 'plumber_output_format', '').lower() == u'mobi': + # Fake first-letter + from lxml.builder import ElementMaker + E = ElementMaker(namespace=XHTML_NS) + for elem in matches: + for x in elem.iter(): + if x.text: + punctuation_chars = [] + text = unicode(x.text) + while text: + category = unicodedata.category(text[0]) + if category[0] not in {'P', 'Z'}: + break + punctuation_chars.append(text[0]) + text = text[1:] - special_text = u''.join(punctuation_chars) + \ - (text[0] if text else u'') - span = E.span(special_text) - span.tail = text[1:] - x.text = None - x.insert(0, span) - self.style(span)._update_cssdict(cssdict) - break + special_text = u''.join(punctuation_chars) + \ + (text[0] if text else u'') + span = E.span(special_text) + span.tail = text[1:] + x.text = None + x.insert(0, span) + self.style(span)._update_cssdict(cssdict) + break + else: # Element pseudo-class + for elem in matches: + self.style(elem)._update_pseudo_class(fl, cssdict) else: for elem in matches: self.style(elem)._update_cssdict(cssdict) @@ -495,6 +503,7 @@ class Style(object): self._height = None self._lineHeight = None self._bgcolor = None + self._pseudo_classes = {} stylizer._styles[element] = self def set(self, prop, val): @@ -506,6 +515,11 @@ class Style(object): def _update_cssdict(self, cssdict): self._style.update(cssdict) + def _update_pseudo_class(self, name, cssdict): + orig = self._pseudo_classes.get(name, {}) + orig.update(cssdict) + self._pseudo_classes[name] = orig + def _apply_style_attr(self, url_replacer=None): attrib = self._element.attrib if 'style' not in attrib: @@ -778,3 +792,14 @@ class Style(object): def cssdict(self): return dict(self._style) + + def pseudo_classes(self, filter_css): + if filter_css: + css = copy.deepcopy(self._pseudo_classes) + for psel, cssdict in css.iteritems(): + for k in filter_css: + cssdict.pop(k, None) + else: + css = self._pseudo_classes + return {k:v for k, v in css.iteritems() if v} + diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 72c9dc0d72..6633651a82 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -222,7 +222,7 @@ class CSSFlattener(object): value = 0.0 cssdict[property] = "%0.5fem" % (value / fsize) - def flatten_node(self, node, stylizer, names, styles, psize, item_id): + def flatten_node(self, node, stylizer, names, styles, pseudo_styles, psize, item_id): if not isinstance(node.tag, basestring) \ or namespace(node.tag) != XHTML_NS: return @@ -357,25 +357,51 @@ class CSSFlattener(object): cssdict.get('text-align', None) not in ('center', 'right')): cssdict['text-indent'] = "%1.1fem" % indent_size - if cssdict: - items = cssdict.items() - items.sort() - css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items) - classes = node.get('class', '').strip() or 'calibre' - klass = STRIPNUM.sub('', classes.split()[0].replace('_', '')) - if css in styles: - match = styles[css] - else: - match = klass + str(names[klass] or '') - styles[css] = match - names[klass] += 1 - node.attrib['class'] = match + pseudo_classes = style.pseudo_classes(self.filter_css) + if cssdict or pseudo_classes: + keep_classes = set() + + if cssdict: + items = cssdict.items() + items.sort() + css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items) + classes = node.get('class', '').strip() or 'calibre' + klass = STRIPNUM.sub('', classes.split()[0].replace('_', '')) + if css in styles: + match = styles[css] + else: + match = klass + str(names[klass] or '') + styles[css] = match + names[klass] += 1 + node.attrib['class'] = match + keep_classes.add(match) + + for psel, cssdict in pseudo_classes.iteritems(): + items = sorted(cssdict.iteritems()) + css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items) + pstyles = pseudo_styles[psel] + if css in pstyles: + match = pstyles[css] + else: + # We have to use a different class for each psel as + # otherwise you can have incorrect styles for a situation + # like: a:hover { color: red } a:link { color: blue } a.x:hover { color: green } + # If the pcalibre class for a:hover and a:link is the same, + # then the class attribute for a.x tags will contain both + # that class and the class for a.x:hover, which is wrong. + klass = 'pcalibre' + match = klass + str(names[klass] or '') + pstyles[css] = match + names[klass] += 1 + keep_classes.add(match) + node.attrib['class'] = ' '.join(keep_classes) + elif 'class' in node.attrib: del node.attrib['class'] if 'style' in node.attrib: del node.attrib['style'] for child in node: - self.flatten_node(child, stylizer, names, styles, psize, item_id) + self.flatten_node(child, stylizer, names, styles, pseudo_styles, psize, item_id) def flatten_head(self, item, href, global_href): html = item.data @@ -446,7 +472,7 @@ class CSSFlattener(object): def flatten_spine(self): names = defaultdict(int) - styles = {} + styles, pseudo_styles = {}, defaultdict(dict) for item in self.oeb.spine: html = item.data stylizer = self.stylizers[item] @@ -454,10 +480,20 @@ class CSSFlattener(object): self.specializer(item, stylizer) body = html.find(XHTML('body')) fsize = self.context.dest.fbase - self.flatten_node(body, stylizer, names, styles, fsize, item.id) + self.flatten_node(body, stylizer, names, styles, pseudo_styles, fsize, item.id) items = [(key, val) for (val, key) in styles.items()] items.sort() + # :hover must come after link and :active must come after :hover + psels = sorted(pseudo_styles.iterkeys(), key=lambda x : + {'hover':1, 'active':2}.get(x, 0)) + for psel in psels: + styles = pseudo_styles[psel] + if not styles: continue + x = sorted(((k+':'+psel, v) for v, k in styles.iteritems())) + items.extend(x) + css = ''.join(".%s {\n%s;\n}\n\n" % (key, val) for key, val in items) + href = self.replace_css(css) global_css = self.collect_global_css() for item in self.oeb.spine: From 9a8b9874a49de15727006d2dc7b77df38b4ea3c2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 11 Sep 2012 12:51:27 +0530 Subject: [PATCH 7/8] ... --- src/calibre/ebooks/oeb/stylizer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index d558f7f49b..37641c91f2 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -267,8 +267,9 @@ class Stylizer(object): rules.sort() self.rules = rules self._styles = {} + pseudo_pat = re.compile(ur':(first-letter|first-line|link|hover|visited|active|focus)', re.I) for _, _, cssdict, text, _ in rules: - fl = re.search(ur':(first-letter|first-line|link|hover|visited|active|focus)', text) + fl = pseudo_pat.search(text) if fl is not None: text = text.replace(fl.group(), '') selector = get_css_selector(text) From 556f9c844faa5e40b5339f72dc9ee3c9ccd9b5f2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 11 Sep 2012 13:33:49 +0530 Subject: [PATCH 8/8] ... --- src/calibre/devices/mtp/driver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 55472d3d44..b43d3db3df 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -47,9 +47,9 @@ class MTP_DEVICE(BASE): from calibre.library.save_to_disk import config self._prefs = p = JSONConfig('mtp_devices') p.defaults['format_map'] = self.FORMATS - p.defaults['send_to'] = ['eBooks/import', - 'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks', - 'eBooks', 'kindle'] + p.defaults['send_to'] = ['Books', 'eBooks/import', 'eBooks', + 'wordplayer/calibretransfer', 'sdcard/ebooks', + 'kindle'] p.defaults['send_template'] = config().parse().send_template p.defaults['blacklist'] = [] p.defaults['history'] = {} @@ -300,7 +300,7 @@ class MTP_DEVICE(BASE): p = path break if p is None: - p = 'eBooks' + p = 'Books' self.location_paths[loc] = p return self.location_paths[on_card]