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' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from collections import defaultdict
from functools import partial from functools import partial
from cssutils.css import CSSRule, CSSStyleDeclaration 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) ans.discard(name)
return ans 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. 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 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 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) report = report or (lambda x:x)
@ -64,6 +88,13 @@ def remove_unused_css(container, report=None, remove_unused_classes=False):
pass pass
sheets = {name:safe_parse(name) for name, mt in container.mime_map.iteritems() if mt in OEB_STYLES} 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} 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} 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 sheets.iteritems()} 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"]'): for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text: if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(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: 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)
@ -130,18 +166,24 @@ def remove_unused_css(container, report=None, remove_unused_classes=False):
[sheet.cssRules.remove(r) for r in unused_rules] [sheet.cssRules.remove(r) for r in unused_rules]
container.dirty(name) container.dirty(name)
if num_of_removed_rules > 0: num_changes = num_of_removed_rules + num_merged + num_of_removed_classes
report(ngettext('Removed %d unused CSS style rule', 'Removed %d unused CSS style rules', if num_changes > 0:
num_of_removed_rules) % num_of_removed_rules) if num_of_removed_rules > 0:
else: report(ngettext('Removed %d unused CSS style rule', 'Removed %d unused CSS style rules',
report(_('No unused CSS style rules found')) num_of_removed_rules) % num_of_removed_rules)
if remove_unused_classes:
if num_of_removed_classes > 0: if num_of_removed_classes > 0:
report(ngettext('Removed %d unused class from the HTML', 'Removed %d unused classes from the HTML', report(ngettext('Removed %d unused class from the HTML', 'Removed %d unused classes from the HTML',
num_of_removed_classes) % num_of_removed_classes) num_of_removed_classes) % num_of_removed_classes)
else: if num_merged > 0:
report(_('No unused class attributes found')) report(ngettext('Merged %d CSS style rule', 'Merged %d CSS style rules',
return num_of_removed_rules + num_of_removed_classes > 0 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=()): def filter_declaration(style, properties=()):
changed = False changed = False

View File

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

View File

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

@ -29,14 +29,25 @@ def customize_remove_unused_css(name, parent, ans):
d.l = l = QVBoxLayout() d.l = l = QVBoxLayout()
d.setLayout(d.l) d.setLayout(d.l)
d.setWindowTitle(_('Remove unused CSS')) d.setWindowTitle(_('Remove unused CSS'))
d.la = la = QLabel(_( def label(text):
'This will remove all CSS rules that do not match any actual content. You' la = QLabel(text)
' can also have it automatically remove any class attributes from the HTML' la.setWordWrap(True), l.addWidget(la), la.setMinimumWidth(450)
' that do not match any CSS rules, by using the check box below:')) l.addWidget(la)
la.setWordWrap(True), 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')) d.c = c = QCheckBox(_('Remove unused &class attributes'))
c.setChecked(tprefs['remove_unused_classes']) c.setChecked(tprefs['remove_unused_classes'])
l.addWidget(c) 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.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)
@ -44,6 +55,7 @@ def customize_remove_unused_css(name, parent, ans):
if d.exec_() != d.Accepted: if d.exec_() != d.Accepted:
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()
def get_customization(action, name, parent): def get_customization(action, name, parent):
ans = CUSTOMIZATION.copy() ans = CUSTOMIZATION.copy()