diff --git a/src/calibre/ebooks/oeb/polish/css.py b/src/calibre/ebooks/oeb/polish/css.py index e6ae0504bc..81b4cc523a 100644 --- a/src/calibre/ebooks/oeb/polish/css.py +++ b/src/calibre/ebooks/oeb/polish/css.py @@ -80,33 +80,38 @@ def merge_identical_properties(sheet): properties_map = defaultdict(list) def declaration_key(declaration): - items = [] - for prop in declaration.getProperties(): - val = prop.propertyValue.value - name = prop.name - items.append((name, val)) - items.sort(key=itemgetter(0)) - return tuple(items) + return tuple(sorted( + ((prop.name, prop.propertyValue.value) for prop in declaration.getProperties()), + key=itemgetter(0) + )) - for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE): - properties_map[declaration_key(rule.style)].append(rule) + for idx, rule in enumerate(sheet.cssRules): + if rule.type == CSSRule.STYLE_RULE: + properties_map[declaration_key(rule.style)].append((idx, rule)) + removals = [] + num_merged = 0 for rule_group in properties_map.values(): if len(rule_group) < 2: continue - selectors = rule_group[0].selectorList + num_merged += len(rule_group) + selectors = rule_group[0][1].selectorList seen = {s.selectorText for s in selectors} rules = iter(rule_group) next(rules) - for rule in rules: + for idx, rule in rules: + removals.append(idx) for s in rule.selectorList: q = s.selectorText if q not in seen: seen.add(q) selectors.append(s) + for idx in sorted(removals, reverse=True): + sheet.cssRules.pop(idx) + return num_merged -def remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False): +def remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False): ''' Remove all unused CSS rules from the book. An unused CSS rule is one that does not match any actual content. @@ -123,13 +128,19 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge pass sheets = {name:safe_parse(name) for name, mt in iteritems(container.mime_map) if mt in OEB_STYLES} sheets = {k:v for k, v in iteritems(sheets) if v is not None} - num_merged = 0 + num_merged = num_rules_merged = 0 if merge_rules: for name, sheet in iteritems(sheets): num = merge_identical_selectors(sheet) if num: container.dirty(name) num_merged += num + if merge_rules_with_identical_properties: + for name, sheet in iteritems(sheets): + num = merge_identical_properties(sheet) + if num: + container.dirty(name) + num_rules_merged += num import_map = {name:get_imported_sheets(name, container, sheets) for name in sheets} if remove_unused_classes: class_map = {name:{icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} for name, sheet in iteritems(sheets)} @@ -151,6 +162,11 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge if num: num_merged += num container.dirty(name) + if merge_rules_with_identical_properties: + num = merge_identical_properties(sheet) + if num: + num_rules_merged += num + container.dirty(name) if remove_unused_classes: used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} imports = get_imported_sheets(name, container, sheets, sheet=sheet) @@ -201,7 +217,7 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge [sheet.cssRules.remove(r) for r in unused_rules] container.dirty(name) - num_changes = num_of_removed_rules + num_merged + num_of_removed_classes + num_changes = num_of_removed_rules + num_merged + num_of_removed_classes + num_rules_merged if num_changes > 0: if num_of_removed_rules > 0: report(ngettext('Removed one unused CSS style rule', 'Removed {} unused CSS style rules', @@ -210,8 +226,11 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge report(ngettext('Removed one unused class from the HTML', 'Removed {} unused classes from the HTML', num_of_removed_classes).format(num_of_removed_classes)) if num_merged > 0: - report(ngettext('Merged one CSS style rule', 'Merged {} CSS style rules', + report(ngettext('Merged one CSS style rule with identical selectors', 'Merged {} CSS style rules with identical selectors', num_merged).format(num_merged)) + if num_rules_merged > 0: + report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties', + num_rules_merged).format(num_rules_merged)) if num_of_removed_rules == 0: report(_('No unused CSS style rules found')) if remove_unused_classes and num_of_removed_classes == 0: diff --git a/src/calibre/ebooks/oeb/polish/main.py b/src/calibre/ebooks/oeb/polish/main.py index 049b73a857..3d1648db7f 100644 --- a/src/calibre/ebooks/oeb/polish/main.py +++ b/src/calibre/ebooks/oeb/polish/main.py @@ -43,6 +43,7 @@ ALL_OPTS = { CUSTOMIZATION = { 'remove_unused_classes': False, 'merge_identical_selectors': False, + 'merge_rules_with_identical_properties': False, } SUPPORTED = {'EPUB', 'AZW3'} @@ -233,7 +234,11 @@ def polish_one(ebook, opts, report, customization=None): if opts.remove_unused_css: rt(_('Removing unused CSS rules')) if remove_unused_css( - ebook, report, remove_unused_classes=customization['remove_unused_classes'], merge_rules=customization['merge_identical_selectors']): + ebook, report, + remove_unused_classes=customization['remove_unused_classes'], + merge_rules=customization['merge_identical_selectors'], + merge_rules_with_identical_properties=customization['merge_rules_with_identical_properties'] + ): changed = True report('') diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 0a817a2273..f9ebd3217b 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -53,6 +53,8 @@ d['inline_spell_check'] = True d['custom_themes'] = {} d['remove_unused_classes'] = False d['merge_identical_selectors'] = False +d['merge_identical_selectors'] = False +d['merge_rules_with_identical_properties'] = False d['global_book_toolbar'] = [ 'new-file', 'open-book', 'save-book', None, 'global-undo', 'global-redo', 'create-checkpoint', None, 'donate', 'user-manual'] d['global_tools_toolbar'] = [ diff --git a/src/calibre/gui2/tweak_book/polish.py b/src/calibre/gui2/tweak_book/polish.py index 79a84274ae..c9d41b9621 100644 --- a/src/calibre/gui2/tweak_book/polish.py +++ b/src/calibre/gui2/tweak_book/polish.py @@ -54,6 +54,14 @@ def customize_remove_unused_css(name, parent, ans): 'Merge CSS rules in the same stylesheet that have identical selectors.' ' Note that in rare cases merging can result in a change to the effective styling' ' of the book, so use with care.')) + d.p = p = QCheckBox(_('Merge CSS rules with identical properties')) + p.setChecked(tprefs['merge_rules_with_identical_properties']) + l.addWidget(p) + d.la4 = label('' + _( + 'Merge CSS rules in the same stylesheet that have identical properties.' + ' Note that in rare cases merging can result in a change to the effective styling' + ' of the book, so use with care.')) + d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) d.l.addWidget(d.bb) d.bb.rejected.connect(d.reject) @@ -62,6 +70,7 @@ def customize_remove_unused_css(name, parent, ans): raise Abort() ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked() ans['merge_identical_selectors'] = tprefs['merge_identical_selectors'] = m.isChecked() + ans['merge_rules_with_identical_properties'] = tprefs['merge_rules_with_identical_properties'] = p.isChecked() def get_customization(action, name, parent):