Edit Book: A new tool to filter style information (Tools->Filter style

information). Useful to easily remove some CSS properties from the
entire book.
This commit is contained in:
Kovid Goyal 2014-05-07 14:34:11 +05:30
parent 418cb87f13
commit 1767c90680
5 changed files with 170 additions and 4 deletions

View File

@ -400,6 +400,17 @@ If you use this tool multiple times, each invocation will cause the previously
created inline Table of Contents to be replaced. The tool can be accessed via created inline Table of Contents to be replaced. The tool can be accessed via
:guilabel:`Tools->Table of Contents->Insert inline Table of Contents`. :guilabel:`Tools->Table of Contents->Insert inline Table of Contents`.
Filter style information
^^^^^^^^^^^^^^^^^^^^^^^^^^^
This tool can be used to easily remove specified CSS style properties from the
entire book. You can tell it what properties you want removed, for example,
``color, background-color, line-height`` and it will remove them from
everywhere they occur --- stylesheets, ``<style>`` tags and inline ``style``
attributes. After removing the style information, a summary of all the changes
made is displayed so you can see exactly what was changed. The tool can be
accessed via :guilabel:`Tools->Filter style information`.
.. _checkpoints: .. _checkpoints:
Checkpoints Checkpoints

View File

@ -14,6 +14,7 @@ from cssselect.xpath import XPathExpr, is_safe_name
from calibre import force_unicode from calibre import force_unicode
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS, XPNSMAP, XHTML_NS from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS, XPNSMAP, XHTML_NS
from calibre.ebooks.oeb.normalize_css import normalize_filter_css, normalizers
from calibre.ebooks.oeb.stylizer import MIN_SPACE_RE, is_non_whitespace, xpath_lower_case, fix_namespace from calibre.ebooks.oeb.stylizer import MIN_SPACE_RE, is_non_whitespace, xpath_lower_case, fix_namespace
from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style
@ -167,3 +168,80 @@ def remove_unused_css(container, report):
else: else:
report(_('No unused CSS style rules found')) report(_('No unused CSS style rules found'))
return num_of_removed_rules > 0 return num_of_removed_rules > 0
def filter_declaration(style, properties):
changed = False
for prop in properties:
if style.removeProperty(prop) != '':
changed = True
all_props = set(style.keys())
for prop in style.getProperties():
n = normalizers.get(prop.name, None)
if n is not None:
normalized = n(prop.name, prop.propertyValue)
removed = properties.intersection(set(normalized))
if removed:
changed = True
style.removeProperty(prop.name)
for prop in set(normalized) - removed - all_props:
style.setProperty(prop, normalized[prop])
return changed
def filter_sheet(sheet, properties):
from cssutils.css import CSSRule
changed = False
remove = []
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
if filter_declaration(rule.style, properties):
changed = True
if rule.style.length == 0:
remove.append(rule)
for rule in remove:
sheet.cssRules.remove(rule)
return changed
def filter_css(container, properties, names=()):
if not names:
types = OEB_STYLES | OEB_DOCS
names = []
for name, mt in container.mime_map.iteritems():
if mt in types:
names.append(name)
properties = normalize_filter_css(properties)
doc_changed = False
for name in names:
mt = container.mime_map[name]
if mt in OEB_STYLES:
sheet = container.parsed(name)
filtered = filter_sheet(sheet, properties)
if filtered:
container.dirty(name)
doc_changed = True
elif mt in OEB_DOCS:
root = container.parsed(name)
changed = False
for style in root.xpath('//*[local-name()="style"]'):
if style.text and style.get('type', 'text/css') in {None, '', 'text/css'}:
sheet = container.parse_css(style.text)
if filter_sheet(sheet, properties):
changed = True
style.text = force_unicode(sheet.cssText, 'utf-8')
pretty_script_or_style(container, style)
for elem in root.xpath('//*[@style]'):
text = elem.get('style', None)
if text:
style = container.parse_css(text, is_declaration=True)
if filter_declaration(style, properties):
changed = True
if style.length == 0:
del elem.attrib['style']
else:
elem.set('style', force_unicode(style.getCssText(separator=' '), 'utf-8'))
if changed:
container.dirty(name)
doc_changed = True
return doc_changed

View File

@ -20,6 +20,7 @@ from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish
from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type, OEB_FONTS from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type, OEB_FONTS
from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage
from calibre.ebooks.oeb.polish.css import filter_css
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all
from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders
from calibre.ebooks.oeb.polish.split import split, merge, AbortError, multisplit from calibre.ebooks.oeb.polish.split import split, merge, AbortError, multisplit
@ -41,7 +42,7 @@ from calibre.gui2.tweak_book.search import validate_search_request, run_search
from calibre.gui2.tweak_book.spell import find_next as find_next_word from calibre.gui2.tweak_book.spell import find_next as find_next_word
from calibre.gui2.tweak_book.widgets import ( from calibre.gui2.tweak_book.widgets import (
RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink, RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink,
InsertSemantics, BusyCursor, InsertTag) InsertSemantics, BusyCursor, InsertTag, FilterCSS)
_diff_dialogs = [] _diff_dialogs = []
@ -674,6 +675,21 @@ class Boss(QObject):
d.apply_changes(current_container()) d.apply_changes(current_container())
self.apply_container_update_to_gui() self.apply_container_update_to_gui()
def filter_css(self):
self.commit_all_editors_to_container()
d = FilterCSS(parent=self.gui)
if d.exec_() == d.Accepted and d.filtered_properties:
self.add_savepoint(_('Before: Filter style information'))
with BusyCursor():
changed = filter_css(current_container(), d.filtered_properties)
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No matching style rules were found'), show=True)
def show_find(self): def show_find(self):
self.gui.central.show_find() self.gui.central.show_find()
ed = self.gui.central.current_editor ed = self.gui.central.current_editor

View File

@ -349,6 +349,8 @@ class Main(MainWindow):
_('Arrange into folders')) _('Arrange into folders'))
self.action_set_semantics = reg('tags.png', _('Set &Semantics'), self.boss.set_semantics, 'set-semantics', (), self.action_set_semantics = reg('tags.png', _('Set &Semantics'), self.boss.set_semantics, 'set-semantics', (),
_('Set Semantics')) _('Set Semantics'))
self.action_filter_css = reg('filter.png', _('&Filter style information'), self.boss.filter_css, 'filter-css', (),
_('Filter style information'))
# Polish actions # Polish actions
group = _('Polish Book') group = _('Polish Book')
@ -482,6 +484,7 @@ class Main(MainWindow):
e.addAction(self.action_pretty_all) e.addAction(self.action_pretty_all)
e.addAction(self.action_rationalize_folders) e.addAction(self.action_rationalize_folders)
e.addAction(self.action_set_semantics) e.addAction(self.action_set_semantics)
e.addAction(self.action_filter_css)
e.addAction(self.action_spell_check_book) e.addAction(self.action_spell_check_book)
e.addAction(self.action_check_book) e.addAction(self.action_check_book)

View File

@ -15,7 +15,7 @@ from PyQt4.Qt import (
QFormLayout, QHBoxLayout, QToolButton, QIcon, QApplication, Qt, QWidget, QFormLayout, QHBoxLayout, QToolButton, QIcon, QApplication, Qt, QWidget,
QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption, QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption,
QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle, QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle,
QListView, QTextDocument, QSize, QComboBox, QFrame, QCursor) QListView, QTextDocument, QSize, QComboBox, QFrame, QCursor, QCheckBox)
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.ebooks.oeb.polish.utils import lead_text from calibre.ebooks.oeb.polish.utils import lead_text
@ -689,7 +689,7 @@ class InsertLink(Dialog):
frag = item.get('id', None) or item.get('name') frag = item.get('id', None) or item.get('name')
text = lead_text(item, num_words=4) text = lead_text(item, num_words=4)
ac.append((text, frag)) ac.append((text, frag))
ac.sort(key=lambda (text, frag): primary_sort_key(text)) ac.sort(key=lambda text_frag: primary_sort_key(text_frag[0]))
self.anchor_names.model().set_names(self.anchor_cache[name]) self.anchor_names.model().set_names(self.anchor_cache[name])
self.update_target() self.update_target()
@ -924,6 +924,64 @@ class InsertSemantics(Dialog):
# }}} # }}}
class FilterCSS(Dialog): # {{{
def __init__(self, parent=None):
Dialog.__init__(self, _('Filter Style Information'), 'filter-css', parent=parent)
def setup_ui(self):
from calibre.gui2.convert.look_and_feel_ui import Ui_Form
f, w = Ui_Form(), QWidget()
f.setupUi(w)
self.l = l = QFormLayout(self)
self.setLayout(l)
l.addRow(QLabel(_('Select what style information you want completely removed:')))
self.h = h = QHBoxLayout()
for name, text in {
'fonts':_('&Fonts'), 'margins':_('&Margins'), 'padding':_('&Padding'), 'floats':_('Flo&ats'), 'colors':_('&Colors')}.iteritems():
c = QCheckBox(text)
setattr(self, 'opt_' + name, c)
h.addWidget(c)
c.setToolTip(getattr(f, 'filter_css_' + name).toolTip())
l.addRow(h)
self.others = o = QLineEdit(self)
l.addRow(_('&Other CSS properties:'), o)
o.setToolTip(f.filter_css_others.toolTip())
l.addRow(self.bb)
@property
def filtered_properties(self):
ans = set()
a = ans.add
if self.opt_fonts.isChecked():
a('font-family')
if self.opt_margins.isChecked():
a('margin')
if self.opt_padding.isChecked():
a('padding')
if self.opt_floats.isChecked():
a('float'), a('clear')
if self.opt_colors.isChecked():
a('color'), a('background-color')
for x in unicode(self.others.text()).split(','):
x = x.strip()
if x:
a(x)
return ans
@classmethod
def test(cls):
d = cls()
if d.exec_() == d.Accepted:
print (d.filtered_properties)
# }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
InsertLink.test() FilterCSS.test()