Edit book: Remove unused CSS: Add an option to merge CSS rules that have identical properties

This commit is contained in:
Kovid Goyal 2020-10-18 08:56:09 +05:30
parent 40e5b7124f
commit 2283b6114e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 51 additions and 16 deletions

View File

@ -80,33 +80,38 @@ def merge_identical_properties(sheet):
properties_map = defaultdict(list) properties_map = defaultdict(list)
def declaration_key(declaration): def declaration_key(declaration):
items = [] return tuple(sorted(
for prop in declaration.getProperties(): ((prop.name, prop.propertyValue.value) for prop in declaration.getProperties()),
val = prop.propertyValue.value key=itemgetter(0)
name = prop.name ))
items.append((name, val))
items.sort(key=itemgetter(0))
return tuple(items)
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE): for idx, rule in enumerate(sheet.cssRules):
properties_map[declaration_key(rule.style)].append(rule) 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(): for rule_group in properties_map.values():
if len(rule_group) < 2: if len(rule_group) < 2:
continue continue
selectors = rule_group[0].selectorList num_merged += len(rule_group)
selectors = rule_group[0][1].selectorList
seen = {s.selectorText for s in selectors} seen = {s.selectorText for s in selectors}
rules = iter(rule_group) rules = iter(rule_group)
next(rules) next(rules)
for rule in rules: for idx, rule in rules:
removals.append(idx)
for s in rule.selectorList: for s in rule.selectorList:
q = s.selectorText q = s.selectorText
if q not in seen: if q not in seen:
seen.add(q) seen.add(q)
selectors.append(s) 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. 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 pass
sheets = {name:safe_parse(name) for name, mt in iteritems(container.mime_map) if mt in OEB_STYLES} 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} 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: if merge_rules:
for name, sheet in iteritems(sheets): for name, sheet in iteritems(sheets):
num = merge_identical_selectors(sheet) num = merge_identical_selectors(sheet)
if num: if num:
container.dirty(name) container.dirty(name)
num_merged += num 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} import_map = {name:get_imported_sheets(name, container, sheets) for name in sheets}
if remove_unused_classes: 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)} 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: if num:
num_merged += num num_merged += num
container.dirty(name) 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: if remove_unused_classes:
used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)}
imports = get_imported_sheets(name, container, sheets, sheet=sheet) 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] [sheet.cssRules.remove(r) for r in unused_rules]
container.dirty(name) 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_changes > 0:
if num_of_removed_rules > 0: if num_of_removed_rules > 0:
report(ngettext('Removed one unused CSS style rule', 'Removed {} unused CSS style rules', 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', report(ngettext('Removed one unused class from the HTML', 'Removed {} unused classes from the HTML',
num_of_removed_classes).format(num_of_removed_classes)) num_of_removed_classes).format(num_of_removed_classes))
if num_merged > 0: 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)) 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: if num_of_removed_rules == 0:
report(_('No unused CSS style rules found')) report(_('No unused CSS style rules found'))
if remove_unused_classes and num_of_removed_classes == 0: if remove_unused_classes and num_of_removed_classes == 0:

View File

@ -43,6 +43,7 @@ ALL_OPTS = {
CUSTOMIZATION = { CUSTOMIZATION = {
'remove_unused_classes': False, 'remove_unused_classes': False,
'merge_identical_selectors': False, 'merge_identical_selectors': False,
'merge_rules_with_identical_properties': False,
} }
SUPPORTED = {'EPUB', 'AZW3'} SUPPORTED = {'EPUB', 'AZW3'}
@ -233,7 +234,11 @@ def polish_one(ebook, opts, report, customization=None):
if opts.remove_unused_css: if opts.remove_unused_css:
rt(_('Removing unused CSS rules')) rt(_('Removing unused CSS rules'))
if remove_unused_css( 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 changed = True
report('') report('')

View File

@ -53,6 +53,8 @@ d['inline_spell_check'] = True
d['custom_themes'] = {} d['custom_themes'] = {}
d['remove_unused_classes'] = False d['remove_unused_classes'] = False
d['merge_identical_selectors'] = False d['merge_identical_selectors'] = False
d['merge_identical_selectors'] = False
d['merge_rules_with_identical_properties'] = False
d['global_book_toolbar'] = [ d['global_book_toolbar'] = [
'new-file', 'open-book', 'save-book', None, 'global-undo', 'global-redo', 'create-checkpoint', None, 'donate', 'user-manual'] 'new-file', 'open-book', 'save-book', None, 'global-undo', 'global-redo', 'create-checkpoint', None, 'donate', 'user-manual']
d['global_tools_toolbar'] = [ d['global_tools_toolbar'] = [

View File

@ -54,6 +54,14 @@ def customize_remove_unused_css(name, parent, ans):
'Merge CSS rules in the same stylesheet that have identical selectors.' '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' ' Note that in rare cases merging can result in a change to the effective styling'
' of the book, so use with care.')) ' 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('<span style="font-size:small; font-style: italic">' + _(
'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.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
d.l.addWidget(d.bb) d.l.addWidget(d.bb)
d.bb.rejected.connect(d.reject) d.bb.rejected.connect(d.reject)
@ -62,6 +70,7 @@ def customize_remove_unused_css(name, parent, ans):
raise Abort() raise Abort()
ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked() ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked()
ans['merge_identical_selectors'] = tprefs['merge_identical_selectors'] = m.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): def get_customization(action, name, parent):