Edit Book: The Remove Unused CSS tool now has an option to also merge CSS rules that have identical selectors

This commit is contained in:
Kovid Goyal 2016-09-07 09:25:41 +05:30
parent 3c05bf2ec2
commit 3318f376d9
4 changed files with 73 additions and 16 deletions

View File

@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from collections import defaultdict
from functools import partial
from cssutils.css import CSSRule, CSSStyleDeclaration
@ -48,12 +49,35 @@ def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None)
ans.discard(name)
return ans
def remove_unused_css(container, report=None, remove_unused_classes=False):
def merge_declarations(first, second):
for prop in second.getProperties():
first.setProperty(prop)
def merge_identical_selectors(sheet):
' Merge rules that have identical selectors '
selector_map = defaultdict(list)
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
selector_map[rule.selectorText].append(rule)
remove = []
for rule_group in selector_map.itervalues():
if len(rule_group) > 1:
for i in range(1, len(rule_group)):
merge_declarations(rule_group[0].style, rule_group[i].style)
remove.append(rule_group[i])
for rule in remove:
sheet.cssRules.remove(rule)
return len(remove)
def remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False):
'''
Remove all unused CSS rules from the book. An unused CSS rule is one that does not match any actual content.
:param report: An optional callable that takes a single argument. It is called with information about the operations being performed.
:param remove_unused_classes: If True, class attributes in the HTML that do not match any CSS rules are also removed.
:param merge_rules: If True, rules with identical selectors are merged.
'''
report = report or (lambda x:x)
@ -64,6 +88,13 @@ def remove_unused_css(container, report=None, remove_unused_classes=False):
pass
sheets = {name:safe_parse(name) for name, mt in container.mime_map.iteritems() if mt in OEB_STYLES}
sheets = {k:v for k, v in sheets.iteritems() if v is not None}
num_merged = 0
if merge_rules:
for name, sheet in sheets.iteritems():
num = merge_identical_selectors(sheet)
if num:
container.dirty(name)
num_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 sheets.iteritems()}
@ -80,6 +111,11 @@ def remove_unused_css(container, report=None, remove_unused_classes=False):
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if merge_rules:
num = merge_identical_selectors(sheet)
if num:
num_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)
@ -130,18 +166,24 @@ def remove_unused_css(container, report=None, remove_unused_classes=False):
[sheet.cssRules.remove(r) for r in unused_rules]
container.dirty(name)
if num_of_removed_rules > 0:
report(ngettext('Removed %d unused CSS style rule', 'Removed %d unused CSS style rules',
num_of_removed_rules) % num_of_removed_rules)
else:
report(_('No unused CSS style rules found'))
if remove_unused_classes:
num_changes = num_of_removed_rules + num_merged + num_of_removed_classes
if num_changes > 0:
if num_of_removed_rules > 0:
report(ngettext('Removed %d unused CSS style rule', 'Removed %d unused CSS style rules',
num_of_removed_rules) % num_of_removed_rules)
if num_of_removed_classes > 0:
report(ngettext('Removed %d unused class from the HTML', 'Removed %d unused classes from the HTML',
num_of_removed_classes) % num_of_removed_classes)
else:
report(_('No unused class attributes found'))
return num_of_removed_rules + num_of_removed_classes > 0
if num_merged > 0:
report(ngettext('Merged %d CSS style rule', 'Merged %d CSS style rules',
num_merged) % num_merged)
if num_of_removed_rules == 0:
report(_('No unused CSS style rules found'))
if remove_unused_classes and num_of_removed_classes == 0:
report(_('No unused class attributes found'))
if merge_rules and num_merged == 0:
report(_('No style rules that could be merged found'))
return num_changes > 0
def filter_declaration(style, properties=()):
changed = False

View File

@ -37,6 +37,7 @@ ALL_OPTS = {
CUSTOMIZATION = {
'remove_unused_classes': False,
'merge_identical_selectors': False,
}
SUPPORTED = {'EPUB', 'AZW3'}
@ -201,7 +202,8 @@ 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']):
if remove_unused_css(
ebook, report, remove_unused_classes=customization['remove_unused_classes'], merge_rules=customization['merge_identical_selectors']):
changed = True
report('')

View File

@ -49,6 +49,7 @@ d['spell_check_case_sensitive_sort'] = False
d['inline_spell_check'] = True
d['custom_themes'] = {}
d['remove_unused_classes'] = False
d['merge_identical_selectors'] = 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'] = [

View File

@ -29,14 +29,25 @@ def customize_remove_unused_css(name, parent, ans):
d.l = l = QVBoxLayout()
d.setLayout(d.l)
d.setWindowTitle(_('Remove unused CSS'))
d.la = la = QLabel(_(
'This will remove all CSS rules that do not match any actual content. You'
' can also have it automatically remove any class attributes from the HTML'
' that do not match any CSS rules, by using the check box below:'))
la.setWordWrap(True), l.addWidget(la)
def label(text):
la = QLabel(text)
la.setWordWrap(True), l.addWidget(la), la.setMinimumWidth(450)
l.addWidget(la)
return la
d.la = label(_(
'This will remove all CSS rules that do not match any actual content.'
' There are a couple of additional cleanups you can enable, below:'))
d.c = c = QCheckBox(_('Remove unused &class attributes'))
c.setChecked(tprefs['remove_unused_classes'])
l.addWidget(c)
d.la2 = label('<span style="font-size:small; font-style: italic">' + _(
'Remove all class attributes from the HTML that do not match any existing CSS rules'))
d.m = m = QCheckBox(_('Merge identical CSS rules'))
m.setChecked(tprefs['merge_identical_selectors'])
l.addWidget(m)
d.la3 = label('<span style="font-size:small; font-style: italic">' + _(
'Merge CSS rules in the same stylesheet that have identical selectors.'))
d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
d.l.addWidget(d.bb)
d.bb.rejected.connect(d.reject)
@ -44,6 +55,7 @@ def customize_remove_unused_css(name, parent, ans):
if d.exec_() != d.Accepted:
raise Abort()
ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked()
ans['merge_identical_selectors'] = tprefs['merge_identical_selectors'] = m.isChecked()
def get_customization(action, name, parent):
ans = CUSTOMIZATION.copy()