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'
|
__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
|
||||||
|
@ -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('')
|
||||||
|
|
||||||
|
@ -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'] = [
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user