mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
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:
parent
3c05bf2ec2
commit
3318f376d9
@ -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
|
||||
|
@ -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('')
|
||||
|
||||
|
@ -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'] = [
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user