Edit book: Remove unused CSS: Fix selectors that dont match from CSS rules containing multiple selectors not being removed. Fixes #1904350 [merged CSS, editor does not remove unsed names](https://bugs.launchpad.net/calibre/+bug/1904350)

This commit is contained in:
Kovid Goyal 2021-01-21 16:58:08 +05:30
parent 4b3ac510bf
commit c116933db8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C

View File

@ -22,21 +22,23 @@ from polyglot.builtins import iteritems, itervalues, unicode_type, filter
from polyglot.functools import lru_cache from polyglot.functools import lru_cache
def filter_used_rules(rules, log, select): def mark_used_selectors(rules, log, select):
any_unused = False
for rule in rules: for rule in rules:
used = False
for selector in rule.selectorList: for selector in rule.selectorList:
if getattr(selector, 'calibre_used', False):
continue
try: try:
if select.has_matches(selector.selectorText): if select.has_matches(selector.selectorText):
used = True selector.calibre_used = True
break else:
any_unused = True
selector.calibre_used = False
except SelectorError: except SelectorError:
# Cannot parse/execute this selector, be safe and assume it # Cannot parse/execute this selector, be safe and assume it
# matches something # matches something
used = True selector.calibre_used = True
break return any_unused
if not used:
yield rule
def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None): def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None):
@ -111,6 +113,25 @@ def merge_identical_properties(sheet):
return num_merged return num_merged
def remove_unused_selectors_and_rules(rules_container, rules, removal_stats):
found_any = False
for r in rules:
removals = []
for i, sel in enumerate(r.selectorList):
if not getattr(sel, 'calibre_used', True):
removals.append(i)
if removals:
found_any = True
if len(removals) == len(r.selectorList):
rules_container.remove(r)
removal_stats['rules'] += 1
else:
removal_stats['selectors'] += len(removals)
for i in reversed(removals):
del r.selectorList[i]
return found_any
def remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=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.
@ -146,7 +167,8 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge
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)}
style_rules = {name:tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) for name, sheet in iteritems(sheets)} style_rules = {name:tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) for name, sheet in iteritems(sheets)}
num_of_removed_rules = num_of_removed_classes = 0 removal_stats = {'rules': 0, 'selectors': 0}
num_of_removed_classes = 0
for name, mt in iteritems(container.mime_map): for name, mt in iteritems(container.mime_map):
if mt not in OEB_DOCS: if mt not in OEB_DOCS:
@ -171,14 +193,12 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge
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)
for imported_sheet in imports: for imported_sheet in imports:
style_rules[imported_sheet] = tuple(filter_used_rules(style_rules[imported_sheet], container.log, select)) mark_used_selectors(style_rules[imported_sheet], container.log, select)
if remove_unused_classes: if remove_unused_classes:
used_classes |= class_map[imported_sheet] used_classes |= class_map[imported_sheet]
rules = tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) rules = tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
unused_rules = tuple(filter_used_rules(rules, container.log, select)) if mark_used_selectors(rules, container.log, select):
if unused_rules: remove_unused_selectors_and_rules(sheet.cssRules, rules, removal_stats)
num_of_removed_rules += len(unused_rules)
[sheet.cssRules.remove(r) for r in unused_rules]
style.text = force_unicode(sheet.cssText, 'utf-8') style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style) pretty_script_or_style(container, style)
container.dirty(name) container.dirty(name)
@ -187,12 +207,12 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge
sname = container.href_to_name(link.get('href'), name) sname = container.href_to_name(link.get('href'), name)
if sname not in sheets: if sname not in sheets:
continue continue
style_rules[sname] = tuple(filter_used_rules(style_rules[sname], container.log, select)) mark_used_selectors(style_rules[sname], container.log, select)
if remove_unused_classes: if remove_unused_classes:
used_classes |= class_map[sname] used_classes |= class_map[sname]
for iname in import_map[sname]: for iname in import_map[sname]:
style_rules[iname] = tuple(filter_used_rules(style_rules[iname], container.log, select)) mark_used_selectors(style_rules[iname], container.log, select)
if remove_unused_classes: if remove_unused_classes:
used_classes |= class_map[iname] used_classes |= class_map[iname]
@ -211,17 +231,18 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge
container.dirty(name) container.dirty(name)
for name, sheet in iteritems(sheets): for name, sheet in iteritems(sheets):
unused_rules = style_rules[name] any_found = remove_unused_selectors_and_rules(sheet.cssRules, style_rules[name], removal_stats)
if unused_rules: if any_found:
num_of_removed_rules += len(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_rules_merged num_changes = num_merged + num_of_removed_classes + num_rules_merged + removal_stats['rules'] + removal_stats['selectors']
if num_changes > 0: if num_changes > 0:
if num_of_removed_rules > 0: if removal_stats['rules']:
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',
num_of_removed_rules).format(num_of_removed_rules)) removal_stats['rules']).format(removal_stats['rules']))
if removal_stats['selectors']:
report(ngettext('Removed one unused CSS selector', 'Removed {} unused CSS selectors',
removal_stats['selectors']).format(removal_stats['selectors']))
if num_of_removed_classes > 0: if num_of_removed_classes > 0:
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))
@ -231,8 +252,10 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge
if num_rules_merged > 0: if num_rules_merged > 0:
report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties', report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties',
num_rules_merged).format(num_rules_merged)) num_rules_merged).format(num_rules_merged))
if num_of_removed_rules == 0: if not removal_stats['rules']:
report(_('No unused CSS style rules found')) report(_('No unused CSS style rules found'))
if not removal_stats['selectors']:
report(_('No unused CSS selectors found'))
if remove_unused_classes and num_of_removed_classes == 0: if remove_unused_classes and num_of_removed_classes == 0:
report(_('No unused class attributes found')) report(_('No unused class attributes found'))
if merge_rules and num_merged == 0: if merge_rules and num_merged == 0: