Edit book: Check book: Allow automatic fixing of various simple CSS errors

This commit is contained in:
Kovid Goyal 2023-01-05 13:52:25 +05:30
parent 49cd1944db
commit f08b238986
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 86 additions and 26 deletions

View File

@ -10,9 +10,10 @@
window.stylelint_results = []; window.stylelint_results = [];
window.check_css = function(src) { window.check_css = function(src, fix) {
stylelint.lint({ stylelint.lint({
code: src, code: src,
fix: fix,
config: { config: {
rules: { rules: {
'annotation-no-unknown': true, 'annotation-no-unknown': true,

View File

@ -22,14 +22,17 @@ from calibre.utils.webengine import secure_webengine, setup_profile
class CSSParseError(BaseError): class CSSParseError(BaseError):
level = ERROR level = ERROR
is_parsing_error = True is_parsing_error = True
FIXABLE_CSS_ERROR = False
class CSSError(BaseError): class CSSError(BaseError):
level = ERROR level = ERROR
FIXABLE_CSS_ERROR = False
class CSSWarning(BaseError): class CSSWarning(BaseError):
level = WARN level = WARN
FIXABLE_CSS_ERROR = False
def as_int_or_none(x): def as_int_or_none(x):
@ -57,10 +60,15 @@ def message_to_error(message, name, line_offset, rule_metadata):
line += line_offset line += line_offset
ans = cls(title, name, line, col) ans = cls(title, name, line, col)
ans.HELP = message.get('text') or '' ans.HELP = message.get('text') or ''
if ans.HELP:
ans.HELP += '. '
ans.css_rule_id = rule_id ans.css_rule_id = rule_id
m = rule_metadata.get(rule_id) m = rule_metadata.get(rule_id) or {}
if m and 'url' in m: if 'url' in m:
ans.HELP += '. ' + _('See <a href="{}">detailed description</a>.').format(m['url']) ans.HELP += _('See <a href="{}">detailed description</a>.').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 return ans
@ -107,7 +115,7 @@ class Worker(QWebEnginePage):
if new_title == 'ready': if new_title == 'ready':
self.ready = True self.ready = True
if self.pending is not None: if self.pending is not None:
self.check_css(self.pending) self.check_css(*self.pending)
self.pending = None self.pending = None
elif new_title == 'checked': elif new_title == 'checked':
self.runJavaScript('window.get_css_results()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.check_done) self.runJavaScript('window.get_css_results()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.check_done)
@ -119,17 +127,17 @@ class Worker(QWebEnginePage):
except Exception: except Exception:
pass pass
def check_css(self, src): def check_css(self, src, fix=False):
self.working = True self.working = True
self.runJavaScript( 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: if self.ready:
self.check_css(src) self.check_css(src, fix)
else: else:
self.working = True self.working = True
self.pending = src self.pending = src, fix
def check_done(self, results): def check_done(self, results):
self.working = False self.working = False
@ -148,7 +156,8 @@ class Pool:
w.work_done.connect(self.work_done) w.work_done.connect(self.work_done)
self.workers.append(w) 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.pending = list(enumerate(css_sources))
self.results = list(repeat(None, len(css_sources))) self.results = list(repeat(None, len(css_sources)))
self.working = True self.working = True
@ -166,7 +175,7 @@ class Pool:
if not w.working: if not w.working:
idx, src = self.pending.pop() idx, src = self.pending.pop()
w.result_idx = idx w.result_idx = idx
w.check_css_when_ready(src) w.check_css_when_ready(src, self.doing_fix)
break break
else: else:
break break
@ -191,14 +200,14 @@ class Pool:
pool = Pool() pool = Pool()
shutdown = pool.shutdown shutdown = pool.shutdown
atexit.register(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: if is_declaration:
css = 'div{\n' + css + '\n}' css = 'div{\n' + css + '\n}'
line_offset -= 1 line_offset -= 1
return Job(name, css, line_offset) return Job(name, css, line_offset, fix_data)
def check_css(jobs): def check_css(jobs):

View File

@ -4,21 +4,23 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from polyglot.builtins import iteritems from collections import namedtuple
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish.utils import guess_type from calibre.ebooks.oeb.polish.check.base import WARN, run_checkers
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.fonts import check_fonts 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 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'} 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 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): def fix_errors(container, errors):
# Fix parsing # Fix parsing
changed = False changed = False
@ -121,11 +165,17 @@ def fix_errors(container, errors):
changed = True changed = True
has_fixable_css_errors = False
for err in errors: for err in errors:
if getattr(err, 'FIXABLE_CSS_ERROR', False):
has_fixable_css_errors = True
if err.INDIVIDUAL_FIX: if err.INDIVIDUAL_FIX:
if err(container) is not False: if err(container) is not False:
# Assume changed unless fixer explicitly says no change (this # Assume changed unless fixer explicitly says no change (this
# is because sometimes I forget to return True, and it is # is because sometimes I forget to return True, and it is
# better to have a false positive than a false negative) # better to have a false positive than a false negative)
changed = True changed = True
if has_fixable_css_errors:
if fix_css(container):
changed = True
return changed return changed