From f08b238986be251d2181b3fe6c01a027d617a260 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 5 Jan 2023 13:52:25 +0530 Subject: [PATCH] Edit book: Check book: Allow automatic fixing of various simple CSS errors --- resources/stylelint.js | 3 +- src/calibre/ebooks/oeb/polish/check/css.py | 37 +++++++---- src/calibre/ebooks/oeb/polish/check/main.py | 72 +++++++++++++++++---- 3 files changed, 86 insertions(+), 26 deletions(-) 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