diff --git a/resources/stylelint.js b/resources/stylelint.js
index f53d110357..9d7c1710f7 100644
--- a/resources/stylelint.js
+++ b/resources/stylelint.js
@@ -10,9 +10,10 @@
window.stylelint_results = [];
-window.check_css = function(src) {
+window.check_css = function(src, fix) {
stylelint.lint({
code: src,
+ fix: fix,
config: {
rules: {
'annotation-no-unknown': true,
diff --git a/src/calibre/ebooks/oeb/polish/check/css.py b/src/calibre/ebooks/oeb/polish/check/css.py
index b97400cf3a..7aa3aac121 100644
--- a/src/calibre/ebooks/oeb/polish/check/css.py
+++ b/src/calibre/ebooks/oeb/polish/check/css.py
@@ -22,14 +22,17 @@ from calibre.utils.webengine import secure_webengine, setup_profile
class CSSParseError(BaseError):
level = ERROR
is_parsing_error = True
+ FIXABLE_CSS_ERROR = False
class CSSError(BaseError):
level = ERROR
+ FIXABLE_CSS_ERROR = False
class CSSWarning(BaseError):
level = WARN
+ FIXABLE_CSS_ERROR = False
def as_int_or_none(x):
@@ -57,10 +60,15 @@ def message_to_error(message, name, line_offset, rule_metadata):
line += line_offset
ans = cls(title, name, line, col)
ans.HELP = message.get('text') or ''
+ if ans.HELP:
+ ans.HELP += '. '
ans.css_rule_id = rule_id
- m = rule_metadata.get(rule_id)
- if m and 'url' in m:
- ans.HELP += '. ' + _('See detailed description.').format(m['url'])
+ m = rule_metadata.get(rule_id) or {}
+ if 'url' in m:
+ ans.HELP += _('See detailed description.').format(m['url']) + ' '
+ if m.get('fixable'):
+ ans.FIXABLE_CSS_ERROR = True
+ ans.HELP += _('This error will be automatically fixed if you click "Try to correct all fixable errors" below.')
return ans
@@ -107,7 +115,7 @@ class Worker(QWebEnginePage):
if new_title == 'ready':
self.ready = True
if self.pending is not None:
- self.check_css(self.pending)
+ self.check_css(*self.pending)
self.pending = None
elif new_title == 'checked':
self.runJavaScript('window.get_css_results()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.check_done)
@@ -119,17 +127,17 @@ class Worker(QWebEnginePage):
except Exception:
pass
- def check_css(self, src):
+ def check_css(self, src, fix=False):
self.working = True
self.runJavaScript(
- f'window.check_css({json.dumps(src)})', QWebEngineScript.ScriptWorldId.ApplicationWorld)
+ f'window.check_css({json.dumps(src)}, {"true" if fix else "false"})', QWebEngineScript.ScriptWorldId.ApplicationWorld)
- def check_css_when_ready(self, src):
+ def check_css_when_ready(self, src, fix=False):
if self.ready:
- self.check_css(src)
+ self.check_css(src, fix)
else:
self.working = True
- self.pending = src
+ self.pending = src, fix
def check_done(self, results):
self.working = False
@@ -148,7 +156,8 @@ class Pool:
w.work_done.connect(self.work_done)
self.workers.append(w)
- def check_css(self, css_sources):
+ def check_css(self, css_sources, fix=False):
+ self.doing_fix = fix
self.pending = list(enumerate(css_sources))
self.results = list(repeat(None, len(css_sources)))
self.working = True
@@ -166,7 +175,7 @@ class Pool:
if not w.working:
idx, src = self.pending.pop()
w.result_idx = idx
- w.check_css_when_ready(src)
+ w.check_css_when_ready(src, self.doing_fix)
break
else:
break
@@ -191,14 +200,14 @@ class Pool:
pool = Pool()
shutdown = pool.shutdown
atexit.register(shutdown)
-Job = namedtuple('Job', 'name css line_offset')
+Job = namedtuple('Job', 'name css line_offset fix_data')
-def create_job(name, css, line_offset=0, is_declaration=False):
+def create_job(name, css, line_offset=0, is_declaration=False, fix_data=None):
if is_declaration:
css = 'div{\n' + css + '\n}'
line_offset -= 1
- return Job(name, css, line_offset)
+ return Job(name, css, line_offset, fix_data)
def check_css(jobs):
diff --git a/src/calibre/ebooks/oeb/polish/check/main.py b/src/calibre/ebooks/oeb/polish/check/main.py
index 1acc5bb9f4..fe6e5b1e28 100644
--- a/src/calibre/ebooks/oeb/polish/check/main.py
+++ b/src/calibre/ebooks/oeb/polish/check/main.py
@@ -4,21 +4,23 @@
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal '
-from polyglot.builtins import iteritems
+from collections import namedtuple
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
-from calibre.ebooks.oeb.polish.utils import guess_type
-from calibre.ebooks.oeb.polish.cover import is_raster_image
-from calibre.ebooks.oeb.polish.check.base import run_checkers, WARN
-from calibre.ebooks.oeb.polish.check.parsing import (
- check_filenames, check_xml_parsing, fix_style_tag,
- check_html_size, check_ids, check_markup, EmptyFile, check_encoding_declarations)
-from calibre.ebooks.oeb.polish.check.images import check_raster_images
-from calibre.ebooks.oeb.polish.check.links import check_links, check_mimetypes, check_link_destinations
+from calibre.ebooks.oeb.polish.check.base import WARN, run_checkers
from calibre.ebooks.oeb.polish.check.fonts import check_fonts
+from calibre.ebooks.oeb.polish.check.images import check_raster_images
+from calibre.ebooks.oeb.polish.check.links import (
+ check_link_destinations, check_links, check_mimetypes,
+)
from calibre.ebooks.oeb.polish.check.opf import check_opf
-from polyglot.builtins import as_unicode
-
+from calibre.ebooks.oeb.polish.check.parsing import (
+ EmptyFile, check_encoding_declarations, check_filenames, check_html_size, check_ids,
+ check_markup, check_xml_parsing, fix_style_tag,
+)
+from calibre.ebooks.oeb.polish.cover import is_raster_image
+from calibre.ebooks.oeb.polish.utils import guess_type
+from polyglot.builtins import as_unicode, iteritems
XML_TYPES = frozenset(map(guess_type, ('a.xml', 'a.svg', 'a.opf', 'a.ncx'))) | {'application/oebps-page-map+xml'}
@@ -105,6 +107,48 @@ def run_checks(container):
return errors
+CSSFix = namedtuple('CSSFix', 'original_css elem attribute')
+
+
+def fix_css(container):
+ from calibre.ebooks.oeb.polish.check.css import create_job, pool
+ jobs = []
+
+ for name, mt in iteritems(container.mime_map):
+ if mt in OEB_STYLES:
+ css = container.raw_data(name, decode=True)
+ jobs.append(create_job(name, css, fix_data=CSSFix(css, None, '')))
+ elif mt in OEB_DOCS:
+ root = container.parsed(name)
+ for style in root.xpath('//*[local-name()="style"]'):
+ if style.get('type', 'text/css') == 'text/css' and style.text:
+ jobs.append(create_job(name, style.text, fix_data=CSSFix(style.text, style, '')))
+ for elem in root.xpath('//*[@style]'):
+ raw = elem.get('style')
+ if raw:
+ jobs.append(create_job(name, raw, is_declaration=True, fix_data=CSSFix(raw, elem, 'style')))
+ results = pool.check_css([j.css for j in jobs], fix=True)
+ changed = False
+ for job, result in zip(jobs, results):
+ if result['type'] == 'error':
+ continue
+ fx = job.fix_data
+ fixed_css = result['results']['output']
+ if fixed_css == fx.original_css:
+ continue
+ changed = True
+ if fx.elem is None:
+ with container.open(job.name, 'wb') as f:
+ f.write(fixed_css.encode('utf-8'))
+ else:
+ if fx.attribute:
+ fx.elem.set(fx.attribute, ' '.join(fixed_css.splitlines()[1:-1]))
+ else:
+ fx.elem.text = fixed_css
+ container.dirty(job.name)
+ return changed
+
+
def fix_errors(container, errors):
# Fix parsing
changed = False
@@ -121,11 +165,17 @@ def fix_errors(container, errors):
changed = True
+ has_fixable_css_errors = False
for err in errors:
+ if getattr(err, 'FIXABLE_CSS_ERROR', False):
+ has_fixable_css_errors = True
if err.INDIVIDUAL_FIX:
if err(container) is not False:
# Assume changed unless fixer explicitly says no change (this
# is because sometimes I forget to return True, and it is
# better to have a false positive than a false negative)
changed = True
+ if has_fixable_css_errors:
+ if fix_css(container):
+ changed = True
return changed