From e263479ba1202e9ac8227bc5d1691150aaa50462 Mon Sep 17 00:00:00 2001 From: Peter Garst Date: Thu, 24 Apr 2014 20:11:56 -0700 Subject: [PATCH 01/52] Debugging in index facility --- src/calibre/ebooks/docx/index.py | 113 +++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/src/calibre/ebooks/docx/index.py b/src/calibre/ebooks/docx/index.py index 5d19721b43..6cac4f0165 100644 --- a/src/calibre/ebooks/docx/index.py +++ b/src/calibre/ebooks/docx/index.py @@ -119,7 +119,7 @@ def process_index(field, index, xe_fields, log): return hyperlinks, blocks -def split_up_block(block, a, text, parts): +def split_up_block(block, a, text, parts, ldict): prefix = parts[:-1] a.text = parts[-1] parent = a.getparent() @@ -127,31 +127,100 @@ def split_up_block(block, a, text, parts): for i, prefix in enumerate(prefix): m = 1.5 * i span = parent.makeelement('span', style=style % m) + ldict[span] = i parent.append(span) span.text = prefix span = parent.makeelement('span', style=style % ((i + 1) * 1.5)) parent.append(span) span.append(a) + ldict[span] = len(prefix) -def merge_blocks(prev_block, next_block, prev_path, next_path): - pa, na = prev_block.xpath('descendant::a'), next_block.xpath('descendant::a[1]') - if not pa or not na: - return - pa, na = pa[-1], na[0] - if prev_path == next_path: +""" +The merge algorithm is a little tricky. +We start with a list of elementary blocks. Each is an HtmlElement, a p node +with a list of child nodes. The last child is a link, and the earlier ones are +just text. +The list is in reverse order from what we want in the index. +There is a dictionary ldict which records the level of each child node. + +Now we want to do a reduce-like operation, combining all blocks with the same +top level index entry into a single block representing the structure of all +references, subentries, etc. under that top entry. +Here's the algorithm. + +Given a block p and the next block n, and the top level entries p1 and n1 in each +block, which we assume have the same text: + +Start with (p, p1) and (n, n1). + +Given (p, p1, ..., pk) and (n, n1, ..., nk) which we want to merge: + +If there are no more levels in n, then add the link from nk to the links for pk. +This might be the first link for pk, or we might get a list of references. + +Otherwise nk+1 is the next level in n. Look for a matching entry in p. It must have +the same text, it must follow pk, it must come before we find any other p entries at +the same level as pk, and it must have the same level as nk+1. + +If we find such a matching entry, go back to the start with (p ... pk+1) and (n ... nk+1). + +If there is no matching entry, then because of the original reversed order we want +to insert nk+1 and all following entries from n into p immediately following pk. +""" + +def find_match(prev_block, pind, nextent, ldict): + curlevel = ldict[prev_block[pind]] + for p in range(pind+1, len(prev_block)): + trylev = ldict[prev_block[p]] + if trylev <= curlevel: + return -1 + if trylev > (curlevel+1): + continue + if prev_block[p].text_content() == nextent.text_content(): + return p + return -1 + +def add_link(pent, nent, ldict): + na = nent.xpath('descendant::a[1]') + na = na[0] + pa = pent.xpath('descendant::a') + if pa and len(pa) > 0: # Put on same line with a comma + pa = pa[-1] pa.tail = ', ' p = pa.getparent() p.insert(p.index(pa) + 1, na) else: - # Add a line to the previous block - ps, ns = pa.getparent(), na.getparent() - p = ps.getparent() - p.insert(p.index(ps) + 1, ns) + # substitute link na for plain text in pent + pent.text = "" + pent.append(na) + +def merge_blocks(prev_block, next_block, pind, nind, next_path, ldict): + # First elements match. Any more in next? + if len(next_path) == (nind + 1): + nextent = next_block[nind] + add_link(prev_block[pind], nextent, ldict) + return + + nind = nind + 1 + nextent = next_block[nind] + prevent = find_match(prev_block, pind, nextent, ldict) + if prevent > 0: + merge_blocks(prev_block, next_block, prevent, nind, next_path, ldict) + return + + # Want to insert elements into previous block + while nind < len(next_block): + # insert takes it out of old + pind = pind + 1 + prev_block.insert(pind, next_block[nind]) + next_block.getparent().remove(next_block) def polish_index_markup(index, blocks): + # Blocks are in reverse order at this point path_map = {} + ldict = {} for block in blocks: cls = block.get('class', '') or '' block.set('class', (cls + ' index-entry').lstrip()) @@ -162,20 +231,22 @@ def polish_index_markup(index, blocks): if ':' in text: path_map[block] = parts = filter(None, (x.strip() for x in text.split(':'))) if len(parts) > 1: - split_up_block(block, a[0], text, parts) + split_up_block(block, a[0], text, parts, ldict) else: + # try using a span all the time path_map[block] = [text] + parent = a[0].getparent() + span = parent.makeelement('span', style='display:block; margin-left: 0em') + parent.append(span) + span.append(a[0]) + ldict[span] = 0 + # We want a single block for each main entry prev_block = blocks[0] for block in blocks[1:]: pp, pn = path_map[prev_block], path_map[block] - if pp == pn: - merge_blocks(prev_block, block, pp, pn) - elif len(pp) > 1 and len(pn) >= len(pp): - if pn[:-1] in (pp[:-1], pp): - merge_blocks(prev_block, block, pp, pn) - # It's possible to have pn starting with pp but having more - # than one extra entry, but until I see that in the wild, I'm not - # going to bother - prev_block = block + if pp[0] == pn[0]: + merge_blocks(prev_block, block, 0, 0, pn, ldict) + else: + prev_block = block From 47b6df13d0abe045dac7223e7dc447893e1d29e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Apr 2014 12:48:34 +0530 Subject: [PATCH 02/52] Edit book: Fix importing of Lithuanian dictionary from OpenOffice, that does not specify a country code. Fixes #1313315 [Lithuanian spell check dictionary from OpenOffice gives an error](https://bugs.launchpad.net/calibre/+bug/1313315) --- src/calibre/spell/import_from.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/calibre/spell/import_from.py b/src/calibre/spell/import_from.py index 615bac39f4..70e3811030 100644 --- a/src/calibre/spell/import_from.py +++ b/src/calibre/spell/import_from.py @@ -68,6 +68,18 @@ def import_from_libreoffice_source_tree(source_path): if want_locales: raise Exception('Failed to find dictionaries for some wanted locales: %s' % want_locales) +def fill_country_code(x): + return {'lt':'lt_LT'}.get(x, x) + +def uniq(vals, kmap=lambda x:x): + ''' Remove all duplicates from vals, while preserving order. kmap must be a + callable that returns a hashable value for every item in vals ''' + vals = vals or () + lvals = (kmap(x) for x in vals) + seen = set() + seen_add = seen.add + return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) + def import_from_oxt(source_path, name, dest_dir=None, prefix='dic-'): from calibre.spell.dictionary import parse_lang_code dest_dir = dest_dir or os.path.join(config_dir, 'dictionaries') @@ -81,10 +93,10 @@ def import_from_oxt(source_path, name, dest_dir=None, prefix='dic-'): for (dic, aff), locales in parse_xcu(zf.open(xcu).read(), origin='').iteritems(): dic, aff = dic.lstrip('/'), aff.lstrip('/') d = tempfile.mkdtemp(prefix=prefix, dir=dest_dir) - locales = [x for x in locales if parse_lang_code(x).countrycode] + locales = uniq([x for x in map(fill_country_code, locales) if parse_lang_code(x).countrycode]) if not locales: continue - metadata = [name] + locales + metadata = [name] + list(locales) with open(os.path.join(d, 'locales'), 'wb') as f: f.write(('\n'.join(metadata)).encode('utf-8')) with open(os.path.join(d, '%s.dic' % locales[0]), 'wb') as f: From 77a58c851169bc7725178fb85726eecc354c0aa3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Apr 2014 16:34:24 +0530 Subject: [PATCH 03/52] Edit book: Fix saved search dialog causing high CPU usage --- src/calibre/gui2/tweak_book/search.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index e76092770c..d327a56851 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -84,6 +84,11 @@ class WhereBox(QComboBox):
Search only within the marked text in the currently opened file. You can mark text using the Search menu.
''')) self.emphasize = emphasize + self.ofont = QFont(self.font()) + if emphasize: + f = self.emph_font = QFont(self.ofont) + f.setBold(True), f.setItalic(True) + self.setFont(f) @dynamic_property def where(self): @@ -94,16 +99,16 @@ class WhereBox(QComboBox): self.setCurrentIndex({v:k for k, v in wm.iteritems()}[val]) return property(fget=fget, fset=fset) - def paintEvent(self, ev): + def showPopup(self): # We do it like this so that the popup uses a normal font if self.emphasize: - ofont = self.font() - f = QFont(ofont) - f.setBold(True), f.setItalic(True) - self.setFont(f) - QComboBox.paintEvent(self, ev) + self.setFont(self.ofont) + QComboBox.showPopup(self) + + def hidePopup(self): if self.emphasize: - self.setFont(ofont) + self.setFont(self.emph_font) + QComboBox.hidePopup(self) class DirectionBox(QComboBox): From 5736f177351e3f77c115fe7c19d45827264c35ba Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Apr 2014 16:37:23 +0530 Subject: [PATCH 04/52] pep8 --- src/calibre/gui2/tweak_book/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index d327a56851..2ee5aaf69a 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -771,7 +771,7 @@ class SavedSearches(Dialog): def err(): error_dialog(self, _('Invalid data'), _( 'The file %s does not contain valid saved searches') % path, show=True) - if not isinstance(obj, dict) or not 'version' in obj or not 'searches' in obj or obj['version'] not in (1,): + if not isinstance(obj, dict) or 'version' not in obj or 'searches' not in obj or obj['version'] not in (1,): return err() searches = [] for item in obj['searches']: From 220daf93f67fbe75ede02ec3072ec6904ef5da03 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Apr 2014 18:47:10 +0530 Subject: [PATCH 05/52] pep8 --- src/calibre/ebooks/oeb/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 0abc1e2197..6eb6d1b341 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -125,8 +125,8 @@ def iterlinks(root, find_links_in_css=True): if tag == XHTML('object'): codebase = None - ## tags have attributes that are relative to - ## codebase + # tags have attributes that are relative to + # codebase if 'codebase' in attribs: codebase = el.get('codebase') yield (el, 'codebase', codebase, 0) @@ -604,8 +604,8 @@ class Metadata(object): allowed = self.allowed if allowed is not None and term not in allowed: raise AttributeError( - 'attribute %r not valid for metadata term %r' - % (self.attr(term), barename(obj.term))) + 'attribute %r not valid for metadata term %r' % ( + self.attr(term), barename(obj.term))) return self.attr(term) def __get__(self, obj, cls): From 2193370d31e230752b203645d660581759f054f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 07:35:05 +0530 Subject: [PATCH 06/52] News download: Fix very long URLs for links to pages causing errors on windows because of max path length restrictions. Fixes #1313982 [Fetch failed from New York Times](https://bugs.launchpad.net/calibre/+bug/1313982) --- src/calibre/web/fetch/simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index b81e62c17a..7dc236c0d9 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -542,7 +542,7 @@ class RecursiveFetcher(object): _fname.decode('latin1', 'replace') _fname = _fname.encode('ascii', 'replace').replace('%', '').replace(os.sep, '') _fname = ascii_filename(_fname) - _fname = os.path.splitext(_fname)[0]+'.xhtml' + _fname = os.path.splitext(_fname)[0][:120] + '.xhtml' res = os.path.join(linkdiskpath, _fname) self.downloaded_paths.append(res) self.filemap[nurl] = res From 46907f98afe4d3be6fb518bc1807a31a824f38d1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 08:40:30 +0530 Subject: [PATCH 07/52] Make the mem leak test for unrar more robust --- src/calibre/utils/unrar.py | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/calibre/utils/unrar.py b/src/calibre/utils/unrar.py index 2995838079..3aeb9cb8c8 100644 --- a/src/calibre/utils/unrar.py +++ b/src/calibre/utils/unrar.py @@ -17,7 +17,8 @@ class UNRARError(Exception): pass class DevNull: - def write(self, x): pass + def write(self, x): + pass class RARStream(object): @@ -128,7 +129,7 @@ def stream_extract(stream, location): except EOFError: break if not is_useful(h): - f.process_current_item() # Skip these + f.process_current_item() # Skip these if h['is_directory']: try: os.makedirs(safe_path(location, h['filename'])) @@ -184,15 +185,15 @@ def extract_member(stream, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), return h['filename'], et.getvalue() def extract_first_alphabetically(stream): - names_ = [x for x in names(stream) if os.path.splitext(x)[1][1:].lower() in - {'png', 'jpg', 'jpeg', 'gif'}] - names_.sort() + names_ = sorted([x for x in names(stream) if os.path.splitext(x)[1][1:].lower() in + {'png', 'jpg', 'jpeg', 'gif'}]) return extract_member(stream, name=names_[0], match=None) # Test normal RAR file {{{ def test_basic(): - stream = BytesIO(b"Rar!\x1a\x07\x00\xcf\x90s\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x14\xe7z\x00\x80#\x00\x17\x00\x00\x00\r\x00\x00\x00\x03\xc2\xb3\x96o\x00\x00\x00\x00\x1d3\x03\x00\x00\x00\x00\x00CMT\x0c\x00\x8b\xec\x8e\xef\x14\xf6\xe6h\x04\x17\xff\xcd\x0f\xffk9b\x11]^\x80\xd3dt \x90+\x00\x14\x00\x00\x00\x08\x00\x00\x00\x03\xf1\x84\x93\\\xb9]yA\x1d3\t\x00\xa4\x81\x00\x001\\sub-one\x00\xc0\x0c\x00\x8f\xec\x89\xfe.JM\x86\x82\x0c_\xfd\xfd\xd7\x11\x1a\xef@\x9eHt \x80'\x00\x0e\x00\x00\x00\x04\x00\x00\x00\x03\x9f\xa8\x17\xf8\xaf]yA\x1d3\x07\x00\xa4\x81\x00\x00one.txt\x00\x08\xbf\x08\xae\xf3\xca\x87\xfeo\xfe\xd2n\x80-Ht \x82:\x00\x18\x00\x00\x00\x10\x00\x00\x00\x03\xa86\x81\xdf\xf9fyA\x1d3\x1a\x00\xa4\x81\x00\x00\xe8\xaf\xb6\xe6\xaf\x94\xe5\xb1\x81.txt\x00\x8bh\xf6\xd4kA\\.\x00txt\x0c\x00\x8b\xec\x8e\xef\x14\xf6\xe2l\x91\x189\xff\xdf\xfe\xc2\xd3:g\x9a\x19F=cYt \x928\x00\x11\x00\x00\x00\x08\x00\x00\x00\x03\x7f\xd6\xb6\x7f\xeafyA\x1d3\x16\x00\xa4\x81\x00\x00F\xc3\xbc\xc3\x9fe.txt\x00\x01\x00F\xfc\xdfe\x00.txt\x00\xc0 1 and not isosx: - raise ValueError('Leaked %s MB for %d calls'%(used, num)) + for i in xrange(3): + gc.collect() + def get_mem_use(num): + start = memory() + s = SaveStream(stream) + for i in xrange(num): + with s: + f = RARFile(stream) + f.test() + del f, s + for i in xrange(3): + gc.collect() + return memory() - start + (get_mem_use(20)) + a, b = get_mem_use(10), get_mem_use(110) + if not isosx and abs(b - a) > 1: + raise ValueError('Leaked %s MB for %d calls'%(b - a, 100)) # }}} def test_rar(path): From c3812f131ba2d498b3a71d36bca9186946cfaac4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 08:46:16 +0530 Subject: [PATCH 08/52] ICU 53 no longer return the same collation order for numbers whose value is different even if they have the same first digit. --- src/calibre/utils/icu_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/utils/icu_test.py b/src/calibre/utils/icu_test.py index 2b6572c35f..99441520e9 100644 --- a/src/calibre/utils/icu_test.py +++ b/src/calibre/utils/icu_test.py @@ -116,13 +116,12 @@ class TestICU(unittest.TestCase): for group in [ ('Šaa', 'Smith', 'Solženicyn', 'Štepánek'), ('01', '1'), - ('1', '11', '13'), ]: last = None for x in group: order, length = icu.numeric_collator().collation_order(x) if last is not None: - self.ae(last, order) + self.ae(last, order, 'Order for %s not correct: %s != %s' % (x, last, order)) last = order self.ae(dict(icu.partition_by_first_letter(['A1', '', 'a1', '\U0001f431', '\U0001f431x'])), From f8d91b7a09f89c93d256dd401b041b56d9885435 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 10:13:14 +0530 Subject: [PATCH 09/52] Edit book: Fix a hang when editing an HTML or XML file with text of the form ': _('An unescaped > is not allowed. Replace it with >'), - '/': _('/ not allowed except at the end of the tag'), - '?': _('Unknown character'), - 'bad-closing': _('A closing tag must contain only the tag name and nothing else'), - 'no-attr-value': _('Expecting an attribute value'), + '<': _('An unescaped < is not allowed. Replace it with <'), + '&': _('An unescaped ampersand is not allowed. Replace it with &'), + '>': _('An unescaped > is not allowed. Replace it with >'), + '/': _('/ not allowed except at the end of the tag'), + '?': _('Unknown character'), + 'bad-closing': _('A closing tag must contain only the tag name and nothing else'), + 'no-attr-value': _('Expecting an attribute value'), + 'only-prefix': _('A tag name cannot end with a colon'), }.iteritems(): f = formats[name] = SyntaxTextCharFormat(formats['error']) f.setToolTip(msg) @@ -507,7 +513,7 @@ if __name__ == '__main__': - +

A heading that should appear in bold, with an italic word

Some text with inline formatting, that is syntax highlighted. A bold word, and an italic word. \ From 4ef845f5928d8aab3ae97f30fa0c93dc058380bb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 11:14:03 +0530 Subject: [PATCH 10/52] Edit Book: Fix an error when merging CSS stylesheets that contain @charset rules --- src/calibre/ebooks/oeb/polish/split.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/polish/split.py b/src/calibre/ebooks/oeb/polish/split.py index 0c7effabb4..dac0c9b4d7 100644 --- a/src/calibre/ebooks/oeb/polish/split.py +++ b/src/calibre/ebooks/oeb/polish/split.py @@ -419,7 +419,7 @@ def merge_css(container, names, master): # Remove charset rules cr = [r for r in sheet.cssRules if r.type == r.CHARSET_RULE] - [sheet.remove(r) for r in cr] + [sheet.deleteRule(sheet.cssRules.index(r)) for r in cr] for rule in sheet.cssRules: msheet.add(rule) From 4f55ec153ba216a1b477211f26a4c6de1d29dcfc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 14:11:46 +0530 Subject: [PATCH 11/52] Fix #1313567 [Editor save button greyed out after failed save](https://bugs.launchpad.net/calibre/+bug/1313567) --- src/calibre/gui2/tweak_book/boss.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 424a4bd4da..322ab81f6b 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -830,6 +830,7 @@ class Boss(QObject): if self.doing_terminal_save: prints(tb, file=sys.stderr) return + self.gui.action_save.setEnabled(True) error_dialog(self.gui, _('Could not save'), _('Saving of the book failed. Click "Show Details"' ' for more information. You can try to save a copy' From 65de38ddc3c1f49313a5be54f55ba66499df18b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 15:01:45 +0530 Subject: [PATCH 12/52] Fix incorrect color profile --- resources/images/devices/itunes.png | Bin 24190 -> 23206 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/images/devices/itunes.png b/resources/images/devices/itunes.png index d83595d691d96d1c3488b13c5b688bcfca7a4824..29bb0765876e89637b6a6c5003e275ce6ccb0411 100644 GIT binary patch literal 23206 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 From 57225a1ce9b41d7bd31c366f84fbe5bca4ea332d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 17:11:26 +0530 Subject: [PATCH 13/52] ... --- manual/edit.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/edit.rst b/manual/edit.rst index 882bd68d63..a7c606c3a5 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -78,7 +78,7 @@ sum of the individual file sizes. Many files have special meaning, in the book. These will typically have an icon next to their names, indicating the special meaning. For example, in the picture to the left, you can see that the files :guilabel:`cover_image.jpg` -and :guilabel:`titlepage.xhtml` have the ocon of a cover next to them, this +and :guilabel:`titlepage.xhtml` have the icon of a cover next to them, this indicates they are the book cover image and titlepage. Similarly, the :guilabel:`content.opf` file has a metadata icon next to it, indicating the book metadata is present in it and the the :guilabel:`toc.ncx` file has a T From 29f11510e2486f673e4520f3923eac4332e6f2b6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 17:19:01 +0530 Subject: [PATCH 14/52] ... --- manual/edit.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manual/edit.rst b/manual/edit.rst index a7c606c3a5..d4a2269156 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -123,7 +123,9 @@ Changing text file order You can re-arrange the order in which text (HTML) files are opened when reading the book by simply dragging and dropping them in the Files browser. For the -technically inclined, this is called re-ordering the book spine. +technically inclined, this is called re-ordering the book spine. Note that you +have to drop the items *between* other items, not on top of them, this can be a +little fiddly until you get used to it. Marking the cover ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From c7d84b206dabf32ec2a20e03d39bbdaa8a819b64 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 29 Apr 2014 17:42:35 +0530 Subject: [PATCH 15/52] Update Private Eye --- recipes/icons/private_eye.png | Bin 0 -> 2906 bytes recipes/private_eye.recipe | 39 ++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 recipes/icons/private_eye.png diff --git a/recipes/icons/private_eye.png b/recipes/icons/private_eye.png new file mode 100644 index 0000000000000000000000000000000000000000..83aad3d46dec92cf2217c776fbc7af195eb05eab GIT binary patch literal 2906 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', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r'More top stories in the latest issue:.*', re.DOTALL|re.IGNORECASE), lambda match: ''), + (re.compile(r'Also Available Online.*', re.DOTALL|re.IGNORECASE), lambda match: ''), + ] feeds = [(u'Private Eye', u'http://www.private-eye.co.uk/rss/rss.php')] From 003058d9eaaf2a2ecb6f9d6284f81f0143eac9f5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 08:03:28 +0530 Subject: [PATCH 16/52] ... --- src/calibre/gui2/tweak_book/editor/syntax/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/base.py b/src/calibre/gui2/tweak_book/editor/syntax/base.py index 5661781480..f0649a7d58 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/base.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/base.py @@ -41,7 +41,6 @@ class SyntaxHighlighter(QSyntaxHighlighter): return SimpleState(max(0, num)) def rehighlight(self): - self.outlineexplorer_data = {} QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QSyntaxHighlighter.rehighlight(self) QApplication.restoreOverrideCursor() From d6e7df6b6f0697d8ceed2fd74517438ff86bd36e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 08:47:43 +0530 Subject: [PATCH 17/52] Add a copy to clipboard action to the context menu for the spell check dialog --- src/calibre/gui2/tweak_book/spell.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index f25575a04f..23f009e986 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -794,9 +794,19 @@ class WordsView(QTableView): a.setMenu(am) for dic in sorted(dictionaries.active_user_dictionaries, key=lambda x:sort_key(x.name)): am.addAction(dic.name, partial(self.add_all.emit, dic.name)) + m.addSeparator() + m.addAction(_('Copy selected words to clipboard'), self.copy_to_clipboard) m.exec_(ev.globalPos()) + def copy_to_clipboard(self): + rows = {i.row() for i in self.selectedIndexes()} + words = {self.model().word_for_row(r) for r in rows} + words.discard(None) + words = sorted({w[0] for w in words}, key=sort_key) + if words: + QApplication.clipboard().setText('\n'.join(words)) + class SpellCheck(Dialog): work_finished = pyqtSignal(object, object) From 3726f69d54cde840944a2035348473397261a501 Mon Sep 17 00:00:00 2001 From: Niels Giesen Date: Wed, 30 Apr 2014 09:02:58 +0200 Subject: [PATCH 18/52] =?UTF-8?q?Fix=20recipe=20"nrc=E2=80=A2next"=20(new?= =?UTF-8?q?=20URL=20etcetera)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows recent changes in news source "NRC Handelsblad" --- recipes/nrc_next.recipe | 72 +++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/recipes/nrc_next.recipe b/recipes/nrc_next.recipe index bd23a37c65..c630296595 100644 --- a/recipes/nrc_next.recipe +++ b/recipes/nrc_next.recipe @@ -3,15 +3,16 @@ # Based on veezh's original recipe, Kovid Goyal's New York Times recipe and Snaabs nrc Handelsblad recipe __license__ = 'GPL v3' -__copyright__ = '2013, Niels Giesen' +__copyright__ = '2014, Niels Giesen' ''' www.nrc.nl ''' import os, zipfile -import time +from io import BytesIO + from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ptempfile import PersistentTemporaryFile +from datetime import date, timedelta class NRCNext(BasicNewsRecipe): @@ -19,8 +20,8 @@ class NRCNext(BasicNewsRecipe): title = u'nrc•next' description = u'De ePaper-versie van nrc•next' language = 'nl' - lang = 'nl-NL' needs_subscription = True + requires_version = (1, 24, 0) __author__ = 'Niels Giesen' @@ -28,48 +29,33 @@ class NRCNext(BasicNewsRecipe): 'no_default_epub_cover' : True } - def get_browser(self): - br = BasicNewsRecipe.get_browser(self) - if self.username is not None and self.password is not None: - br.open('http://login.nrc.nl/login') - br.select_form(nr=0) - br['username'] = self.username - br['password'] = self.password - br.submit() - return br - def build_index(self): + from calibre.web.jsbrowser.browser import Browser, ElementNotFound + br = Browser() + br.visit('http://login.nrc.nl/login', timeout=60) + f = br.select_form('#command') + f['username'] = self.username + f['password'] = self.password + br.submit() + raw = br.html + if '>log out<' not in raw: + raise ValueError('Failed to login, check username and password') + epubraw = None + for today in (date.today(), date.today() - timedelta(days=1),): + url = 'http://digitaleeditie.nrc.nl/digitaleeditie/NN/%s/3/%s___/downloads.html' % (today.strftime('%Y'), today.strftime('%Y%m%d')) + self.log('Trying to download epub from:', url) + br.start_load(url, timeout=60) + try: + epubraw = br.download_file('#CompleteDownloads .download-list .download-button') + break + except ElementNotFound: + self.log('%r not available yet' % url) + continue - today = time.strftime("%Y%m%d") - - domain = "http://digitaleeditie.nrc.nl" - - url = domain + "/digitaleeditie/helekrant/epub/nn_" + today + ".epub" - #print url - - try: - br = self.get_browser() - f = br.open(url) - except: - self.report_progress(0,_('Kan niet inloggen om editie te downloaden')) + if epubraw is None: raise ValueError('Krant van vandaag nog niet beschikbaar') - tmp = PersistentTemporaryFile(suffix='.epub') - self.report_progress(0,_('downloading epub')) - tmp.write(f.read()) - f.close() - br.close() - if zipfile.is_zipfile(tmp): - try: - zfile = zipfile.ZipFile(tmp.name, 'r') - zfile.extractall(self.output_dir) - self.report_progress(0,_('extracting epub')) - except zipfile.BadZipfile: - self.report_progress(0,_('BadZip error, continuing')) - - tmp.close() + zfile = zipfile.ZipFile(BytesIO(epubraw), 'r') + zfile.extractall(self.output_dir) index = os.path.join(self.output_dir, 'metadata.opf') - - self.report_progress(1,_('epub downloaded and extracted')) - return index From 7e5cdd0330edffbaea8440ab036ee9aee3133695 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 13:14:03 +0530 Subject: [PATCH 19/52] Spellcheck dialog: Pressing Ctrl+C on the words list copies only selected words, regardless of current cell --- src/calibre/gui2/tweak_book/spell.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index 23f009e986..d1b8ff162f 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -16,7 +16,7 @@ from PyQt4.Qt import ( QStackedLayout, QLabel, QVBoxLayout, QWidget, QPushButton, QIcon, QMenu, QDialogButtonBox, QLineEdit, QDialog, QToolButton, QFormLayout, QHBoxLayout, pyqtSignal, QAbstractTableModel, QModelIndex, QTimer, QTableView, QCheckBox, - QComboBox, QListWidget, QListWidgetItem, QInputDialog, QPlainTextEdit) + QComboBox, QListWidget, QListWidgetItem, QInputDialog, QPlainTextEdit, QKeySequence) from calibre.constants import __appname__, plugins from calibre.ebooks.oeb.polish.spell import replace_word, get_all_words, merge_locations @@ -762,6 +762,10 @@ class WordsView(QTableView): self.verticalHeader().close() def keyPressEvent(self, ev): + if ev == QKeySequence.Copy: + self.copy_to_clipboard() + ev.accept() + return ret = QTableView.keyPressEvent(self, ev) if ev.key() in (Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Up, Qt.Key_Down): idx = self.currentIndex() From 7e3164123413fcd0de57a55cd0929979dc4efad1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 13:28:09 +0530 Subject: [PATCH 20/52] Spellcheck dialog: Indicate whether a word is ignored in the Misspelled column --- src/calibre/gui2/tweak_book/spell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index d1b8ff162f..5102366445 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -586,6 +586,11 @@ class WordsModel(QAbstractTableModel): elif role == Qt.InitialSortOrderRole: return Qt.DescendingOrder if section == 1 else Qt.AscendingOrder + def misspelled_text(self, w): + if self.spell_map[w]: + return _('Ignored') if dictionaries.is_word_ignored(*w) else '' + return '✓' + def data(self, index, role=Qt.DisplayRole): try: word, locale = self.items[index.row()] @@ -604,7 +609,7 @@ class WordsModel(QAbstractTableModel): pl = '%s (%s)' % (pl, countrycode) return pl if col == 3: - return '' if self.spell_map[(word, locale)] else '✓' + return self.misspelled_text((word, locale)) if role == Qt.TextAlignmentRole: return Qt.AlignVCenter | (Qt.AlignLeft if index.column() == 0 else Qt.AlignHCenter) @@ -635,7 +640,7 @@ class WordsModel(QAbstractTableModel): locale = w[1] return (calibre_langcode_to_name(locale.langcode), locale.countrycode) else: - key = self.spell_map.get + key = self.misspelled_text return key def do_sort(self): From b381966b79e98a36566b289ce211bdad02614dd9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 14:55:11 +0530 Subject: [PATCH 21/52] Easier debugging of syntax highlighters --- src/calibre/gui2/tweak_book/editor/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tweak_book/editor/__init__.py b/src/calibre/gui2/tweak_book/editor/__init__.py index 270f45f2dc..a16cc7fae6 100644 --- a/src/calibre/gui2/tweak_book/editor/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/__init__.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -from PyQt4.Qt import QTextCharFormat +from PyQt4.Qt import QTextCharFormat, QFont from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.polish.container import guess_type @@ -41,3 +41,9 @@ class SyntaxTextCharFormat(QTextCharFormat): QTextCharFormat.__init__(self, *args) self.setProperty(SYNTAX_PROPERTY, True) + def __repr__(self): + return 'SyntaxFormat(id=%s, color=%s, italic=%s, bold=%s)' % ( + id(self), self.foreground().color().name(), self.fontItalic(), self.fontWeight() >= QFont.DemiBold) + __str__ = __repr__ + + From 16bee93353344d479fd2920e3bd11c482dda1a9f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2014 15:14:08 +0530 Subject: [PATCH 22/52] Edit Book: Redesign the syntax highlighter to improve performance for large documents and extended editing sessions. Fixes #1314339 [edit book app "hangs" during edit session](https://bugs.launchpad.net/calibre/+bug/1314339) --- .../gui2/tweak_book/editor/syntax/base.py | 163 +++++++++++++----- .../gui2/tweak_book/editor/syntax/css.py | 97 +++++++---- .../gui2/tweak_book/editor/syntax/html.py | 155 ++++++----------- src/calibre/gui2/tweak_book/editor/text.py | 6 +- 4 files changed, 239 insertions(+), 182 deletions(-) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/base.py b/src/calibre/gui2/tweak_book/editor/syntax/base.py index f0649a7d58..00fe98efbf 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/base.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/base.py @@ -6,44 +6,53 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -from PyQt4.Qt import (QSyntaxHighlighter, QApplication, QCursor, Qt) +import weakref + +from PyQt4.Qt import ( + QTextCursor, pyqtSlot, QTextBlockUserData, QTextLayout) from ..themes import highlight_to_char_format +from calibre.gui2.tweak_book.widgets import BusyCursor -class SimpleState(object): - - def __init__(self, value): - self.parse = value - - @property - def value(self): - return self.parse - -def run_loop(state, state_map, formats, text): +def run_loop(user_data, state_map, formats, text): + state = user_data.state i = 0 while i < len(text): - fmt = state_map[state.parse](state, text, i, formats) + fmt = state_map[state.parse](state, text, i, formats, user_data) for num, f in fmt: yield i, num, f i += num -class SyntaxHighlighter(QSyntaxHighlighter): +class SimpleState(object): + + __slots__ = ('parse',) + + def __init__(self): + self.parse = 0 + + def copy(self): + s = SimpleState() + s.parse = self.parse + return s + +class SimpleUserData(QTextBlockUserData): + + def __init__(self): + QTextBlockUserData.__init__(self) + self.state = SimpleState() + + def clear(self, state=None): + self.state = SimpleState() if state is None else state + +class SyntaxHighlighter(object): - state_map = {0:lambda state, text, i, formats:[(len(text), None)]} create_formats_func = lambda highlighter: {} spell_attributes = () tag_ok_for_spell = lambda x: False + user_data_factory = SimpleUserData - def __init__(self, *args, **kwargs): - QSyntaxHighlighter.__init__(self, *args, **kwargs) - - def create_state(self, num): - return SimpleState(max(0, num)) - - def rehighlight(self): - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) - QSyntaxHighlighter.rehighlight(self) - QApplication.restoreOverrideCursor() + def __init__(self): + self.document_ref = lambda : None def apply_theme(self, theme): self.theme = {k:highlight_to_char_format(v) for k, v in theme.iteritems()} @@ -53,20 +62,94 @@ class SyntaxHighlighter(QSyntaxHighlighter): def create_formats(self): self.formats = self.create_formats_func() - def highlightBlock(self, text): - try: - state = self.previousBlockState() - self.setCurrentBlockUserData(None) # Ensure that any stale user data is discarded - state = self.create_state(state) - state.get_user_data, state.set_user_data = self.currentBlockUserData, self.setCurrentBlockUserData - for i, num, fmt in run_loop(state, self.state_map, self.formats, unicode(text)): - if fmt is not None: - self.setFormat(i, num, fmt) - self.setCurrentBlockState(state.value) - except: - import traceback - traceback.print_exc() - finally: - # Disabled as it causes crashes - pass # QApplication.processEvents() # Try to keep the editor responsive to user input + def set_document(self, doc): + old_doc = self.document_ref() + if old_doc is not None: + old_doc.contentsChange.disconnect(self.reformat_blocks) + c = QTextCursor(old_doc) + c.beginEditBlock() + blk = old_doc.begin() + while blk.isValid(): + blk.layout().clearAdditionalFormats() + blk = blk.next() + c.endEditBlock() + if doc is not None: + self.document_ref = weakref.ref(doc) + doc.contentsChange.connect(self.reformat_blocks) + self.rehighlight() + else: + self.document_ref = lambda : None + + def rehighlight(self): + doc = self.document_ref() + if doc is None: + return + lb = doc.lastBlock() + with BusyCursor(): + self.reformat_blocks(0, 0, lb.position() + lb.length()) + + def get_user_data(self, block): + ud = block.userData() + new_data = False + if ud is None: + ud = self.user_data_factory() + block.setUserData(ud) + new_data = True + return ud, new_data + + @pyqtSlot(int, int, int) + def reformat_blocks(self, position, removed, added): + doc = self.document_ref() + if doc is None: + return + last_block = doc.findBlock(position + added + (1 if removed > 0 else 0)) + if not last_block.isValid(): + last_block = doc.lastBlock() + end_pos = last_block.position() + last_block.length() + force_next_highlight = False + + doc.contentsChange.disconnect(self.reformat_blocks) + try: + block = doc.findBlock(position) + while block.isValid() and (block.position() < end_pos or force_next_highlight): + ud, new_ud = self.get_user_data(block) + orig_state = ud.state + pblock = block.previous() + if pblock.isValid(): + start_state = pblock.userData() + if start_state is None: + start_state = self.user_data_factory().state + else: + start_state = start_state.state.copy() + else: + start_state = self.user_data_factory().state + ud.clear(state=start_state) # Ensure no stale user data lingers + formats = [] + for i, num, fmt in run_loop(ud, self.state_map, self.formats, unicode(block.text())): + if fmt is not None: + formats.append((i, num, fmt)) + self.apply_format_changes(doc, block, formats) + force_next_highlight = new_ud or ud.state != orig_state + block = block.next() + finally: + doc.contentsChange.connect(self.reformat_blocks) + + def apply_format_changes(self, doc, block, formats): + layout = block.layout() + preedit_start = layout.preeditAreaPosition() + preedit_length = layout.preeditAreaText().length() + ranges = [] + R = QTextLayout.FormatRange + for i, num, fmt in formats: + # Adjust range by pre-edit text, if any + if preedit_start != 0: + if i >= preedit_start: + i += preedit_length + elif i + num >= preedit_start: + num += preedit_length + r = R() + r.start, r.length, r.format = i, num, fmt + ranges.append(r) + layout.setAdditionalFormats(ranges) + doc.markContentsDirty(block.position(), block.length()) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/css.py b/src/calibre/gui2/tweak_book/editor/syntax/css.py index 04c9182b70..7e19790516 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/css.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/css.py @@ -8,6 +8,8 @@ __copyright__ = '2013, Kovid Goyal ' import re +from PyQt4.Qt import QTextBlockUserData + from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter @@ -118,41 +120,63 @@ content_tokens = [(re.compile(k), v, n) for k, v, n in [ ]] -class State(object): +NORMAL = 0 +IN_COMMENT_NORMAL = 1 +IN_SQS = 2 +IN_DQS = 3 +IN_CONTENT = 4 +IN_COMMENT_CONTENT = 5 - NORMAL = 0 - IN_COMMENT_NORMAL = 1 - IN_SQS = 2 - IN_DQS = 3 - IN_CONTENT = 4 - IN_COMMENT_CONTENT = 5 +class CSSState(object): - def __init__(self, num): - self.parse = num & 0b1111 - self.blocks = num >> 4 + __slots__ = ('parse', 'blocks') - @property - def value(self): - return ((self.parse & 0b1111) | (max(0, self.blocks) << 4)) + def __init__(self): + self.parse = NORMAL + self.blocks = 0 + def copy(self): + s = CSSState() + s.parse, s.blocks = self.parse, self.blocks + return s -def normal(state, text, i, formats): + def __eq__(self, other): + return self.parse == getattr(other, 'parse', -1) and \ + self.blocks == getattr(other, 'blocks', -1) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "CSSState(parse=%s, blocks=%s)" % (self.parse, self.blocks) + __str__ = __repr__ + +class CSSUserData(QTextBlockUserData): + + def __init__(self): + QTextBlockUserData.__init__(self) + self.state = CSSState() + + def clear(self, state=None): + self.state = CSSState() if state is None else state + +def normal(state, text, i, formats, user_data): ' The normal state (outside content blocks {})' m = space_pat.match(text, i) if m is not None: return [(len(m.group()), None)] cdo = cdo_pat.match(text, i) if cdo is not None: - state.parse = State.IN_COMMENT_NORMAL + state.parse = IN_COMMENT_NORMAL return [(len(cdo.group()), formats['comment'])] if text[i] == '"': - state.parse = State.IN_DQS + state.parse = IN_DQS return [(1, formats['string'])] if text[i] == "'": - state.parse = State.IN_SQS + state.parse = IN_SQS return [(1, formats['string'])] if text[i] == '{': - state.parse = State.IN_CONTENT + state.parse = IN_CONTENT state.blocks += 1 return [(1, formats['bracket'])] for token, fmt, name in sheet_tokens: @@ -162,24 +186,24 @@ def normal(state, text, i, formats): return [(len(text) - i, formats['unknown-normal'])] -def content(state, text, i, formats): +def content(state, text, i, formats, user_data): ' Inside content blocks ' m = space_pat.match(text, i) if m is not None: return [(len(m.group()), None)] cdo = cdo_pat.match(text, i) if cdo is not None: - state.parse = State.IN_COMMENT_CONTENT + state.parse = IN_COMMENT_CONTENT return [(len(cdo.group()), formats['comment'])] if text[i] == '"': - state.parse = State.IN_DQS + state.parse = IN_DQS return [(1, formats['string'])] if text[i] == "'": - state.parse = State.IN_SQS + state.parse = IN_SQS return [(1, formats['string'])] if text[i] == '}': state.blocks -= 1 - state.parse = State.NORMAL if state.blocks < 1 else State.IN_CONTENT + state.parse = NORMAL if state.blocks < 1 else IN_CONTENT return [(1, formats['bracket'])] if text[i] == '{': state.blocks += 1 @@ -191,34 +215,34 @@ def content(state, text, i, formats): return [(len(text) - i, formats['unknown-normal'])] -def comment(state, text, i, formats): +def comment(state, text, i, formats, user_data): ' Inside a comment ' pos = text.find('*/', i) if pos == -1: return [(len(text), formats['comment'])] - state.parse = State.NORMAL if state.parse == State.IN_COMMENT_NORMAL else State.IN_CONTENT + state.parse = NORMAL if state.parse == IN_COMMENT_NORMAL else IN_CONTENT return [(pos - i + 2, formats['comment'])] -def in_string(state, text, i, formats): +def in_string(state, text, i, formats, user_data): 'Inside a string' - q = '"' if state.parse == State.IN_DQS else "'" + q = '"' if state.parse == IN_DQS else "'" pos = text.find(q, i) if pos == -1: if text[-1] == '\\': # Multi-line string return [(len(text) - i, formats['string'])] - state.parse = (State.NORMAL if state.blocks < 1 else State.IN_CONTENT) + state.parse = (NORMAL if state.blocks < 1 else IN_CONTENT) return [(len(text) - i, formats['unterminated-string'])] - state.parse = (State.NORMAL if state.blocks < 1 else State.IN_CONTENT) + state.parse = (NORMAL if state.blocks < 1 else IN_CONTENT) return [(pos - i + len(q), formats['string'])] state_map = { - State.NORMAL:normal, - State.IN_COMMENT_NORMAL: comment, - State.IN_COMMENT_CONTENT: comment, - State.IN_SQS: in_string, - State.IN_DQS: in_string, - State.IN_CONTENT: content, + NORMAL:normal, + IN_COMMENT_NORMAL: comment, + IN_COMMENT_CONTENT: comment, + IN_SQS: in_string, + IN_DQS: in_string, + IN_CONTENT: content, } def create_formats(highlighter): @@ -252,9 +276,8 @@ class CSSHighlighter(SyntaxHighlighter): state_map = state_map create_formats_func = create_formats + user_data_factory = CSSUserData - def create_state(self, num): - return State(max(0, num)) if __name__ == '__main__': from calibre.gui2.tweak_book.editor.widget import launch_editor diff --git a/src/calibre/gui2/tweak_book/editor/syntax/html.py b/src/calibre/gui2/tweak_book/editor/syntax/html.py index 9583090132..2419e59721 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/html.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/html.py @@ -15,7 +15,8 @@ from PyQt4.Qt import QFont, QTextBlockUserData from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags from calibre.gui2.tweak_book.editor import SyntaxTextCharFormat from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop -from calibre.gui2.tweak_book.editor.syntax.css import create_formats as create_css_formats, state_map as css_state_map, State as CSSState +from calibre.gui2.tweak_book.editor.syntax.css import ( + create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData) from html5lib.constants import cdataElements, rcdataElements @@ -51,41 +52,33 @@ Attr = namedtuple('Attr', 'offset type data') class Tag(object): - __slots__ = ('name', 'bold', 'italic', 'lang', 'hash') + __slots__ = ('name', 'bold', 'italic', 'lang') def __init__(self, name, bold=None, italic=None): self.name = name self.bold = name in bold_tags if bold is None else bold self.italic = name in italic_tags if italic is None else italic self.lang = None - self.hash = 0 - - def __hash__(self): - return self.hash def __eq__(self, other): return self.name == getattr(other, 'name', None) and self.lang == getattr(other, 'lang', False) def copy(self): ans = Tag(self.name, self.bold, self.italic) - ans.lang, ans.hash = self.lang, self.hash + ans.lang = self.lang return ans - def update_hash(self): - self.hash = hash((self.name, self.lang)) - class State(object): - __slots__ = ('tag_being_defined', 'tags', 'is_bold', 'is_italic', - 'current_lang', 'parse', 'get_user_data', 'set_user_data', - 'css_formats', 'stack', 'sub_parser_state', 'default_lang', - 'attribute_name',) + __slots__ = ( + 'tag_being_defined', 'tags', 'is_bold', 'is_italic', 'current_lang', + 'parse', 'css_formats', 'sub_parser_state', 'default_lang', 'attribute_name',) def __init__(self): self.tags = [] self.is_bold = self.is_italic = False - self.tag_being_defined = self.current_lang = self.get_user_data = self.set_user_data = \ - self.css_formats = self.stack = self.sub_parser_state = self.default_lang = self.attribute_name = None + self.tag_being_defined = self.current_lang = self.css_formats = \ + self.sub_parser_state = self.default_lang = self.attribute_name = None self.parse = NORMAL def copy(self): @@ -95,17 +88,10 @@ class State(object): self.tags = [x.copy() for x in self.tags] if self.tag_being_defined is not None: self.tag_being_defined = self.tag_being_defined.copy() + if self.sub_parser_state is not None: + ans.sub_parser_state = self.sub_parser_state.copy() return ans - @property - def value(self): - if self.tag_being_defined is not None: - self.tag_being_defined.update_hash() - return self.stack.index_for(self) - - def __hash__(self): - return hash((self.parse, self.sub_parser_state, self.tag_being_defined, self.attribute_name, tuple(self.tags))) - def __eq__(self, other): return ( self.parse == getattr(other, 'parse', -1) and @@ -115,6 +101,9 @@ class State(object): self.tags == getattr(other, 'tags', None) ) + def __ne__(self, other): + return not self.__eq__(other) + def open_tag(self, name): self.tag_being_defined = Tag(name) @@ -128,7 +117,7 @@ class State(object): return # No matching open tag found, ignore the closing tag # Remove all tags upto the matching open tag self.tags = self.tags[:-len(removed_tags)] - self.sub_parser_state = 0 + self.sub_parser_state = None # Check if we should still be bold or italic if self.is_bold: self.is_bold = False @@ -154,71 +143,41 @@ class State(object): if self.tag_being_defined is None: return t, self.tag_being_defined = self.tag_being_defined, None - t.update_hash() self.tags.append(t) self.is_bold = self.is_bold or t.bold self.is_italic = self.is_italic or t.italic self.current_lang = t.lang or self.current_lang if t.name in cdata_tags: self.parse = CSS if t.name == 'style' else CDATA - self.sub_parser_state = 0 + self.sub_parser_state = None def __repr__(self): return '' % ( '->'.join(x.name for x in self.tags), self.is_bold, self.is_italic, self.current_lang) __str__ = __repr__ -class Stack(object): - - ''' Maintain an efficient bi-directional mapping between states and index - numbers. Ensures that if state1 == state2 then their corresponding index - numbers are the same and vice versa. This is need so that the state number - passed to Qt does not change unless the underlying state has actually - changed. ''' - - def __init__(self): - self.index_map = [] - self.state_map = {} - - def index_for(self, state): - ans = self.state_map.get(state, None) - if ans is None: - self.state_map[state] = ans = len(self.index_map) - self.index_map.append(state) - return ans - - def state_for(self, index): - try: - return self.index_map[index] - except IndexError: - return None - class HTMLUserData(QTextBlockUserData): def __init__(self): QTextBlockUserData.__init__(self) self.tags = [] self.attributes = [] + self.state = State() + self.css_user_data = None -def add_tag_data(state, tag): - ud = q = state.get_user_data() - if ud is None: - ud = HTMLUserData() - ud.tags.append(tag) - if q is None: - state.set_user_data(ud) + def clear(self, state=None): + self.tags, self.attributes = [], [] + self.state = State() if state is None else state + +def add_tag_data(user_data, tag): + user_data.tags.append(tag) ATTR_NAME, ATTR_VALUE, ATTR_START, ATTR_END = object(), object(), object(), object() -def add_attr_data(state, data_type, data, offset): - ud = q = state.get_user_data() - if ud is None: - ud = HTMLUserData() - ud.attributes.append(Attr(offset, data_type, data)) - if q is None: - state.set_user_data(ud) +def add_attr_data(user_data, data_type, data, offset): + user_data.attributes.append(Attr(offset, data_type, data)) -def css(state, text, i, formats): +def css(state, text, i, formats, user_data): ' Inside a