%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/ebooks/oeb/polish/check/ |
Current File : //usr/lib/calibre/calibre/ebooks/oeb/polish/check/css.py |
#!/usr/bin/env python3 # License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> import atexit import json import numbers import sys from collections import namedtuple from itertools import repeat from qt.core import QApplication, QEventLoop, pyqtSignal, sip from qt.webengine import ( QWebEnginePage, QWebEngineProfile, QWebEngineScript ) from calibre import detect_ncpus as cpu_count, prints from calibre.ebooks.oeb.polish.check.base import ERROR, WARN, BaseError from calibre.gui2 import must_use_qt from calibre.gui2.webengine import secure_webengine class CSSParseError(BaseError): level = ERROR is_parsing_error = True class CSSError(BaseError): level = ERROR class CSSWarning(BaseError): level = WARN def as_int_or_none(x): if x is not None and not isinstance(x, numbers.Integral): try: x = int(x) except Exception: x = None return x def message_to_error(message, name, line_offset=0): rule = message.get('rule', {}) rule_id = rule.get('id') or '' cls = CSSWarning if message.get('type') == 'error': cls = CSSParseError if rule.get('name') == 'Parsing Errors' else CSSError title = message.get('message') or _('Unknown error') line = as_int_or_none(message.get('line')) col = as_int_or_none(message.get('col')) if col is not None: col -= 1 if line is not None: line += line_offset ans = cls(title, name, line, col) ans.HELP = rule.get('desc') or '' ans.css_rule_id = rule_id if ans.HELP and 'url' in rule: ans.HELP += ' ' + _('See <a href="{}">detailed description</a>.').format(rule['url']) return ans def csslint_js(): ans = getattr(csslint_js, 'ans', None) if ans is None: ans = csslint_js.ans = P('csslint.js', data=True, allow_user_override=False).decode('utf-8') + ''' window.check_css = function(src) { var rules = CSSLint.getRules(); var ruleset = {}; var ignored_rules = { 'order-alphabetical': 1, 'font-sizes': 1, 'zero-units': 1, 'bulletproof-font-face': 1, 'import': 1, 'box-model': 1, 'adjoining-classes': 1, 'box-sizing': 1, 'compatible-vendor-prefixes': 1, 'text-indent': 1, 'unique-headings': 1, 'fallback-colors': 1, 'font-faces': 1, 'regex-selectors': 1, 'universal-selector': 1, 'unqualified-attributes': 1, 'overqualified-elements': 1, 'shorthand': 1, 'duplicate-background-images': 1, 'floats': 1, 'ids': 1, 'gradients': 1 }; var error_rules = { 'known-properties': 1, 'duplicate-properties': 1, 'vendor-prefix': 1 }; for (var i = 0; i < rules.length; i++) { var rule = rules[i]; if (!ignored_rules[rule.id] && rule.browsers === "All") ruleset[rule.id] = error_rules[rule.id] ? 2 : 1; } var result = CSSLint.verify(src, ruleset); return result; } document.title = 'ready'; ''' return ans def create_profile(): ans = getattr(create_profile, 'ans', None) if ans is None: ans = create_profile.ans = QWebEngineProfile(QApplication.instance()) s = QWebEngineScript() s.setName('csslint.js') s.setSourceCode(csslint_js()) s.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld) ans.scripts().insert(s) return ans class Worker(QWebEnginePage): work_done = pyqtSignal(object, object) def __init__(self): must_use_qt() QWebEnginePage.__init__(self, create_profile(), QApplication.instance()) self.titleChanged.connect(self.title_changed) secure_webengine(self.settings()) self.console_messages = [] self.ready = False self.working = False self.pending = None self.setHtml('') def title_changed(self, new_title): if new_title == 'ready': self.ready = True if self.pending is not None: self.check_css(self.pending) self.pending = None def javaScriptConsoleMessage(self, level, msg, lineno, source_id): msg = f'{source_id}:{lineno}:{msg}' self.console_messages.append(msg) try: print(msg) except Exception: pass def check_css(self, src): self.working = True self.console_messages = [] self.runJavaScript( f'window.check_css({json.dumps(src)})', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.check_done) def check_css_when_ready(self, src): if self.ready: self.check_css(src) else: self.working = True self.pending = src def check_done(self, result): self.working = False self.work_done.emit(self, result) class Pool: def __init__(self): self.workers = [] self.max_workers = cpu_count() def add_worker(self): w = Worker() w.work_done.connect(self.work_done) self.workers.append(w) def check_css(self, css_sources): self.pending = list(enumerate(css_sources)) self.results = list(repeat(None, len(css_sources))) self.working = True self.assign_work() app = QApplication.instance() while self.working: app.processEvents(QEventLoop.ProcessEventsFlag.WaitForMoreEvents | QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) return self.results def assign_work(self): while self.pending: if len(self.workers) < self.max_workers: self.add_worker() for w in self.workers: if not w.working: idx, src = self.pending.pop() w.result_idx = idx w.check_css_when_ready(src) break else: break def work_done(self, worker, result): if not isinstance(result, dict): result = worker.console_messages self.results[worker.result_idx] = result self.assign_work() if not self.pending and not [w for w in self.workers if w.working]: self.working = False def shutdown(self): def safe_delete(x): if not sip.isdeleted(x): sip.delete(x) for i in self.workers: safe_delete(i) self.workers = [] pool = Pool() shutdown = pool.shutdown atexit.register(shutdown) Job = namedtuple('Job', 'name css line_offset') def create_job(name, css, line_offset=0, is_declaration=False): if is_declaration: css = 'div{\n' + css + '\n}' line_offset -= 1 return Job(name, css, line_offset) def check_css(jobs): errors = [] if not jobs: return errors results = pool.check_css([j.css for j in jobs]) for job, result in zip(jobs, results): if isinstance(result, dict): for msg in result['messages']: err = message_to_error(msg, job.name, job.line_offset) if err is not None: errors.append(err) elif isinstance(result, list) and result: errors.append(CSSParseError(_('Failed to process CSS in {name} with errors: {errors}').format( name=job.name, errors='\n'.join(result)), job.name)) else: errors.append(CSSParseError(_('Failed to process CSS in {name}').format(name=job.name), job.name)) return errors def main(): with open(sys.argv[-1], 'rb') as f: css = f.read().decode('utf-8') errors = check_css([create_job(sys.argv[-1], css)]) for error in errors: prints(error) if __name__ == '__main__': try: main() finally: shutdown()