From 30ec8216e15262d6332a7fc0f3faa9b23595776f Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 13:17:03 +0200 Subject: [PATCH] Minify on backend --- couchpotato/core/_base/clientscript/main.py | 12 +- .../core/plugins/profile/static/profile.css | 12 +- .../static/scripts/library/prefix_free.js | 487 ------- couchpotato/static/style/main.css | 16 +- couchpotato/static/style/settings.css | 16 +- libs/cssprefixer/__init__.py | 20 + libs/cssprefixer/engine.py | 117 ++ libs/cssprefixer/rules.py | 271 ++++ libs/cssutils/__init__.py | 385 +++++ libs/cssutils/_codec2.py | 584 ++++++++ libs/cssutils/_codec3.py | 608 ++++++++ libs/cssutils/_fetch.py | 44 + libs/cssutils/_fetchgae.py | 68 + libs/cssutils/codec.py | 16 + libs/cssutils/css/__init__.py | 80 ++ libs/cssutils/css/colors.py | 184 +++ libs/cssutils/css/csscharsetrule.py | 159 +++ libs/cssutils/css/csscomment.py | 87 ++ libs/cssutils/css/cssfontfacerule.py | 184 +++ libs/cssutils/css/cssimportrule.py | 396 ++++++ libs/cssutils/css/cssmediarule.py | 302 ++++ libs/cssutils/css/cssnamespacerule.py | 295 ++++ libs/cssutils/css/csspagerule.py | 436 ++++++ libs/cssutils/css/cssproperties.py | 122 ++ libs/cssutils/css/cssrule.py | 304 ++++ libs/cssutils/css/cssrulelist.py | 53 + libs/cssutils/css/cssstyledeclaration.py | 697 +++++++++ libs/cssutils/css/cssstylerule.py | 234 +++ libs/cssutils/css/cssstylesheet.py | 804 +++++++++++ libs/cssutils/css/cssunknownrule.py | 209 +++ libs/cssutils/css/cssvalue.py | 1251 +++++++++++++++++ libs/cssutils/css/cssvariablesdeclaration.py | 330 +++++ libs/cssutils/css/cssvariablesrule.py | 198 +++ libs/cssutils/css/marginrule.py | 215 +++ libs/cssutils/css/property.py | 510 +++++++ libs/cssutils/css/selector.py | 813 +++++++++++ libs/cssutils/css/selectorlist.py | 234 +++ libs/cssutils/css/value.py | 871 ++++++++++++ libs/cssutils/css2productions.py | 131 ++ libs/cssutils/cssproductions.py | 124 ++ libs/cssutils/errorhandler.py | 118 ++ libs/cssutils/helper.py | 137 ++ libs/cssutils/parse.py | 232 +++ libs/cssutils/prodparser.py | 733 ++++++++++ libs/cssutils/profiles.py | 791 +++++++++++ libs/cssutils/sac.py | 428 ++++++ libs/cssutils/script.py | 362 +++++ libs/cssutils/scripts/__init__.py | 4 + libs/cssutils/scripts/csscapture.py | 69 + libs/cssutils/scripts/csscombine.py | 94 ++ libs/cssutils/scripts/cssparse.py | 62 + libs/cssutils/serialize.py | 1138 +++++++++++++++ libs/cssutils/settings.py | 15 + libs/cssutils/stylesheets/__init__.py | 11 + libs/cssutils/stylesheets/medialist.py | 235 ++++ libs/cssutils/stylesheets/mediaquery.py | 207 +++ libs/cssutils/stylesheets/stylesheet.py | 123 ++ libs/cssutils/stylesheets/stylesheetlist.py | 32 + libs/cssutils/tokenize2.py | 223 +++ libs/cssutils/util.py | 884 ++++++++++++ libs/encutils/__init__.py | 690 +++++++++ libs/minify/cssmin.py | 223 --- 62 files changed, 17953 insertions(+), 737 deletions(-) delete mode 100644 couchpotato/static/scripts/library/prefix_free.js create mode 100755 libs/cssprefixer/__init__.py create mode 100755 libs/cssprefixer/engine.py create mode 100755 libs/cssprefixer/rules.py create mode 100755 libs/cssutils/__init__.py create mode 100755 libs/cssutils/_codec2.py create mode 100755 libs/cssutils/_codec3.py create mode 100755 libs/cssutils/_fetch.py create mode 100755 libs/cssutils/_fetchgae.py create mode 100755 libs/cssutils/codec.py create mode 100755 libs/cssutils/css/__init__.py create mode 100755 libs/cssutils/css/colors.py create mode 100755 libs/cssutils/css/csscharsetrule.py create mode 100755 libs/cssutils/css/csscomment.py create mode 100755 libs/cssutils/css/cssfontfacerule.py create mode 100755 libs/cssutils/css/cssimportrule.py create mode 100755 libs/cssutils/css/cssmediarule.py create mode 100755 libs/cssutils/css/cssnamespacerule.py create mode 100755 libs/cssutils/css/csspagerule.py create mode 100755 libs/cssutils/css/cssproperties.py create mode 100755 libs/cssutils/css/cssrule.py create mode 100755 libs/cssutils/css/cssrulelist.py create mode 100755 libs/cssutils/css/cssstyledeclaration.py create mode 100755 libs/cssutils/css/cssstylerule.py create mode 100755 libs/cssutils/css/cssstylesheet.py create mode 100755 libs/cssutils/css/cssunknownrule.py create mode 100755 libs/cssutils/css/cssvalue.py create mode 100755 libs/cssutils/css/cssvariablesdeclaration.py create mode 100755 libs/cssutils/css/cssvariablesrule.py create mode 100755 libs/cssutils/css/marginrule.py create mode 100755 libs/cssutils/css/property.py create mode 100755 libs/cssutils/css/selector.py create mode 100755 libs/cssutils/css/selectorlist.py create mode 100755 libs/cssutils/css/value.py create mode 100755 libs/cssutils/css2productions.py create mode 100755 libs/cssutils/cssproductions.py create mode 100755 libs/cssutils/errorhandler.py create mode 100755 libs/cssutils/helper.py create mode 100755 libs/cssutils/parse.py create mode 100755 libs/cssutils/prodparser.py create mode 100755 libs/cssutils/profiles.py create mode 100755 libs/cssutils/sac.py create mode 100755 libs/cssutils/script.py create mode 100755 libs/cssutils/scripts/__init__.py create mode 100755 libs/cssutils/scripts/csscapture.py create mode 100755 libs/cssutils/scripts/csscombine.py create mode 100755 libs/cssutils/scripts/cssparse.py create mode 100755 libs/cssutils/serialize.py create mode 100755 libs/cssutils/settings.py create mode 100755 libs/cssutils/stylesheets/__init__.py create mode 100755 libs/cssutils/stylesheets/medialist.py create mode 100755 libs/cssutils/stylesheets/mediaquery.py create mode 100755 libs/cssutils/stylesheets/stylesheet.py create mode 100755 libs/cssutils/stylesheets/stylesheetlist.py create mode 100755 libs/cssutils/tokenize2.py create mode 100755 libs/cssutils/util.py create mode 100755 libs/encutils/__init__.py delete mode 100644 libs/minify/cssmin.py diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 4938a22a..9536463e 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -1,10 +1,11 @@ from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from minify.cssmin import cssmin from minify.jsmin import jsmin +import cssprefixer import os import traceback @@ -23,7 +24,6 @@ class ClientScript(Plugin): 'script': [ 'scripts/library/mootools.js', 'scripts/library/mootools_more.js', - 'scripts/library/prefix_free.js', 'scripts/library/uniform.js', 'scripts/library/form_replacement/form_check.js', 'scripts/library/form_replacement/form_radio.js', @@ -69,7 +69,8 @@ class ClientScript(Plugin): addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_scripts', self.getScripts) - addEvent('app.load', self.minify) + if not Env.get('dev'): + addEvent('app.load', self.minify) self.addCore() @@ -108,9 +109,10 @@ class ClientScript(Plugin): if file_type == 'script': data = jsmin(f) else: - data = cssmin(f) + data = cssprefixer.process(f, debug = False, minify = True) data = data.replace('../images/', '../static/images/') data = data.replace('../fonts/', '../static/fonts/') + data = data.replace('../../static/', '../static/') # Replace inside plugins raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) @@ -120,7 +122,7 @@ class ClientScript(Plugin): data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += r.get('data') + '\n\n' - self.createFile(out, data.strip()) + self.createFile(out, ss(data.strip())) if not self.minified.get(file_type): self.minified[file_type] = {} diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index 9d50d2fd..26ee64c7 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -71,13 +71,13 @@ } .profile .types .type .handle { - background: url('./handle.png') center; + background: url('../../static/profile_plugin/handle.png') center; display: inline-block; height: 20px; width: 20px; - cursor: grab; - cursor: -moz-grab; cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; margin: 0; } @@ -105,9 +105,9 @@ } #profile_ordering li { - cursor: grab; - cursor: -moz-grab; cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.2); padding: 0 5px; } @@ -126,7 +126,7 @@ } #profile_ordering li .handle { - background: url('./handle.png') center; + background: url('../../static/profile_plugin/handle.png') center; width: 20px; float: right; } diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js deleted file mode 100644 index b6d9812a..00000000 --- a/couchpotato/static/scripts/library/prefix_free.js +++ /dev/null @@ -1,487 +0,0 @@ -/** - * StyleFix 1.0.3 & PrefixFree 1.0.7 - * @author Lea Verou - * MIT license - */ - -(function(){ - -if(!window.addEventListener) { - return; -} - -var self = window.StyleFix = { - link: function(link) { - try { - // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets - if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { - return; - } - } - catch(e) { - return; - } - - var url = link.href || link.getAttribute('data-href'), - base = url.replace(/[^\/]+$/, ''), - base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0], - base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0], - base_query = /^([^?]*)\??/.exec(url)[1], - parent = link.parentNode, - xhr = new XMLHttpRequest(), - process; - - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - process(); - } - }; - - process = function() { - var css = xhr.responseText; - - if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) { - css = self.fix(css, true, link); - - // Convert relative URLs to absolute, if needed - if(base) { - css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) { - if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative - return $0; - } - else if(/^\/\//.test(url)) { // Scheme-relative - // May contain sequences like /../ and /./ but those DO work - return 'url("' + base_scheme + url + '")'; - } - else if(/^\//.test(url)) { // Domain-relative - return 'url("' + base_domain + url + '")'; - } - else if(/^\?/.test(url)) { // Query-relative - return 'url("' + base_query + url + '")'; - } - else { - // Path-relative - return 'url("' + base + url + '")'; - } - }); - - // behavior URLs shoudn’t be converted (Issue #19) - // base should be escaped before added to RegExp (Issue #81) - var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1"); - css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1'); - } - - var style = document.createElement('style'); - style.textContent = css; - style.media = link.media; - style.disabled = link.disabled; - style.setAttribute('data-href', link.getAttribute('href')); - - parent.insertBefore(style, link); - parent.removeChild(link); - - style.media = link.media; // Duplicate is intentional. See issue #31 - } - }; - - try { - xhr.open('GET', url); - xhr.send(null); - } catch (e) { - // Fallback to XDomainRequest if available - if (typeof XDomainRequest != "undefined") { - xhr = new XDomainRequest(); - xhr.onerror = xhr.onprogress = function() {}; - xhr.onload = process; - xhr.open("GET", url); - xhr.send(null); - } - } - - link.setAttribute('data-inprogress', ''); - }, - - styleElement: function(style) { - if (style.hasAttribute('data-noprefix')) { - return; - } - var disabled = style.disabled; - - style.textContent = self.fix(style.textContent, true, style); - - style.disabled = disabled; - }, - - styleAttribute: function(element) { - var css = element.getAttribute('style'); - - css = self.fix(css, false, element); - - element.setAttribute('style', css); - }, - - process: function() { - // Linked stylesheets - $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); - - // Inline stylesheets - $('style').forEach(StyleFix.styleElement); - - // Inline styles - $('[style]').forEach(StyleFix.styleAttribute); - }, - - register: function(fixer, index) { - (self.fixers = self.fixers || []) - .splice(index === undefined? self.fixers.length : index, 0, fixer); - }, - - fix: function(css, raw, element) { - for(var i=0; i -1) { - // Gradients are supported with a prefix, convert angles to legacy - css = css.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig, function ($0, delim, repeating, deg) { - return delim + (repeating || '') + 'linear-gradient(' + (90-deg) + 'deg'; - }); - } - - css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css); - css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css); - css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css); - - // Prefix properties *inside* values (issue #8) - if (self.properties.length) { - var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); - - css = fix('valueProperties', '\\b', ':(.+?);', function($0) { - return $0.replace(regex, prefix + "$1") - }, css); - } - - if(raw) { - css = fix('selectors', '', '\\b', self.prefixSelector, css); - css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css); - } - - // Fix double prefixing - css = css.replace(RegExp('-' + prefix, 'g'), '-'); - - // Prefix wildcard - css = css.replace(/-\*-(?=[a-z]+)/gi, self.prefix); - - return css; - }, - - property: function(property) { - return (self.properties.indexOf(property)? self.prefix : '') + property; - }, - - value: function(value, property) { - value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value); - value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value); - - // TODO properties inside values - - return value; - }, - - // Warning: Prefixes no matter what, even if the selector is supported prefix-less - prefixSelector: function(selector) { - return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) - }, - - // Warning: Prefixes no matter what, even if the property is supported prefix-less - prefixProperty: function(property, camelCase) { - var prefixed = self.prefix + property; - - return camelCase? StyleFix.camelCase(prefixed) : prefixed; - } -}; - -/************************************** - * Properties - **************************************/ -(function() { - var prefixes = {}, - properties = [], - shorthands = {}, - style = getComputedStyle(document.documentElement, null), - dummy = document.createElement('div').style; - - // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. - var iterate = function(property) { - if(property.charAt(0) === '-') { - properties.push(property); - - var parts = property.split('-'), - prefix = parts[1]; - - // Count prefix uses - prefixes[prefix] = ++prefixes[prefix] || 1; - - // This helps determining shorthands - while(parts.length > 3) { - parts.pop(); - - var shorthand = parts.join('-'); - - if(supported(shorthand) && properties.indexOf(shorthand) === -1) { - properties.push(shorthand); - } - } - } - }, - supported = function(property) { - return StyleFix.camelCase(property) in dummy; - } - - // Some browsers have numerical indices for the properties, some don't - if(style.length > 0) { - for(var i=0; i .spinner, .mask{ border-radius:30px; box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - background: url('../images/sprite.png') no-repeat 94% -53px, -*-linear-gradient( - 270deg, + background: url('../images/sprite.png') no-repeat 94% -53px, linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); @@ -583,8 +583,8 @@ body > .spinner, .mask{ border: 1px solid #252930; box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2); background: rgb(55,62,74); - background-image: -*-linear-gradient( - 90deg, + background-image: linear-gradient( + 0, rgb(55,62,74) 0%, rgb(73,83,98) 100% ); @@ -664,8 +664,8 @@ body > .spinner, .mask{ box-shadow: 0 20px 20px -5px rgba(0,0,0,0.1); text-align: center; color: #000; - background-image: -*-linear-gradient( - 45deg, + background-image: linear-gradient( + -45deg, rgb(200,200,200) 0%, rgb(255,255,255) 100% ); @@ -725,8 +725,8 @@ body > .spinner, .mask{ overflow: hidden; transition: all .6s cubic-bezier(0.9,0,0.1,1); box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - background-image: -*-linear-gradient( - 270deg, + background-image: linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 638a6585..1f001b7c 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -20,8 +20,8 @@ padding: 35px 0; margin: 0; min-height: 470px; - background-image: -*-linear-gradient( - 14deg, + background-image: linear-gradient( + 76deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.3) 100% ); @@ -443,16 +443,16 @@ border-radius: 2px; } .page .tag_input > ul:hover > li.choice { - background: -*-linear-gradient( - 270deg, + background: linear-gradient( + 180deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100% ); } .page .tag_input > ul > li.choice:hover, .page .tag_input > ul > li.choice.selected { - background: -*-linear-gradient( - 270deg, + background: linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); @@ -490,8 +490,8 @@ margin: -9px 0 0 -16px; border-radius: 30px 30px 0 0; cursor: pointer; - background: url('../images/icon.delete.png') no-repeat center 2px, -*-linear-gradient( - 270deg, + background: url('../images/icon.delete.png') no-repeat center 2px, linear-gradient( + 180deg, #5b9bd1 0%, #5b9bd1 100% ); diff --git a/libs/cssprefixer/__init__.py b/libs/cssprefixer/__init__.py new file mode 100755 index 00000000..05674329 --- /dev/null +++ b/libs/cssprefixer/__init__.py @@ -0,0 +1,20 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import engine +import rules +from engine import process + +__all__ = ('process', 'engine', 'rules') diff --git a/libs/cssprefixer/engine.py b/libs/cssprefixer/engine.py new file mode 100755 index 00000000..894f6e7f --- /dev/null +++ b/libs/cssprefixer/engine.py @@ -0,0 +1,117 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cssutils +import re +from rules import rules as tr_rules +from rules import prefixRegex + + +keyframesRegex = re.compile(r'@keyframes\s?\w+\s?{(.*)}') +blockRegex = re.compile(r'\w+\s?\{(.*)\}') + + +def magic(ruleset, debug, minify, filt, parser): + if isinstance(ruleset, cssutils.css.CSSUnknownRule): + if ruleset.cssText.startswith('@keyframes'): + inner = parser.parseString(keyframesRegex.split(ruleset.cssText.replace('\n', ''))[1]) + # BUG: doesn't work when minified + s = '' if minify else '\n' + return '@-webkit-keyframes {' + s + \ + ''.join([magic(rs, debug, minify, ['webkit'], parser) for rs in inner]) \ + + '}' + s + '@-moz-keyframes {' + s + \ + ''.join([magic(rs, debug, minify, ['moz'], parser) for rs in inner]) \ + + '}' + s + ruleset.cssText + elif ruleset.cssText.startswith('from') or ruleset.cssText.startswith('to'): + return ''.join([magic(rs, debug, minify, filt, parser) + for rs in parser.parseString(blockRegex.sub(r'\1', ruleset.cssText.replace('\n', ''))[1])]) + else: + return + elif hasattr(ruleset, 'style'): # Comments don't + ruleSet = set() + rules = list() + children = list(ruleset.style.children()) + ruleset.style = cssutils.css.CSSStyleDeclaration() # clear out the styles that were there + for rule in children: + if not hasattr(rule, 'name'): # comments don't have name + rules.append(rule) + continue + name = prefixRegex.sub('', rule.name) + if name in tr_rules: + rule.name = name + if rule.cssText in ruleSet: + continue + ruleSet.add(rule.cssText) + rules.append(rule) + + ruleset.style.seq._readonly = False + for rule in rules: + if not hasattr(rule, 'name'): + ruleset.style.seq.append(rule, 'Comment') + continue + processor = None + try: # try except so if anything goes wrong we don't lose the original property + if rule.name in tr_rules: + processor = tr_rules[rule.name](rule) + [ruleset.style.seq.append(prop, 'Property') for prop in processor.get_prefixed_props(filt) if prop] + # always add the original rule + if processor and hasattr(processor, 'get_base_prop'): + ruleset.style.seq.append(processor.get_base_prop(), 'Property') + else: + ruleset.style.seq.append(rule, 'Property') + except: + if debug: + print 'warning with ' + str(rule) + ruleset.style.seq.append(rule, 'Property') + ruleset.style.seq._readonly = True + elif hasattr(ruleset, 'cssRules'): + for subruleset in ruleset: + magic(subruleset, debug, minify, filt, parser) + cssText = ruleset.cssText + if not cssText: # blank rules return None so return an empty string + return + if minify or not hasattr(ruleset, 'style'): + return unicode(cssText) + return unicode(cssText) + '\n' + + +def process(string, debug = False, minify = False, filt = ['webkit', 'moz', 'o', 'ms'], **prefs): + loglevel = 'DEBUG' if debug else 'ERROR' + parser = cssutils.CSSParser(loglevel = 'CRITICAL') + if minify: + cssutils.ser.prefs.useMinified() + else: + cssutils.ser.prefs.useDefaults() + + # use the passed in prefs + for key, value in prefs.iteritems(): + if hasattr(cssutils.ser.prefs, key): + cssutils.ser.prefs.__dict__[key] = value + + results = [] + sheet = parser.parseString(string) + for ruleset in sheet.cssRules: + cssText = magic(ruleset, debug, minify, filt, parser) + if cssText: + results.append(cssText) + + # format with newlines based on minify + joinStr = '' if minify else '\n' + + # Not using sheet.cssText - it's buggy: + # it skips some prefixed properties. + return joinStr.join(results).rstrip() + +__all__ = ['process'] diff --git a/libs/cssprefixer/rules.py b/libs/cssprefixer/rules.py new file mode 100755 index 00000000..156d4f18 --- /dev/null +++ b/libs/cssprefixer/rules.py @@ -0,0 +1,271 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import cssutils + +prefixRegex = re.compile('^(-o-|-ms-|-moz-|-webkit-)') + + +class BaseReplacementRule(object): + vendor_prefixes = ['moz', 'webkit'] + + def __init__(self, prop): + self.prop = prop + + def get_prefixed_props(self, filt): + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield cssutils.css.Property( + name='-%s-%s' % (prefix, self.prop.name), + value=self.prop.value, + priority=self.prop.priority + ) + + @staticmethod + def should_prefix(): + return True + + +class FullReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o', 'ms']) + + +class BaseAndIEReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['ms']) + + +class BaseAndOperaReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o']) + + +class WebkitReplacementRule(BaseReplacementRule): + vendor_prefixes = ['webkit'] + + +class OperaAndIEReplacementRule(BaseReplacementRule): + vendor_prefixes = ['ms', 'o'] + + +class MozReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz'] + + +class BorderRadiusReplacementRule(BaseReplacementRule): + """ + Mozilla's Gecko engine uses different syntax for rounded corners. + """ + vendor_prefixes = ['webkit'] + + def get_prefixed_props(self, filt): + for prop in BaseReplacementRule.get_prefixed_props(self, filt): + yield prop + if 'moz' in filt: + name = '-moz-' + self.prop.name.replace('top-left-radius', 'radius-topleft') \ + .replace('top-right-radius', 'radius-topright') \ + .replace('bottom-right-radius', 'radius-bottomright') \ + .replace('bottom-left-radius', 'radius-bottomleft') + yield cssutils.css.Property( + name=name, + value=self.prop.value, + priority=self.prop.priority + ) + + +class DisplayReplacementRule(BaseReplacementRule): + """ + Flexible Box Model stuff. + CSSUtils parser doesn't support duplicate properties, so that's dirty. + """ + def get_prefixed_props(self, filt): + if self.prop.value == 'box': # only add prefixes if the value is box + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield cssutils.css.Property( + name='display', + value='-%s-box' % prefix, + priority=self.prop.priority + ) + + +class TransitionReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz', 'o', 'webkit'] + + def __get_prefixed_prop(self, prefix=None): + name = self.prop.name + if prefix: + name = '-%s-%s' % (prefix, self.prop.name) + newValues = [] + for value in self.prop.value.split(','): + parts = value.strip().split(' ') + parts[0] = prefixRegex.sub('', parts[0]) + if parts[0] in rules and prefix and rules[parts[0]].should_prefix(): + parts[0] = '-%s-%s' % (prefix, parts[0]) + newValues.append(' '.join(parts)) + return cssutils.css.Property( + name=name, + value=', '.join(newValues), + priority=self.prop.priority + ) + + def get_prefixed_props(self, filt): + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield self.__get_prefixed_prop(prefix) + + def get_base_prop(self): + return self.__get_prefixed_prop() + + +class GradientReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz', 'o', 'webkit'] + + def __iter_values(self): + valueSplit = self.prop.value.split(',') + index = 0 + # currentString = '' + while(True): + if index >= len(valueSplit): + break + rawValue = valueSplit[index].strip() + snip = prefixRegex.sub('', rawValue) + if snip.startswith('linear-gradient'): + values = [re.sub('^linear-gradient\(', '', snip)] + if valueSplit[index + 1].strip().endswith(')'): + values.append(re.sub('\)+$', '', valueSplit[index + 1].strip())) + else: + values.append(valueSplit[index + 1].strip()) + values.append(re.sub('\)+$', '', valueSplit[index + 2].strip())) + if len(values) == 2: + yield { + 'start': values[0], + 'end': values[1] + } + else: + yield { + 'pos': values[0], + 'start': values[1], + 'end': values[2] + } + index += len(values) + elif snip.startswith('gradient'): + yield { + 'start': re.sub('\)+$', '', valueSplit[index + 4].strip()), + 'end': re.sub('\)+$', '', valueSplit[index + 6].strip()), + } + index += 7 + else: + # not a gradient so just yield the raw string + yield rawValue + index += 1 + + def __get_prefixed_prop(self, values, prefix=None): + gradientName = 'linear-gradient' + if prefix: + gradientName = '-%s-%s' % (prefix, gradientName) + newValues = [] + for value in values: + if isinstance(value, dict): + if 'pos' in value: + newValues.append(gradientName + '(%(pos)s, %(start)s, %(end)s)' % value) + else: + newValues.append(gradientName + '(%(start)s, %(end)s)' % value) + else: + newValues.append(value) + return cssutils.css.Property( + name=self.prop.name, + value=', '.join(newValues), + priority=self.prop.priority + ) + + def get_prefixed_props(self, filt): + values = list(self.__iter_values()) + needPrefix = False + for value in values: # check if there are any gradients + if isinstance(value, dict): + needPrefix = True + break + if needPrefix: + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield self.__get_prefixed_prop(values, prefix) + if prefix == 'webkit': + newValues = [] + for value in values: + if isinstance(value, dict): + newValues.append('-webkit-gradient(linear, left top, left bottom, color-stop(0, %(start)s), color-stop(1, %(end)s))' % value) + else: + newValues.append(value) + yield cssutils.css.Property( + name=self.prop.name, + value=', '.join(newValues), + priority=self.prop.priority + ) + else: + yield None + + def get_base_prop(self): + values = self.__iter_values() + return self.__get_prefixed_prop(values) + +rules = { + 'border-radius': BaseReplacementRule, + 'border-top-left-radius': BorderRadiusReplacementRule, + 'border-top-right-radius': BorderRadiusReplacementRule, + 'border-bottom-right-radius': BorderRadiusReplacementRule, + 'border-bottom-left-radius': BorderRadiusReplacementRule, + 'border-image': FullReplacementRule, + 'box-shadow': BaseReplacementRule, + 'box-sizing': MozReplacementRule, + 'box-orient': BaseAndIEReplacementRule, + 'box-direction': BaseAndIEReplacementRule, + 'box-ordinal-group': BaseAndIEReplacementRule, + 'box-align': BaseAndIEReplacementRule, + 'box-flex': BaseAndIEReplacementRule, + 'box-flex-group': BaseReplacementRule, + 'box-pack': BaseAndIEReplacementRule, + 'box-lines': BaseAndIEReplacementRule, + 'user-select': BaseReplacementRule, + 'user-modify': BaseReplacementRule, + 'margin-start': BaseReplacementRule, + 'margin-end': BaseReplacementRule, + 'padding-start': BaseReplacementRule, + 'padding-end': BaseReplacementRule, + 'column-count': BaseReplacementRule, + 'column-gap': BaseReplacementRule, + 'column-rule': BaseReplacementRule, + 'column-rule-color': BaseReplacementRule, + 'column-rule-style': BaseReplacementRule, + 'column-rule-width': BaseReplacementRule, + 'column-span': WebkitReplacementRule, + 'column-width': BaseReplacementRule, + 'columns': WebkitReplacementRule, + + 'background-clip': WebkitReplacementRule, + 'background-origin': WebkitReplacementRule, + 'background-size': WebkitReplacementRule, + 'background-image': GradientReplacementRule, + 'background': GradientReplacementRule, + + 'text-overflow': OperaAndIEReplacementRule, + + 'transition': TransitionReplacementRule, + 'transition-delay': BaseAndOperaReplacementRule, + 'transition-duration': BaseAndOperaReplacementRule, + 'transition-property': TransitionReplacementRule, + 'transition-timing-function': BaseAndOperaReplacementRule, + 'transform': FullReplacementRule, + 'transform-origin': FullReplacementRule, + + 'display': DisplayReplacementRule, + 'appearance': WebkitReplacementRule, + 'hyphens': BaseReplacementRule, +} diff --git a/libs/cssutils/__init__.py b/libs/cssutils/__init__.py new file mode 100755 index 00000000..8157ce57 --- /dev/null +++ b/libs/cssutils/__init__.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +"""cssutils - CSS Cascading Style Sheets library for Python + + Copyright (C) 2004-2013 Christof Hoeke + + cssutils is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + + +A Python package to parse and build CSS Cascading Style Sheets. DOM only, not +any rendering facilities! + +Based upon and partly implementing the following specifications : + +`CSS 2.1 `__ + General CSS rules and properties are defined here +`CSS 2.1 Errata `__ + A few errata, mainly the definition of CHARSET_SYM tokens +`CSS3 Module: Syntax `__ + Used in parts since cssutils 0.9.4. cssutils tries to use the features from + CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some + parts are from CSS 2.1 +`MediaQueries `__ + MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in + @import and @media rules. +`Namespaces `__ + Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5 + for dev version +`CSS3 Module: Pages Media `__ + Most properties of this spec are implemented including MarginRules +`Selectors `__ + The selector syntax defined here (and not in CSS 2.1) should be parsable + with cssutils (*should* mind though ;) ) + +`DOM Level 2 Style CSS `__ + DOM for package css. 0.9.8 removes support for CSSValue and related API, + see PropertyValue and Value API for now +`DOM Level 2 Style Stylesheets `__ + DOM for package stylesheets +`CSSOM `__ + A few details (mainly the NamespaceRule DOM) is taken from here. Plan is + to move implementation to the stuff defined here which is newer but still + no REC so might change anytime... + + +The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax +(W3C Working Draft 13 August 2003) `__ which +itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as +possible but uses some (helpful) parts of the CSS 2.1 tokenizer. + +I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least +be able to parse both grammars including some more real world cases (some CSS +hacks are actually parsed and serialized). Both official grammars are not final +nor bugfree but still feasible. cssutils aim is not to be fully compliant to +any CSS specification (the specifications seem to be in a constant flow anyway) +but cssutils *should* be able to read and write as many as possible CSS +stylesheets "in the wild" while at the same time implement the official APIs +which are well documented. Some minor extensions are provided as well. + +Please visit http://cthedot.de/cssutils/ for more details. + + +Tested with Python 2.7.3 and 3.3 on Windows 8 64bit. + + +This library may be used ``from cssutils import *`` which +import subpackages ``css`` and ``stylesheets``, CSSParser and +CSSSerializer classes only. + +Usage may be:: + + >>> from cssutils import * + >>> parser = CSSParser() + >>> sheet = parser.parseString(u'a { color: red}') + >>> print sheet.cssText + a { + color: red + } + +""" +__all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer'] +__docformat__ = 'restructuredtext' +__author__ = 'Christof Hoeke with contributions by Walter Doerwald' +__date__ = '$LastChangedDate:: $:' + +VERSION = '0.9.10' + +__version__ = '%s $Id$' % VERSION + +import sys +if sys.version_info < (2,6): + bytes = str + +import codec +import os.path +import urllib +import urlparse +import xml.dom + +# order of imports is important (partly circular) +from . import util +import errorhandler +log = errorhandler.ErrorHandler() + +import css +import stylesheets +from parse import CSSParser + +from serialize import CSSSerializer +ser = CSSSerializer() + +from profiles import Profiles +profile = Profiles(log=log) + +# used by Selector defining namespace prefix '*' +_ANYNS = -1 + +class DOMImplementationCSS(object): + """This interface allows the DOM user to create a CSSStyleSheet + outside the context of a document. There is no way to associate + the new CSSStyleSheet with a document in DOM Level 2. + + This class is its *own factory*, as it is given to + xml.dom.registerDOMImplementation which simply calls it and receives + an instance of this class then. + """ + _features = [ + ('css', '1.0'), + ('css', '2.0'), + ('stylesheets', '1.0'), + ('stylesheets', '2.0') + ] + + def createCSSStyleSheet(self, title, media): + """ + Creates a new CSSStyleSheet. + + title of type DOMString + The advisory title. See also the Style Sheet Interfaces + section. + media of type DOMString + The comma-separated list of media associated with the new style + sheet. See also the Style Sheet Interfaces section. + + returns + CSSStyleSheet: A new CSS style sheet. + + TODO: DOMException + SYNTAX_ERR: Raised if the specified media string value has a + syntax error and is unparsable. + """ + return css.CSSStyleSheet(title=title, media=media) + + def createDocument(self, *args): + # not needed to HTML, also not for CSS? + raise NotImplementedError + + def createDocumentType(self, *args): + # not needed to HTML, also not for CSS? + raise NotImplementedError + + def hasFeature(self, feature, version): + return (feature.lower(), unicode(version)) in self._features + +xml.dom.registerDOMImplementation('cssutils', DOMImplementationCSS) + + +def parseString(*a, **k): + return CSSParser().parseString(*a, **k) +parseString.__doc__ = CSSParser.parseString.__doc__ + +def parseFile(*a, **k): + return CSSParser().parseFile(*a, **k) +parseFile.__doc__ = CSSParser.parseFile.__doc__ + +def parseUrl(*a, **k): + return CSSParser().parseUrl(*a, **k) +parseUrl.__doc__ = CSSParser.parseUrl.__doc__ + +def parseStyle(*a, **k): + return CSSParser().parseStyle(*a, **k) +parseStyle.__doc__ = CSSParser.parseStyle.__doc__ + +# set "ser", default serializer +def setSerializer(serializer): + """Set the global serializer used by all class in cssutils.""" + global ser + ser = serializer + +def getUrls(sheet): + """Retrieve all ``url(urlstring)`` values (in e.g. + :class:`cssutils.css.CSSImportRule` or :class:`cssutils.css.CSSValue` + objects of given `sheet`. + + :param sheet: + :class:`cssutils.css.CSSStyleSheet` object whose URLs are yielded + + This function is a generator. The generated URL values exclude ``url(`` and + ``)`` and surrounding single or double quotes. + """ + for importrule in (r for r in sheet if r.type == r.IMPORT_RULE): + yield importrule.href + + def styleDeclarations(base): + "recursive generator to find all CSSStyleDeclarations" + if hasattr(base, 'cssRules'): + for rule in base.cssRules: + for s in styleDeclarations(rule): + yield s + elif hasattr(base, 'style'): + yield base.style + + for style in styleDeclarations(sheet): + for p in style.getProperties(all=True): + for v in p.propertyValue: + if v.type == 'URI': + yield v.uri + +def replaceUrls(sheetOrStyle, replacer, ignoreImportRules=False): + """Replace all URLs in :class:`cssutils.css.CSSImportRule` or + :class:`cssutils.css.CSSValue` objects of given `sheetOrStyle`. + + :param sheetOrStyle: + a :class:`cssutils.css.CSSStyleSheet` or a + :class:`cssutils.css.CSSStyleDeclaration` which is changed in place + :param replacer: + a function which is called with a single argument `url` which + is the current value of each url() excluding ``url(``, ``)`` and + surrounding (single or double) quotes. + :param ignoreImportRules: + if ``True`` does not call `replacer` with URLs from @import rules. + """ + if not ignoreImportRules and not isinstance(sheetOrStyle, + css.CSSStyleDeclaration): + for importrule in (r for r in sheetOrStyle if r.type == r.IMPORT_RULE): + importrule.href = replacer(importrule.href) + + def styleDeclarations(base): + "recursive generator to find all CSSStyleDeclarations" + if hasattr(base, 'cssRules'): + for rule in base.cssRules: + for s in styleDeclarations(rule): + yield s + elif hasattr(base, 'style'): + yield base.style + elif isinstance(sheetOrStyle, css.CSSStyleDeclaration): + # base is a style already + yield base + + for style in styleDeclarations(sheetOrStyle): + for p in style.getProperties(all=True): + for v in p.propertyValue: + if v.type == v.URI: + v.uri = replacer(v.uri) + +def resolveImports(sheet, target=None): + """Recurcively combine all rules in given `sheet` into a `target` sheet. + @import rules which use media information are tried to be wrapped into + @media rules so keeping the media information. This may not work in + all instances (if e.g. an @import rule itself contains an @import rule + with different media infos or if it contains rules which may not be + used inside an @media block like @namespace rules.). In these cases + the @import rule is kept as in the original sheet and a WARNING is issued. + + :param sheet: + in this given :class:`cssutils.css.CSSStyleSheet` all import rules are + resolved and added to a resulting *flat* sheet. + :param target: + A :class:`cssutils.css.CSSStyleSheet` object which will be the + resulting *flat* sheet if given + :returns: given `target` or a new :class:`cssutils.css.CSSStyleSheet` + object + """ + if not target: + target = css.CSSStyleSheet(href=sheet.href, + media=sheet.media, + title=sheet.title) + + def getReplacer(targetbase): + "Return a replacer which uses base to return adjusted URLs" + basesch, baseloc, basepath, basequery, basefrag = urlparse.urlsplit(targetbase) + basepath, basepathfilename = os.path.split(basepath) + + def replacer(uri): + scheme, location, path, query, fragment = urlparse.urlsplit(uri) + if not scheme and not location and not path.startswith(u'/'): + # relative + path, filename = os.path.split(path) + combined = os.path.normpath(os.path.join(basepath, path, filename)) + return urllib.pathname2url(combined) + else: + # keep anything absolute + return uri + + return replacer + + for rule in sheet.cssRules: + if rule.type == rule.CHARSET_RULE: + pass + elif rule.type == rule.IMPORT_RULE: + log.info(u'Processing @import %r' % rule.href, neverraise=True) + + if rule.hrefFound: + # add all rules of @import to current sheet + target.add(css.CSSComment(cssText=u'/* START @import "%s" */' + % rule.href)) + + try: + # nested imports + importedSheet = resolveImports(rule.styleSheet) + except xml.dom.HierarchyRequestErr, e: + log.warn(u'@import: Cannot resolve target, keeping rule: %s' + % e, neverraise=True) + target.add(rule) + else: + # adjust relative URI references + log.info(u'@import: Adjusting paths for %r' % rule.href, + neverraise=True) + replaceUrls(importedSheet, + getReplacer(rule.href), + ignoreImportRules=True) + + # might have to wrap rules in @media if media given + if rule.media.mediaText == u'all': + mediaproxy = None + else: + keepimport = False + for r in importedSheet: + # check if rules present which may not be + # combined with media + if r.type not in (r.COMMENT, + r.STYLE_RULE, + r.IMPORT_RULE): + keepimport = True + break + if keepimport: + log.warn(u'Cannot combine imported sheet with' + u' given media as other rules then' + u' comments or stylerules found %r,' + u' keeping %r' % (r, + rule.cssText), + neverraise=True) + target.add(rule) + continue + + # wrap in @media if media is not `all` + log.info(u'@import: Wrapping some rules in @media ' + u' to keep media: %s' + % rule.media.mediaText, neverraise=True) + mediaproxy = css.CSSMediaRule(rule.media.mediaText) + + for r in importedSheet: + if mediaproxy: + mediaproxy.add(r) + else: + # add to top sheet directly but are difficult anyway + target.add(r) + + if mediaproxy: + target.add(mediaproxy) + + else: + # keep @import as it is + log.error(u'Cannot get referenced stylesheet %r, keeping rule' + % rule.href, neverraise=True) + target.add(rule) + + else: + target.add(rule) + + return target + + +if __name__ == '__main__': + print __doc__ diff --git a/libs/cssutils/_codec2.py b/libs/cssutils/_codec2.py new file mode 100755 index 00000000..d0ae617f --- /dev/null +++ b/libs/cssutils/_codec2.py @@ -0,0 +1,584 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import codecs +import marshal + +# We're using bits to store all possible candidate encodings (or variants, i.e. +# we have two bits for the variants of UTF-16 and two for the +# variants of UTF-32). +# +# Prefixes for various CSS encodings +# UTF-8-SIG xEF xBB xBF +# UTF-16 (LE) xFF xFE ~x00|~x00 +# UTF-16 (BE) xFE xFF +# UTF-16-LE @ x00 @ x00 +# UTF-16-BE x00 @ +# UTF-32 (LE) xFF xFE x00 x00 +# UTF-32 (BE) x00 x00 xFE xFF +# UTF-32-LE @ x00 x00 x00 +# UTF-32-BE x00 x00 x00 @ +# CHARSET @ c h a ... + + +def detectencoding_str(input, final=False): + """ + Detect the encoding of the byte string ``input``, which contains the + beginning of a CSS file. This function returns the detected encoding (or + ``None`` if it hasn't got enough data), and a flag that indicates whether + that encoding has been detected explicitely or implicitely. To detect the + encoding the first few bytes are used (or if ``input`` is ASCII compatible + and starts with a charset rule the encoding name from the rule). "Explicit" + detection means that the bytes start with a BOM or a charset rule. + + If the encoding can't be detected yet, ``None`` is returned as the encoding. + ``final`` specifies whether more data will be available in later calls or + not. If ``final`` is true, ``detectencoding_str()`` will never return + ``None`` as the encoding. + """ + + # A bit for every candidate + CANDIDATE_UTF_8_SIG = 1 + CANDIDATE_UTF_16_AS_LE = 2 + CANDIDATE_UTF_16_AS_BE = 4 + CANDIDATE_UTF_16_LE = 8 + CANDIDATE_UTF_16_BE = 16 + CANDIDATE_UTF_32_AS_LE = 32 + CANDIDATE_UTF_32_AS_BE = 64 + CANDIDATE_UTF_32_LE = 128 + CANDIDATE_UTF_32_BE = 256 + CANDIDATE_CHARSET = 512 + + candidates = 1023 # all candidates + + li = len(input) + if li>=1: + # Check first byte + c = input[0] + if c != "\xef": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "\xff": + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE) + if c != "\xfe": + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != "@": + candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET) + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE) + if li>=2: + # Check second byte + c = input[1] + if c != "\xbb": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "\xfe": + candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE) + if c != "\xff": + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != "@": + candidates &= ~CANDIDATE_UTF_16_BE + if c != "c": + candidates &= ~CANDIDATE_CHARSET + if li>=3: + # Check third byte + c = input[2] + if c != "\xbf": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "c": + candidates &= ~CANDIDATE_UTF_16_LE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != "\xfe": + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != "h": + candidates &= ~CANDIDATE_CHARSET + if li>=4: + # Check fourth byte + c = input[3] + if input[2:4] == "\x00\x00": + candidates &= ~CANDIDATE_UTF_16_AS_LE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE) + if c != "\xff": + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != "@": + candidates &= ~CANDIDATE_UTF_32_BE + if c != "a": + candidates &= ~CANDIDATE_CHARSET + if candidates == 0: + return ("utf-8", False) + if not (candidates & (candidates-1)): # only one candidate remaining + if candidates == CANDIDATE_UTF_8_SIG and li >= 3: + return ("utf-8-sig", True) + elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_LE and li >= 4: + return ("utf-16-le", False) + elif candidates == CANDIDATE_UTF_16_BE and li >= 2: + return ("utf-16-be", False) + elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_LE and li >= 4: + return ("utf-32-le", False) + elif candidates == CANDIDATE_UTF_32_BE and li >= 4: + return ("utf-32-be", False) + elif candidates == CANDIDATE_CHARSET and li >= 4: + prefix = '@charset "' + if input[:len(prefix)] == prefix: + pos = input.find('"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + # if this is the last call, and we haven't determined an encoding yet, + # we default to UTF-8 + if final: + return ("utf-8", False) + return (None, False) # dont' know yet + + +def detectencoding_unicode(input, final=False): + """ + Detect the encoding of the unicode string ``input``, which contains the + beginning of a CSS file. The encoding is detected from the charset rule + at the beginning of ``input``. If there is no charset rule, ``"utf-8"`` + will be returned. + + If the encoding can't be detected yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. If + ``final`` is true, ``detectencoding_unicode()`` will never return ``None``. + """ + prefix = u'@charset "' + if input.startswith(prefix): + pos = input.find(u'"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + elif final or not prefix.startswith(input): + # if this is the last call, and we haven't determined an encoding yet, + # (or the string definitely doesn't start with prefix) we default to UTF-8 + return ("utf-8", False) + return (None, False) # don't know yet + + +def _fixencoding(input, encoding, final=False): + """ + Replace the name of the encoding in the charset rule at the beginning of + ``input`` with ``encoding``. If ``input`` doesn't starts with a charset + rule, ``input`` will be returned unmodified. + + If the encoding can't be found yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. + If ``final`` is true, ``_fixencoding()`` will never return ``None``. + """ + prefix = u'@charset "' + if len(input) > len(prefix): + if input.startswith(prefix): + pos = input.find(u'"', len(prefix)) + if pos >= 0: + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = u"utf-8" + return prefix + encoding + input[pos:] + # we haven't seen the end of the encoding name yet => fall through + else: + return input # doesn't start with prefix, so nothing to fix + elif not prefix.startswith(input) or final: + # can't turn out to be a @charset rule later (or there is no "later") + return input + if final: + return input + return None # don't know yet + + +def decode(input, errors="strict", encoding=None, force=True): + if encoding is None or not force: + (_encoding, explicit) = detectencoding_str(input, True) + if _encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not force) or encoding is None: # Take the encoding from the input + encoding = _encoding + (input, consumed) = codecs.getdecoder(encoding)(input, errors) + return (_fixencoding(input, unicode(encoding), True), consumed) + + +def encode(input, errors="strict", encoding=None): + consumed = len(input) + if encoding is None: + encoding = detectencoding_unicode(input, True)[0] + if encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + else: + input = _fixencoding(input, unicode(encoding), True) + if encoding == "css": + raise ValueError("css not allowed as encoding name") + encoder = codecs.getencoder(encoding) + return (encoder(input, errors)[0], consumed) + + +def _bytes2int(bytes): + # Helper: convert an 8 bit string into an ``int``. + i = 0 + for byte in bytes: + i = (i<<8) + ord(byte) + return i + + +def _int2bytes(i): + # Helper: convert an ``int`` into an 8-bit string. + v = [] + while i: + v.insert(0, chr(i&0xff)) + i >>= 8 + return "".join(v) + + +if hasattr(codecs, "IncrementalDecoder"): + class IncrementalDecoder(codecs.IncrementalDecoder): + def __init__(self, errors="strict", encoding=None, force=True): + self.decoder = None + self.encoding = encoding + self.force = force + codecs.IncrementalDecoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = u"".encode() + self.headerfixed = False + + def iterdecode(self, input): + for part in input: + result = self.decode(part, False) + if result: + yield result + result = self.decode("", True) + if result: + yield result + + def decode(self, input, final=False): + # We're doing basically the same as a ``BufferedIncrementalDecoder``, + # but since the buffer is only relevant until the encoding has been + # detected (in which case the buffer of the underlying codec might + # kick in), we're implementing buffering ourselves to avoid some + # overhead. + if self.decoder is None: + input = self.buffer + input + # Do we have to detect the encoding from the input? + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, final) + if encoding is None: # no encoding determined yet + self.buffer = input # retry the complete input on the next call + return u"" # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + self.buffer = "" # drop buffer, as the decoder might keep its own + decoder = codecs.getincrementaldecoder(self.encoding) + self.decoder = decoder(self._errors) + if self.headerfixed: + return self.decoder.decode(input, final) + # If we haven't fixed the header yet, + # the content of ``self.buffer`` is a ``unicode`` object + output = self.buffer + self.decoder.decode(input, final) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, unicode(encoding), final) + if newoutput is None: + # retry fixing the @charset rule (but keep the decoded stuff) + self.buffer = output + return u"" + self.headerfixed = True + return newoutput + + def reset(self): + codecs.IncrementalDecoder.reset(self) + self.decoder = None + self.buffer = u"".encode() + self.headerfixed = False + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the real decoder too + if self.decoder is not None: + self.decoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.decoder is not None: + state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate()) + else: + state = (self.encoding, self.buffer, self.headerfixed, False, None) + return ("", _bytes2int(marshal.dumps(state))) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state[1])) # ignore buffered input + self.encoding = state[0] + self.buffer = state[1] + self.headerfixed = state[2] + if state[3] is not None: + self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors) + self.decoder.setstate(state[4]) + else: + self.decoder = None + + +if hasattr(codecs, "IncrementalEncoder"): + class IncrementalEncoder(codecs.IncrementalEncoder): + def __init__(self, errors="strict", encoding=None): + self.encoder = None + self.encoding = encoding + codecs.IncrementalEncoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = u"" + + def iterencode(self, input): + for part in input: + result = self.encode(part, False) + if result: + yield result + result = self.encode(u"", True) + if result: + yield result + + def encode(self, input, final=False): + if self.encoder is None: + input = self.buffer + input + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, unicode(encoding), final) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return "" + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, final)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + info = codecs.lookup(self.encoding) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + self.encoder = info.incrementalencoder(self._errors) + self.buffer = u"" + else: + self.buffer = input + return "" + return self.encoder.encode(input, final) + + def reset(self): + codecs.IncrementalEncoder.reset(self) + self.encoder = None + self.buffer = u"" + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors ``must be done on the real encoder too + if self.encoder is not None: + self.encoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.encoder is not None: + state = (self.encoding, self.buffer, True, self.encoder.getstate()) + else: + state = (self.encoding, self.buffer, False, None) + return _bytes2int(marshal.dumps(state)) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state)) + self.encoding = state[0] + self.buffer = state[1] + if state[2] is not None: + self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors) + self.encoder.setstate(state[4]) + else: + self.encoder = None + + +class StreamWriter(codecs.StreamWriter): + def __init__(self, stream, errors="strict", encoding=None, header=False): + codecs.StreamWriter.__init__(self, stream, errors) + self.streamwriter = None + self.encoding = encoding + self._errors = errors + self.buffer = u"" + + def encode(self, input, errors='strict'): + li = len(input) + if self.streamwriter is None: + input = self.buffer + input + li = len(input) + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, unicode(encoding), False) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return ("", 0) + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, False)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + self.buffer = u"" + else: + self.buffer = input + return ("", 0) + return (self.streamwriter.encode(input, errors)[0], li) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamwriter too + if self.streamwriter is not None: + self.streamwriter.errors = errors + self._errors = errors + + errors = property(_geterrors, _seterrors) + + +class StreamReader(codecs.StreamReader): + def __init__(self, stream, errors="strict", encoding=None, force=True): + codecs.StreamReader.__init__(self, stream, errors) + self.streamreader = None + self.encoding = encoding + self.force = force + self._errors = errors + + def decode(self, input, errors='strict'): + if self.streamreader is None: + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, False) + if encoding is None: # no encoding determined yet + return (u"", 0) # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + streamreader = codecs.getreader(self.encoding) + streamreader = streamreader(self.stream, self._errors) + (output, consumed) = streamreader.decode(input, errors) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, unicode(encoding), False) + if newoutput is not None: + self.streamreader = streamreader + return (newoutput, consumed) + return (u"", 0) # we will create a new streamreader on the next call + return self.streamreader.decode(input, errors) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamreader too + if self.streamreader is not None: + self.streamreader.errors = errors + self._errors = errors + + errors = property(_geterrors, _seterrors) + + +if hasattr(codecs, "CodecInfo"): + # We're running on Python 2.5 or better + def search_function(name): + if name == "css": + return codecs.CodecInfo( + name="css", + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) +else: + # If we're running on Python 2.4, define the utf-8-sig codec here + def utf8sig_encode(input, errors='strict'): + return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input)) + + def utf8sig_decode(input, errors='strict'): + prefix = 0 + if input[:3] == codecs.BOM_UTF8: + input = input[3:] + prefix = 3 + (output, consumed) = codecs.utf_8_decode(input, errors, True) + return (output, consumed+prefix) + + class UTF8SigStreamWriter(codecs.StreamWriter): + def reset(self): + codecs.StreamWriter.reset(self) + try: + del self.encode + except AttributeError: + pass + + def encode(self, input, errors='strict'): + self.encode = codecs.utf_8_encode + return utf8sig_encode(input, errors) + + class UTF8SigStreamReader(codecs.StreamReader): + def reset(self): + codecs.StreamReader.reset(self) + try: + del self.decode + except AttributeError: + pass + + def decode(self, input, errors='strict'): + if len(input) < 3 and codecs.BOM_UTF8.startswith(input): + # not enough data to decide if this is a BOM + # => try again on the next call + return (u"", 0) + self.decode = codecs.utf_8_decode + return utf8sig_decode(input, errors) + + def search_function(name): + import encodings + name = encodings.normalize_encoding(name) + if name == "css": + return (encode, decode, StreamReader, StreamWriter) + elif name == "utf_8_sig": + return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter) + + +codecs.register(search_function) + + +# Error handler for CSS escaping + +def cssescape(exc): + if not isinstance(exc, UnicodeEncodeError): + raise TypeError("don't know how to handle %r" % exc) + return (u"".join(u"\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end) + +codecs.register_error("cssescape", cssescape) diff --git a/libs/cssutils/_codec3.py b/libs/cssutils/_codec3.py new file mode 100755 index 00000000..878a7b29 --- /dev/null +++ b/libs/cssutils/_codec3.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import sys +import codecs +import marshal + +# We're using bits to store all possible candidate encodings (or variants, i.e. +# we have two bits for the variants of UTF-16 and two for the +# variants of UTF-32). +# +# Prefixes for various CSS encodings +# UTF-8-SIG xEF xBB xBF +# UTF-16 (LE) xFF xFE ~x00|~x00 +# UTF-16 (BE) xFE xFF +# UTF-16-LE @ x00 @ x00 +# UTF-16-BE x00 @ +# UTF-32 (LE) xFF xFE x00 x00 +# UTF-32 (BE) x00 x00 xFE xFF +# UTF-32-LE @ x00 x00 x00 +# UTF-32-BE x00 x00 x00 @ +# CHARSET @ c h a ... + + +def chars(bytestring): + return ''.join(chr(byte) for byte in bytestring) + + +def detectencoding_str(input, final=False): + """ + Detect the encoding of the byte string ``input``, which contains the + beginning of a CSS file. This function returns the detected encoding (or + ``None`` if it hasn't got enough data), and a flag that indicates whether + that encoding has been detected explicitely or implicitely. To detect the + encoding the first few bytes are used (or if ``input`` is ASCII compatible + and starts with a charset rule the encoding name from the rule). "Explicit" + detection means that the bytes start with a BOM or a charset rule. + + If the encoding can't be detected yet, ``None`` is returned as the encoding. + ``final`` specifies whether more data will be available in later calls or + not. If ``final`` is true, ``detectencoding_str()`` will never return + ``None`` as the encoding. + """ + + # A bit for every candidate + CANDIDATE_UTF_8_SIG = 1 + CANDIDATE_UTF_16_AS_LE = 2 + CANDIDATE_UTF_16_AS_BE = 4 + CANDIDATE_UTF_16_LE = 8 + CANDIDATE_UTF_16_BE = 16 + CANDIDATE_UTF_32_AS_LE = 32 + CANDIDATE_UTF_32_AS_BE = 64 + CANDIDATE_UTF_32_LE = 128 + CANDIDATE_UTF_32_BE = 256 + CANDIDATE_CHARSET = 512 + + candidates = 1023 # all candidates + + #input = chars(input) + li = len(input) + if li>=1: + # Check first byte + c = input[0] + if c != b"\xef"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"\xff"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE) + if c != b"\xfe"[0]: + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != b"@"[0]: + candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET) + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE) + if li>=2: + # Check second byte + c = input[1] + if c != b"\xbb"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"\xfe"[0]: + candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE) + if c != b"\xff"[0]: + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != b"@"[0]: + candidates &= ~CANDIDATE_UTF_16_BE + if c != b"c"[0]: + candidates &= ~CANDIDATE_CHARSET + if li>=3: + # Check third byte + c = input[2] + if c != b"\xbf"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"c"[0]: + candidates &= ~CANDIDATE_UTF_16_LE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != b"\xfe"[0]: + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != b"h"[0]: + candidates &= ~CANDIDATE_CHARSET + if li>=4: + # Check fourth byte + c = input[3] + if input[2:4] == b"\x00\x00"[0:2]: + candidates &= ~CANDIDATE_UTF_16_AS_LE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE) + if c != b"\xff"[0]: + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != b"@"[0]: + candidates &= ~CANDIDATE_UTF_32_BE + if c != b"a"[0]: + candidates &= ~CANDIDATE_CHARSET + if candidates == 0: + return ("utf-8", False) + if not (candidates & (candidates-1)): # only one candidate remaining + if candidates == CANDIDATE_UTF_8_SIG and li >= 3: + return ("utf-8-sig", True) + elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_LE and li >= 4: + return ("utf-16-le", False) + elif candidates == CANDIDATE_UTF_16_BE and li >= 2: + return ("utf-16-be", False) + elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_LE and li >= 4: + return ("utf-32-le", False) + elif candidates == CANDIDATE_UTF_32_BE and li >= 4: + return ("utf-32-be", False) + elif candidates == CANDIDATE_CHARSET and li >= 4: + prefix = '@charset "' + charsinput = chars(input) + if charsinput[:len(prefix)] == prefix: + pos = charsinput.find('"', len(prefix)) + if pos >= 0: + # TODO: return str and not bytes! + return (charsinput[len(prefix):pos], True) + # if this is the last call, and we haven't determined an encoding yet, + # we default to UTF-8 + if final: + return ("utf-8", False) + return (None, False) # dont' know yet + + +def detectencoding_unicode(input, final=False): + """ + Detect the encoding of the unicode string ``input``, which contains the + beginning of a CSS file. The encoding is detected from the charset rule + at the beginning of ``input``. If there is no charset rule, ``"utf-8"`` + will be returned. + + If the encoding can't be detected yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. If + ``final`` is true, ``detectencoding_unicode()`` will never return ``None``. + """ + prefix = '@charset "' + if input.startswith(prefix): + pos = input.find('"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + elif final or not prefix.startswith(input): + # if this is the last call, and we haven't determined an encoding yet, + # (or the string definitely doesn't start with prefix) we default to UTF-8 + return ("utf-8", False) + return (None, False) # don't know yet + + +def _fixencoding(input, encoding, final=False): + """ + Replace the name of the encoding in the charset rule at the beginning of + ``input`` with ``encoding``. If ``input`` doesn't starts with a charset + rule, ``input`` will be returned unmodified. + + If the encoding can't be found yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. + If ``final`` is true, ``_fixencoding()`` will never return ``None``. + """ + prefix = '@charset "' + if len(input) > len(prefix): + if input.startswith(prefix): + pos = input.find('"', len(prefix)) + if pos >= 0: + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + return prefix + encoding + input[pos:] + # we haven't seen the end of the encoding name yet => fall through + else: + return input # doesn't start with prefix, so nothing to fix + elif not prefix.startswith(input) or final: + # can't turn out to be a @charset rule later (or there is no "later") + return input + if final: + return input + return None # don't know yet + + +def decode(input, errors="strict", encoding=None, force=True): + try: + # py 3 only, memory?! object to bytes + input = input.tobytes() + except AttributeError as e: + pass + + if encoding is None or not force: + (_encoding, explicit) = detectencoding_str(input, True) + if _encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not force) or encoding is None: # Take the encoding from the input + encoding = _encoding + + # NEEDS: change in parse.py (str to bytes!) + (input, consumed) = codecs.getdecoder(encoding)(input, errors) + return (_fixencoding(input, str(encoding), True), consumed) + + +def encode(input, errors="strict", encoding=None): + consumed = len(input) + if encoding is None: + encoding = detectencoding_unicode(input, True)[0] + if encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + else: + input = _fixencoding(input, str(encoding), True) + if encoding == "css": + raise ValueError("css not allowed as encoding name") + encoder = codecs.getencoder(encoding) + return (encoder(input, errors)[0], consumed) + + +def _bytes2int(bytes): + # Helper: convert an 8 bit string into an ``int``. + i = 0 + for byte in bytes: + i = (i<<8) + ord(byte) + return i + + +def _int2bytes(i): + # Helper: convert an ``int`` into an 8-bit string. + v = [] + while i: + v.insert(0, chr(i&0xff)) + i >>= 8 + return "".join(v) + + +if hasattr(codecs, "IncrementalDecoder"): + class IncrementalDecoder(codecs.IncrementalDecoder): + def __init__(self, errors="strict", encoding=None, force=True): + self.decoder = None + self.encoding = encoding + self.force = force + codecs.IncrementalDecoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = b"" + self.headerfixed = False + + def iterdecode(self, input): + for part in input: + result = self.decode(part, False) + if result: + yield result + result = self.decode("", True) + if result: + yield result + + def decode(self, input, final=False): + # We're doing basically the same as a ``BufferedIncrementalDecoder``, + # but since the buffer is only relevant until the encoding has been + # detected (in which case the buffer of the underlying codec might + # kick in), we're implementing buffering ourselves to avoid some + # overhead. + if self.decoder is None: + input = self.buffer + input + # Do we have to detect the encoding from the input? + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, final) + if encoding is None: # no encoding determined yet + self.buffer = input # retry the complete input on the next call + return "" # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + self.buffer = "" # drop buffer, as the decoder might keep its own + decoder = codecs.getincrementaldecoder(self.encoding) + self.decoder = decoder(self._errors) + if self.headerfixed: + return self.decoder.decode(input, final) + # If we haven't fixed the header yet, + # the content of ``self.buffer`` is a ``unicode`` object + output = self.buffer + self.decoder.decode(input, final) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, str(encoding), final) + if newoutput is None: + # retry fixing the @charset rule (but keep the decoded stuff) + self.buffer = output + return "" + self.headerfixed = True + return newoutput + + def reset(self): + codecs.IncrementalDecoder.reset(self) + self.decoder = None + self.buffer = b"" + self.headerfixed = False + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the real decoder too + if self.decoder is not None: + self.decoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.decoder is not None: + state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate()) + else: + state = (self.encoding, self.buffer, self.headerfixed, False, None) + return ("", _bytes2int(marshal.dumps(state))) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state[1])) # ignore buffered input + self.encoding = state[0] + self.buffer = state[1] + self.headerfixed = state[2] + if state[3] is not None: + self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors) + self.decoder.setstate(state[4]) + else: + self.decoder = None + + +if hasattr(codecs, "IncrementalEncoder"): + class IncrementalEncoder(codecs.IncrementalEncoder): + def __init__(self, errors="strict", encoding=None): + self.encoder = None + self.encoding = encoding + codecs.IncrementalEncoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = "" + + def iterencode(self, input): + for part in input: + result = self.encode(part, False) + if result: + yield result + result = self.encode("", True) + if result: + yield result + + def encode(self, input, final=False): + if self.encoder is None: + input = self.buffer + input + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, str(encoding), final) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return "" + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, final)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + info = codecs.lookup(self.encoding) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + self.encoder = info.incrementalencoder(self._errors) + self.buffer = "" + else: + self.buffer = input + return "" + return self.encoder.encode(input, final) + + def reset(self): + codecs.IncrementalEncoder.reset(self) + self.encoder = None + self.buffer = "" + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors ``must be done on the real encoder too + if self.encoder is not None: + self.encoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.encoder is not None: + state = (self.encoding, self.buffer, True, self.encoder.getstate()) + else: + state = (self.encoding, self.buffer, False, None) + return _bytes2int(marshal.dumps(state)) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state)) + self.encoding = state[0] + self.buffer = state[1] + if state[2] is not None: + self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors) + self.encoder.setstate(state[4]) + else: + self.encoder = None + + +class StreamWriter(codecs.StreamWriter): + def __init__(self, stream, errors="strict", encoding=None, header=False): + codecs.StreamWriter.__init__(self, stream, errors) + self.streamwriter = None + self.encoding = encoding + self._errors = errors + self.buffer = "" + + def encode(self, input, errors='strict'): + li = len(input) + if self.streamwriter is None: + input = self.buffer + input + li = len(input) + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, str(encoding), False) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return ("", 0) + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, False)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + self.buffer = "" + else: + self.buffer = input + return ("", 0) + return (self.streamwriter.encode(input, errors)[0], li) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamwriter too + try: + if self.streamwriter is not None: + self.streamwriter.errors = errors + except AttributeError as e: + # TODO: py3 only exception? + pass + + self._errors = errors + errors = property(_geterrors, _seterrors) + + +class StreamReader(codecs.StreamReader): + def __init__(self, stream, errors="strict", encoding=None, force=True): + codecs.StreamReader.__init__(self, stream, errors) + self.streamreader = None + self.encoding = encoding + self.force = force + self._errors = errors + + def decode(self, input, errors='strict'): + if self.streamreader is None: + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, False) + if encoding is None: # no encoding determined yet + return ("", 0) # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + streamreader = codecs.getreader(self.encoding) + streamreader = streamreader(self.stream, self._errors) + (output, consumed) = streamreader.decode(input, errors) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, str(encoding), False) + if newoutput is not None: + self.streamreader = streamreader + return (newoutput, consumed) + return ("", 0) # we will create a new streamreader on the next call + return self.streamreader.decode(input, errors) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamreader too + try: + if self.streamreader is not None: + self.streamreader.errors = errors + except AttributeError as e: + # TODO: py3 only exception? + pass + + self._errors = errors + errors = property(_geterrors, _seterrors) + + +if hasattr(codecs, "CodecInfo"): + # We're running on Python 2.5 or better + def search_function(name): + if name == "css": + return codecs.CodecInfo( + name="css", + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) +else: + # If we're running on Python 2.4, define the utf-8-sig codec here + def utf8sig_encode(input, errors='strict'): + return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input)) + + def utf8sig_decode(input, errors='strict'): + prefix = 0 + if input[:3] == codecs.BOM_UTF8: + input = input[3:] + prefix = 3 + (output, consumed) = codecs.utf_8_decode(input, errors, True) + return (output, consumed+prefix) + + class UTF8SigStreamWriter(codecs.StreamWriter): + def reset(self): + codecs.StreamWriter.reset(self) + try: + del self.encode + except AttributeError: + pass + + def encode(self, input, errors='strict'): + self.encode = codecs.utf_8_encode + return utf8sig_encode(input, errors) + + class UTF8SigStreamReader(codecs.StreamReader): + def reset(self): + codecs.StreamReader.reset(self) + try: + del self.decode + except AttributeError: + pass + + def decode(self, input, errors='strict'): + if len(input) < 3 and codecs.BOM_UTF8.startswith(input): + # not enough data to decide if this is a BOM + # => try again on the next call + return ("", 0) + self.decode = codecs.utf_8_decode + return utf8sig_decode(input, errors) + + def search_function(name): + import encodings + name = encodings.normalize_encoding(name) + if name == "css": + return (encode, decode, StreamReader, StreamWriter) + elif name == "utf_8_sig": + return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter) + + +codecs.register(search_function) + + +# Error handler for CSS escaping + +def cssescape(exc): + if not isinstance(exc, UnicodeEncodeError): + raise TypeError("don't know how to handle %r" % exc) + return ("".join("\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end) + +codecs.register_error("cssescape", cssescape) diff --git a/libs/cssutils/_fetch.py b/libs/cssutils/_fetch.py new file mode 100755 index 00000000..a9138fe3 --- /dev/null +++ b/libs/cssutils/_fetch.py @@ -0,0 +1,44 @@ +"""Default URL reading functions""" +__all__ = ['_defaultFetcher'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $' + +import cssutils +from cssutils import VERSION +import encutils +import errorhandler +import urllib2 + +log = errorhandler.ErrorHandler() + +def _defaultFetcher(url): + """Retrieve data from ``url``. cssutils default implementation of fetch + URL function. + + Returns ``(encoding, string)`` or ``None`` + """ + try: + request = urllib2.Request(url) + request.add_header('User-agent', + 'cssutils %s (http://www.cthedot.de/cssutils/)' % VERSION) + res = urllib2.urlopen(request) + except OSError, e: + # e.g if file URL and not found + log.warn(e, error=OSError) + except (OSError, ValueError), e: + # invalid url, e.g. "1" + log.warn(u'ValueError, %s' % e.args[0], error=ValueError) + except urllib2.HTTPError, e: + # http error, e.g. 404, e can be raised + log.warn(u'HTTPError opening url=%s: %s %s' % + (url, e.code, e.msg), error=e) + except urllib2.URLError, e: + # URLError like mailto: or other IO errors, e can be raised + log.warn(u'URLError, %s' % e.reason, error=e) + else: + if res: + mimeType, encoding = encutils.getHTTPInfo(res) + if mimeType != u'text/css': + log.error(u'Expected "text/css" mime type for url=%r but found: %r' % + (url, mimeType), error=ValueError) + return encoding, res.read() diff --git a/libs/cssutils/_fetchgae.py b/libs/cssutils/_fetchgae.py new file mode 100755 index 00000000..ef91d412 --- /dev/null +++ b/libs/cssutils/_fetchgae.py @@ -0,0 +1,68 @@ +"""GAE specific URL reading functions""" +__all__ = ['_defaultFetcher'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $' + +# raises ImportError of not on GAE +from google.appengine.api import urlfetch +import cgi +import errorhandler +import util + +log = errorhandler.ErrorHandler() + +def _defaultFetcher(url): + """ + uses GoogleAppEngine (GAE) + fetch(url, payload=None, method=GET, headers={}, allow_truncated=False) + + Response + content + The body content of the response. + content_was_truncated + True if the allow_truncated parameter to fetch() was True and + the response exceeded the maximum response size. In this case, + the content attribute contains the truncated response. + status_code + The HTTP status code. + headers + The HTTP response headers, as a mapping of names to values. + + Exceptions + exception InvalidURLError() + The URL of the request was not a valid URL, or it used an + unsupported method. Only http and https URLs are supported. + exception DownloadError() + There was an error retrieving the data. + + This exception is not raised if the server returns an HTTP + error code: In that case, the response data comes back intact, + including the error code. + + exception ResponseTooLargeError() + The response data exceeded the maximum allowed size, and the + allow_truncated parameter passed to fetch() was False. + """ + #from google.appengine.api import urlfetch + try: + r = urlfetch.fetch(url, method=urlfetch.GET) + except urlfetch.Error, e: + log.warn(u'Error opening url=%r: %s' % (url, e), + error=IOError) + else: + if r.status_code == 200: + # find mimetype and encoding + mimetype = 'application/octet-stream' + try: + mimetype, params = cgi.parse_header(r.headers['content-type']) + encoding = params['charset'] + except KeyError: + encoding = None + if mimetype != u'text/css': + log.error(u'Expected "text/css" mime type for url %r but found: %r' % + (url, mimetype), error=ValueError) + return encoding, r.content + else: + # TODO: 301 etc + log.warn(u'Error opening url=%r: HTTP status %s' % + (url, r.status_code), error=IOError) diff --git a/libs/cssutils/codec.py b/libs/cssutils/codec.py new file mode 100755 index 00000000..c694e1fc --- /dev/null +++ b/libs/cssutils/codec.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import sys + +if sys.version_info < (3,): + from _codec2 import * + # for tests + from _codec2 import _fixencoding +else: + from _codec3 import * + # for tests + from _codec3 import _fixencoding diff --git a/libs/cssutils/css/__init__.py b/libs/cssutils/css/__init__.py new file mode 100755 index 00000000..4cc4a904 --- /dev/null +++ b/libs/cssutils/css/__init__.py @@ -0,0 +1,80 @@ +"""Implements Document Object Model Level 2 CSS +http://www.w3.org/TR/2000/PR-DOM-Level-2-Style-20000927/css.html + +currently implemented + - CSSStyleSheet + - CSSRuleList + - CSSRule + - CSSComment (cssutils addon) + - CSSCharsetRule + - CSSFontFaceRule + - CSSImportRule + - CSSMediaRule + - CSSNamespaceRule (WD) + - CSSPageRule + - CSSStyleRule + - CSSUnkownRule + - Selector and SelectorList + - CSSStyleDeclaration + - CSS2Properties + - CSSValue + - CSSPrimitiveValue + - CSSValueList + - CSSVariablesRule + - CSSVariablesDeclaration + +todo + - RGBColor, Rect, Counter +""" +__all__ = [ + 'CSSStyleSheet', + 'CSSRuleList', + 'CSSRule', + 'CSSComment', + 'CSSCharsetRule', + 'CSSFontFaceRule' + 'CSSImportRule', + 'CSSMediaRule', + 'CSSNamespaceRule', + 'CSSPageRule', + 'MarginRule', + 'CSSStyleRule', + 'CSSUnknownRule', + 'CSSVariablesRule', + 'CSSVariablesDeclaration', + 'Selector', 'SelectorList', + 'CSSStyleDeclaration', 'Property', + #'CSSValue', 'CSSPrimitiveValue', 'CSSValueList' + 'PropertyValue', + 'Value', + 'ColorValue', + 'DimensionValue', + 'URIValue', + 'CSSFunction', + 'CSSVariable', + 'MSValue' + ] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstylesheet import * +from cssrulelist import * +from cssrule import * +from csscomment import * +from csscharsetrule import * +from cssfontfacerule import * +from cssimportrule import * +from cssmediarule import * +from cssnamespacerule import * +from csspagerule import * +from marginrule import * +from cssstylerule import * +from cssvariablesrule import * +from cssunknownrule import * +from selector import * +from selectorlist import * +from cssstyledeclaration import * +from cssvariablesdeclaration import * +from property import * +#from cssvalue import * +from value import * diff --git a/libs/cssutils/css/colors.py b/libs/cssutils/css/colors.py new file mode 100755 index 00000000..5f8dd899 --- /dev/null +++ b/libs/cssutils/css/colors.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +Built from something like this: + + print [ + ( + row[2].text_content().strip(), + eval(row[4].text_content().strip()) + ) + for row in lxml.html.parse('http://www.w3.org/TR/css3-color/') + .xpath("//*[@class='colortable']//tr[position()>1]") + ] + +by Simon Sapin +""" + +COLORS = { + 'transparent': (0, 0, 0, 0.0), + + 'black': (0, 0, 0, 1.0), + 'silver': (192, 192, 192, 1.0), + 'gray': (128, 128, 128, 1.0), + 'white': (255, 255, 255, 1.0), + 'maroon': (128, 0, 0, 1.0), + 'red': (255, 0, 0, 1.0), + 'purple': (128, 0, 128, 1.0), + 'fuchsia': (255, 0, 255, 1.0), + 'green': (0, 128, 0, 1.0), + 'lime': (0, 255, 0, 1.0), + 'olive': (128, 128, 0, 1.0), + 'yellow': (255, 255, 0, 1.0), + 'navy': (0, 0, 128, 1.0), + 'blue': (0, 0, 255, 1.0), + 'teal': (0, 128, 128, 1.0), + + 'aqua': (0, 255, 255, 1.0), + 'aliceblue': (240, 248, 255, 1.0), + 'antiquewhite': (250, 235, 215, 1.0), + 'aqua': (0, 255, 255, 1.0), + 'aquamarine': (127, 255, 212, 1.0), + 'azure': (240, 255, 255, 1.0), + 'beige': (245, 245, 220, 1.0), + 'bisque': (255, 228, 196, 1.0), + 'black': (0, 0, 0, 1.0), + 'blanchedalmond': (255, 235, 205, 1.0), + 'blue': (0, 0, 255, 1.0), + 'blueviolet': (138, 43, 226, 1.0), + 'brown': (165, 42, 42, 1.0), + 'burlywood': (222, 184, 135, 1.0), + 'cadetblue': (95, 158, 160, 1.0), + 'chartreuse': (127, 255, 0, 1.0), + 'chocolate': (210, 105, 30, 1.0), + 'coral': (255, 127, 80, 1.0), + 'cornflowerblue': (100, 149, 237, 1.0), + 'cornsilk': (255, 248, 220, 1.0), + 'crimson': (220, 20, 60, 1.0), + 'cyan': (0, 255, 255, 1.0), + 'darkblue': (0, 0, 139, 1.0), + 'darkcyan': (0, 139, 139, 1.0), + 'darkgoldenrod': (184, 134, 11, 1.0), + 'darkgray': (169, 169, 169, 1.0), + 'darkgreen': (0, 100, 0, 1.0), + 'darkgrey': (169, 169, 169, 1.0), + 'darkkhaki': (189, 183, 107, 1.0), + 'darkmagenta': (139, 0, 139, 1.0), + 'darkolivegreen': (85, 107, 47, 1.0), + 'darkorange': (255, 140, 0, 1.0), + 'darkorchid': (153, 50, 204, 1.0), + 'darkred': (139, 0, 0, 1.0), + 'darksalmon': (233, 150, 122, 1.0), + 'darkseagreen': (143, 188, 143, 1.0), + 'darkslateblue': (72, 61, 139, 1.0), + 'darkslategray': (47, 79, 79, 1.0), + 'darkslategrey': (47, 79, 79, 1.0), + 'darkturquoise': (0, 206, 209, 1.0), + 'darkviolet': (148, 0, 211, 1.0), + 'deeppink': (255, 20, 147, 1.0), + 'deepskyblue': (0, 191, 255, 1.0), + 'dimgray': (105, 105, 105, 1.0), + 'dimgrey': (105, 105, 105, 1.0), + 'dodgerblue': (30, 144, 255, 1.0), + 'firebrick': (178, 34, 34, 1.0), + 'floralwhite': (255, 250, 240, 1.0), + 'forestgreen': (34, 139, 34, 1.0), + 'fuchsia': (255, 0, 255, 1.0), + 'gainsboro': (220, 220, 220, 1.0), + 'ghostwhite': (248, 248, 255, 1.0), + 'gold': (255, 215, 0, 1.0), + 'goldenrod': (218, 165, 32, 1.0), + 'gray': (128, 128, 128, 1.0), + 'green': (0, 128, 0, 1.0), + 'greenyellow': (173, 255, 47, 1.0), + 'grey': (128, 128, 128, 1.0), + 'honeydew': (240, 255, 240, 1.0), + 'hotpink': (255, 105, 180, 1.0), + 'indianred': (205, 92, 92, 1.0), + 'indigo': (75, 0, 130, 1.0), + 'ivory': (255, 255, 240, 1.0), + 'khaki': (240, 230, 140, 1.0), + 'lavender': (230, 230, 250, 1.0), + 'lavenderblush': (255, 240, 245, 1.0), + 'lawngreen': (124, 252, 0, 1.0), + 'lemonchiffon': (255, 250, 205, 1.0), + 'lightblue': (173, 216, 230, 1.0), + 'lightcoral': (240, 128, 128, 1.0), + 'lightcyan': (224, 255, 255, 1.0), + 'lightgoldenrodyellow': (250, 250, 210, 1.0), + 'lightgray': (211, 211, 211, 1.0), + 'lightgreen': (144, 238, 144, 1.0), + 'lightgrey': (211, 211, 211, 1.0), + 'lightpink': (255, 182, 193, 1.0), + 'lightsalmon': (255, 160, 122, 1.0), + 'lightseagreen': (32, 178, 170, 1.0), + 'lightskyblue': (135, 206, 250, 1.0), + 'lightslategray': (119, 136, 153, 1.0), + 'lightslategrey': (119, 136, 153, 1.0), + 'lightsteelblue': (176, 196, 222, 1.0), + 'lightyellow': (255, 255, 224, 1.0), + 'lime': (0, 255, 0, 1.0), + 'limegreen': (50, 205, 50, 1.0), + 'linen': (250, 240, 230, 1.0), + 'magenta': (255, 0, 255, 1.0), + 'maroon': (128, 0, 0, 1.0), + 'mediumaquamarine': (102, 205, 170, 1.0), + 'mediumblue': (0, 0, 205, 1.0), + 'mediumorchid': (186, 85, 211, 1.0), + 'mediumpurple': (147, 112, 219, 1.0), + 'mediumseagreen': (60, 179, 113, 1.0), + 'mediumslateblue': (123, 104, 238, 1.0), + 'mediumspringgreen': (0, 250, 154, 1.0), + 'mediumturquoise': (72, 209, 204, 1.0), + 'mediumvioletred': (199, 21, 133, 1.0), + 'midnightblue': (25, 25, 112, 1.0), + 'mintcream': (245, 255, 250, 1.0), + 'mistyrose': (255, 228, 225, 1.0), + 'moccasin': (255, 228, 181, 1.0), + 'navajowhite': (255, 222, 173, 1.0), + 'navy': (0, 0, 128, 1.0), + 'oldlace': (253, 245, 230, 1.0), + 'olive': (128, 128, 0, 1.0), + 'olivedrab': (107, 142, 35, 1.0), + 'orange': (255, 165, 0, 1.0), + 'orangered': (255, 69, 0, 1.0), + 'orchid': (218, 112, 214, 1.0), + 'palegoldenrod': (238, 232, 170, 1.0), + 'palegreen': (152, 251, 152, 1.0), + 'paleturquoise': (175, 238, 238, 1.0), + 'palevioletred': (219, 112, 147, 1.0), + 'papayawhip': (255, 239, 213, 1.0), + 'peachpuff': (255, 218, 185, 1.0), + 'peru': (205, 133, 63, 1.0), + 'pink': (255, 192, 203, 1.0), + 'plum': (221, 160, 221, 1.0), + 'powderblue': (176, 224, 230, 1.0), + 'purple': (128, 0, 128, 1.0), + 'red': (255, 0, 0, 1.0), + 'rosybrown': (188, 143, 143, 1.0), + 'royalblue': (65, 105, 225, 1.0), + 'saddlebrown': (139, 69, 19, 1.0), + 'salmon': (250, 128, 114, 1.0), + 'sandybrown': (244, 164, 96, 1.0), + 'seagreen': (46, 139, 87, 1.0), + 'seashell': (255, 245, 238, 1.0), + 'sienna': (160, 82, 45, 1.0), + 'silver': (192, 192, 192, 1.0), + 'skyblue': (135, 206, 235, 1.0), + 'slateblue': (106, 90, 205, 1.0), + 'slategray': (112, 128, 144, 1.0), + 'slategrey': (112, 128, 144, 1.0), + 'snow': (255, 250, 250, 1.0), + 'springgreen': (0, 255, 127, 1.0), + 'steelblue': (70, 130, 180, 1.0), + 'tan': (210, 180, 140, 1.0), + 'teal': (0, 128, 128, 1.0), + 'thistle': (216, 191, 216, 1.0), + 'tomato': (255, 99, 71, 1.0), + 'turquoise': (64, 224, 208, 1.0), + 'violet': (238, 130, 238, 1.0), + 'wheat': (245, 222, 179, 1.0), + 'white': (255, 255, 255, 1.0), + 'whitesmoke': (245, 245, 245, 1.0), + 'yellow': (255, 255, 0, 1.0), + 'yellowgreen': (154, 205, 50, 1.0), +} diff --git a/libs/cssutils/css/csscharsetrule.py b/libs/cssutils/css/csscharsetrule.py new file mode 100755 index 00000000..fa6b7c4d --- /dev/null +++ b/libs/cssutils/css/csscharsetrule.py @@ -0,0 +1,159 @@ +"""CSSCharsetRule implements DOM Level 2 CSS CSSCharsetRule.""" +__all__ = ['CSSCharsetRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import codecs +import cssrule +import cssutils +import xml.dom + +class CSSCharsetRule(cssrule.CSSRule): + """ + The CSSCharsetRule interface represents an @charset rule in a CSS style + sheet. The value of the encoding attribute does not affect the encoding + of text data in the DOM objects; this encoding is always UTF-16 + (also in Python?). After a stylesheet is loaded, the value of the + encoding attribute is the value found in the @charset rule. If there + was no @charset in the original document, then no CSSCharsetRule is + created. The value of the encoding attribute may also be used as a hint + for the encoding used on serialization of the style sheet. + + The value of the @charset rule (and therefore of the CSSCharsetRule) + may not correspond to the encoding the document actually came in; + character encoding information e.g. in an HTTP header, has priority + (see CSS document representation) but this is not reflected in the + CSSCharsetRule. + + This rule is not really needed anymore as setting + :attr:`CSSStyleSheet.encoding` is much easier. + + Format:: + + charsetrule: + CHARSET_SYM S* STRING S* ';' + + BUT: Only valid format is (single space, double quotes!):: + + @charset "ENCODING"; + """ + def __init__(self, encoding=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param encoding: + a valid character encoding + :param readonly: + defaults to False, not used yet + """ + super(CSSCharsetRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = '@charset' + + if encoding: + self.encoding = encoding + else: + self._encoding = None + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(encoding=%r)" % ( + self.__class__.__name__, + self.encoding) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.encoding, + id(self)) + + def _getCssText(self): + """The parsable textual representation.""" + return cssutils.ser.do_CSSCharsetRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + A parsable DOMString. + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSCharsetRule, self)._setCssText(cssText) + + wellformed = True + tokenizer = self._tokenize2(cssText) + + if self._type(self._nexttoken(tokenizer)) != self._prods.CHARSET_SYM: + wellformed = False + self._log.error(u'CSSCharsetRule must start with "@charset "', + error=xml.dom.InvalidModificationErr) + + encodingtoken = self._nexttoken(tokenizer) + encodingtype = self._type(encodingtoken) + encoding = self._stringtokenvalue(encodingtoken) + if self._prods.STRING != encodingtype or not encoding: + wellformed = False + self._log.error(u'CSSCharsetRule: no encoding found; %r.' % + self._valuestr(cssText)) + + semicolon = self._tokenvalue(self._nexttoken(tokenizer)) + EOFtype = self._type(self._nexttoken(tokenizer)) + if u';' != semicolon or EOFtype not in ('EOF', None): + wellformed = False + self._log.error(u'CSSCharsetRule: Syntax Error: %r.' % + self._valuestr(cssText)) + + if wellformed: + self.encoding = encoding + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + def _setEncoding(self, encoding): + """ + :param encoding: + a valid encoding to be used. Currently only valid Python encodings + are allowed. + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this encoding rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified encoding value has a syntax error and + is unparsable. + """ + self._checkReadonly() + tokenizer = self._tokenize2(encoding) + encodingtoken = self._nexttoken(tokenizer) + unexpected = self._nexttoken(tokenizer) + + if not encodingtoken or unexpected or\ + self._prods.IDENT != self._type(encodingtoken): + self._log.error(u'CSSCharsetRule: Syntax Error in encoding value ' + u'%r.' % encoding) + else: + try: + codecs.lookup(encoding) + except LookupError: + self._log.error(u'CSSCharsetRule: Unknown (Python) encoding %r.' + % encoding) + else: + self._encoding = encoding.lower() + + encoding = property(lambda self: self._encoding, _setEncoding, + doc=u"(DOM)The encoding information used in this @charset rule.") + + type = property(lambda self: self.CHARSET_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.encoding)) diff --git a/libs/cssutils/css/csscomment.py b/libs/cssutils/css/csscomment.py new file mode 100755 index 00000000..311df0fd --- /dev/null +++ b/libs/cssutils/css/csscomment.py @@ -0,0 +1,87 @@ +"""CSSComment is not defined in DOM Level 2 at all but a cssutils defined +class only. + +Implements CSSRule which is also extended for a CSSComment rule type. +""" +__all__ = ['CSSComment'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSComment(cssrule.CSSRule): + """ + Represents a CSS comment (cssutils only). + + Format:: + + /*...*/ + """ + def __init__(self, cssText=None, parentRule=None, + parentStyleSheet=None, readonly=False): + super(CSSComment, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self._cssText = None + if cssText: + self._setCssText(cssText) + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSComment(self) + + def _setCssText(self, cssText): + """ + :param cssText: + textual text to set or tokenlist which is not tokenized + anymore. May also be a single token for this rule + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSComment, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + + commenttoken = self._nexttoken(tokenizer) + unexpected = self._nexttoken(tokenizer) + + if not commenttoken or\ + self._type(commenttoken) != self._prods.COMMENT or\ + unexpected: + self._log.error(u'CSSComment: Not a CSSComment: %r' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + self._cssText = self._tokenvalue(commenttoken) + + cssText = property(_getCssText, _setCssText, + doc=u"The parsable textual representation of this rule.") + + type = property(lambda self: self.COMMENT, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/cssfontfacerule.py b/libs/cssutils/css/cssfontfacerule.py new file mode 100755 index 00000000..1e936066 --- /dev/null +++ b/libs/cssutils/css/cssfontfacerule.py @@ -0,0 +1,184 @@ +"""CSSFontFaceRule implements DOM Level 2 CSS CSSFontFaceRule. + +From cssutils 0.9.6 additions from CSS Fonts Module Level 3 are +added http://www.w3.org/TR/css3-fonts/. +""" +__all__ = ['CSSFontFaceRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstyledeclaration import CSSStyleDeclaration +import cssrule +import cssutils +import xml.dom + +class CSSFontFaceRule(cssrule.CSSRule): + """ + The CSSFontFaceRule interface represents a @font-face rule in a CSS + style sheet. The @font-face rule is used to hold a set of font + descriptions. + + Format:: + + font_face + : FONT_FACE_SYM S* + '{' S* declaration [ ';' S* declaration ]* '}' S* + ; + + cssutils uses a :class:`~cssutils.css.CSSStyleDeclaration` to + represent the font descriptions. For validation a specific profile + is used though were some properties have other valid values than + when used in e.g. a :class:`~cssutils.css.CSSStyleRule`. + """ + def __init__(self, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + + :param style: + CSSStyleDeclaration used to hold any font descriptions + for this CSSFontFaceRule + """ + super(CSSFontFaceRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@font-face' + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(style=%r)" % ( + self.__class__.__name__, + self.style.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.style.cssText, + self.valid, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSFontFaceRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSFontFaceRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.FONT_FACE_SYM: + self._log.error(u'CSSFontFaceRule: No CSSFontFaceRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + beforetokens, brace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + if self._tokenvalue(brace) != u'{': + ok = False + self._log.error(u'CSSFontFaceRule: No start { of style ' + u'declaration found: %r' + % self._valuestr(cssText), brace) + + # parse stuff before { which should be comments and S only + new = {'wellformed': True} + newseq = self._tempSeq() + + beforewellformed, expected = self._parse(expected=':', + seq=newseq, tokenizer=self._tokenize2(beforetokens), + productions={}) + ok = ok and beforewellformed and new['wellformed'] + + styletokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + + val, type_ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error(u'CSSFontFaceRule: No "}" after style ' + u'declaration found: %r' + % self._valuestr(cssText)) + + nonetoken = self._nexttoken(tokenizer) + if nonetoken: + ok = False + self._log.error(u'CSSFontFaceRule: Trailing content found.', + token=nonetoken) + + if 'EOF' == type_: + # add again as style needs it + styletokens.append(braceorEOFtoken) + + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + # contains probably comments only (upto ``{``) + self._setSeq(newseq) + self.style = newStyle + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setStyle(self, style): + """ + :param style: + a CSSStyleDeclaration or string + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set, " + u"a :class:`~cssutils.css.CSSStyleDeclaration`.") + + type = property(lambda self: self.FONT_FACE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + def _getValid(self): + needed = ['font-family', 'src'] + for p in self.style.getProperties(all=True): + if not p.valid: + return False + try: + needed.remove(p.name) + except ValueError: + pass + return not bool(needed) + + valid = property(_getValid, + doc=u"CSSFontFace is valid if properties `font-family` " + u"and `src` are set and all properties are valid.") + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/cssimportrule.py b/libs/cssutils/css/cssimportrule.py new file mode 100755 index 00000000..59fe2564 --- /dev/null +++ b/libs/cssutils/css/cssimportrule.py @@ -0,0 +1,396 @@ +"""CSSImportRule implements DOM Level 2 CSS CSSImportRule plus the +``name`` property from http://www.w3.org/TR/css3-cascade/#cascading.""" +__all__ = ['CSSImportRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import os +import urlparse +import xml.dom + +class CSSImportRule(cssrule.CSSRule): + """ + Represents an @import rule within a CSS style sheet. The @import rule + is used to import style rules from other style sheets. + + Format:: + + import + : IMPORT_SYM S* + [STRING|URI] S* [ medium [ COMMA S* medium]* ]? S* STRING? S* ';' S* + ; + """ + def __init__(self, href=None, mediaText=None, name=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only + + :param href: + location of the style sheet to be imported. + :param mediaText: + A list of media types for which this style sheet may be used + as a string + :param name: + Additional name of imported style sheet + """ + super(CSSImportRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@import' + self._styleSheet = None + + # string or uri used for reserialization + self.hreftype = None + + # prepare seq + seq = self._tempSeq() + seq.append(None, 'href') + #seq.append(None, 'media') + seq.append(None, 'name') + self._setSeq(seq) + + # 1. media + if mediaText: + self.media = mediaText + else: + # must be all for @import + self.media = cssutils.stylesheets.MediaList(mediaText=u'all') + # 2. name + self.name = name + # 3. href and styleSheet + self.href = href + + self._readonly = readonly + + def __repr__(self): + if self._usemedia: + mediaText = self.media.mediaText + else: + mediaText = None + return u"cssutils.css.%s(href=%r, mediaText=%r, name=%r)" % ( + self.__class__.__name__, + self.href, + self.media.mediaText, + self.name) + + def __str__(self): + if self._usemedia: + mediaText = self.media.mediaText + else: + mediaText = None + return u""\ + % (self.__class__.__name__, + self.href, + mediaText, + self.name, + id(self)) + + _usemedia = property(lambda self: self.media.mediaText not in (u'', u'all'), + doc="if self.media is used (or simply empty)") + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSImportRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + super(CSSImportRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.IMPORT_SYM: + self._log.error(u'CSSImportRule: No CSSImportRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'keyword': self._tokenvalue(attoken), + 'href': None, + 'hreftype': None, + 'media': None, + 'name': None, + 'wellformed': True + } + + def __doname(seq, token): + # called by _string or _ident + new['name'] = self._stringtokenvalue(token) + seq.append(new['name'], 'name') + return ';' + + def _string(expected, seq, token, tokenizer=None): + if 'href' == expected: + # href + new['href'] = self._stringtokenvalue(token) + new['hreftype'] = 'string' + seq.append(new['href'], 'href') + return 'media name ;' + elif 'name' in expected: + # name + return __doname(seq, token) + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected string.', token) + return expected + + def _uri(expected, seq, token, tokenizer=None): + # href + if 'href' == expected: + uri = self._uritokenvalue(token) + new['hreftype'] = 'uri' + new['href'] = uri + seq.append(new['href'], 'href') + return 'media name ;' + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected URI.', token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # medialist ending with ; which is checked upon too + if expected.startswith('media'): + mediatokens = self._tokensupto2( + tokenizer, importmediaqueryendonly=True) + mediatokens.insert(0, token) # push found token + + last = mediatokens.pop() # retrieve ; + lastval, lasttyp = self._tokenvalue(last), self._type(last) + if lastval != u';' and lasttyp not in ('EOF', + self._prods.STRING): + new['wellformed'] = False + self._log.error(u'CSSImportRule: No ";" found: %s' % + self._valuestr(cssText), token=token) + + newMedia = cssutils.stylesheets.MediaList(parentRule=self) + newMedia.mediaText = mediatokens + if newMedia.wellformed: + new['media'] = newMedia + seq.append(newMedia, 'media') + else: + new['wellformed'] = False + self._log.error(u'CSSImportRule: Invalid MediaList: %s' % + self._valuestr(cssText), token=token) + + if lasttyp == self._prods.STRING: + # name + return __doname(seq, last) + else: + return 'EOF' # ';' is token "last" + else: + new['wellformed'] = False + self._log.error(u'CSSImportRule: Unexpected ident.', token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # final ; + val = self._tokenvalue(token) + if expected.endswith(';') and u';' == val: + return 'EOF' + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected char.', token) + return expected + + # import : IMPORT_SYM S* [STRING|URI] + # S* [ medium [ ',' S* medium]* ]? ';' S* + # STRING? # see http://www.w3.org/TR/css3-cascade/#cascading + # ; + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='href', + seq=newseq, tokenizer=tokenizer, + productions={'STRING': _string, + 'URI': _uri, + 'IDENT': _ident, + 'CHAR': _char}, + new=new) + + # wellformed set by parse + ok = wellformed and new['wellformed'] + + # post conditions + if not new['href']: + ok = False + self._log.error(u'CSSImportRule: No href found: %s' % + self._valuestr(cssText)) + + if expected != 'EOF': + ok = False + self._log.error(u'CSSImportRule: No ";" found: %s' % + self._valuestr(cssText)) + + # set all + if ok: + self._setSeq(newseq) + + self.atkeyword = new['keyword'] + self.hreftype = new['hreftype'] + self.name = new['name'] + + if new['media']: + self.media = new['media'] + else: + # must be all for @import + self.media = cssutils.stylesheets.MediaList(mediaText=u'all') + + # needs new self.media + self.href = new['href'] + + cssText = property(fget=_getCssText, fset=_setCssText, + doc="(DOM) The parsable textual representation of this rule.") + + def _setHref(self, href): + # set new href + self._href = href + # update seq + for i, item in enumerate(self.seq): + val, type_ = item.value, item.type + if 'href' == type_: + self._seq[i] = (href, type_, item.line, item.col) + break + + importedSheet = cssutils.css.CSSStyleSheet(media=self.media, + ownerRule=self, + title=self.name) + self.hrefFound = False + # set styleSheet + if href and self.parentStyleSheet: + # loading errors are all catched! + + # relative href + parentHref = self.parentStyleSheet.href + if parentHref is None: + # use cwd instead + parentHref = cssutils.helper.path2url(os.getcwd()) + '/' + + fullhref = urlparse.urljoin(parentHref, self.href) + + # all possible exceptions are ignored + try: + usedEncoding, enctype, cssText = \ + self.parentStyleSheet._resolveImport(fullhref) + + if cssText is None: + # catched in next except below! + raise IOError('Cannot read Stylesheet.') + + # contentEncoding with parentStyleSheet.overrideEncoding, + # HTTP or parent + encodingOverride, encoding = None, None + + if enctype == 0: + encodingOverride = usedEncoding + elif 0 < enctype < 5: + encoding = usedEncoding + + # inherit fetcher for @imports in styleSheet + importedSheet._href = fullhref + importedSheet._setFetcher(self.parentStyleSheet._fetcher) + importedSheet._setCssTextWithEncodingOverride( + cssText, + encodingOverride=encodingOverride, + encoding=encoding) + + except (OSError, IOError, ValueError), e: + self._log.warn(u'CSSImportRule: While processing imported ' + u'style sheet href=%s: %r' + % (self.href, e), neverraise=True) + + else: + # used by resolveImports if to keep unprocessed href + self.hrefFound = True + + self._styleSheet = importedSheet + + _href = None # needs to be set + href = property(lambda self: self._href, _setHref, + doc=u"Location of the style sheet to be imported.") + + def _setMedia(self, media): + """ + :param media: + a :class:`~cssutils.stylesheets.MediaList` or string + """ + self._checkReadonly() + if isinstance(media, basestring): + self._media = cssutils.stylesheets.MediaList(mediaText=media, + parentRule=self) + else: + media._parentRule = self + self._media = media + + # update seq + ihref = 0 + for i, item in enumerate(self.seq): + if item.type == 'href': + ihref = i + elif item.type == 'media': + self.seq[i] = (self._media, 'media', None, None) + break + else: + # if no media until now add after href + self.seq.insert(ihref+1, + self._media, 'media', None, None) + + media = property(lambda self: self._media, _setMedia, + doc=u"(DOM) A list of media types for this rule " + u"of type :class:`~cssutils.stylesheets.MediaList`.") + + def _setName(self, name=u''): + """Raises xml.dom.SyntaxErr if name is not a string.""" + if name is None or isinstance(name, basestring): + # "" or '' handled as None + if not name: + name = None + + # save name + self._name = name + + # update seq + for i, item in enumerate(self.seq): + val, typ = item.value, item.type + if 'name' == typ: + self._seq[i] = (name, typ, item.line, item.col) + break + + # set title of imported sheet + if self.styleSheet: + self.styleSheet.title = name + + else: + self._log.error(u'CSSImportRule: Not a valid name: %s' % name) + + name = property(lambda self: self._name, _setName, + doc=u"An optional name for the imported sheet.") + + styleSheet = property(lambda self: self._styleSheet, + doc=u"(readonly) The style sheet referred to by this " + u"rule.") + + type = property(lambda self: self.IMPORT_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + def _getWellformed(self): + "Depending on if media is used at all." + if self._usemedia: + return bool(self.href and self.media.wellformed) + else: + return bool(self.href) + + wellformed = property(_getWellformed) diff --git a/libs/cssutils/css/cssmediarule.py b/libs/cssutils/css/cssmediarule.py new file mode 100755 index 00000000..a7ebf463 --- /dev/null +++ b/libs/cssutils/css/cssmediarule.py @@ -0,0 +1,302 @@ +"""CSSMediaRule implements DOM Level 2 CSS CSSMediaRule.""" +__all__ = ['CSSMediaRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSMediaRule(cssrule.CSSRuleRules): + """ + Objects implementing the CSSMediaRule interface can be identified by the + MEDIA_RULE constant. On these objects the type attribute must return the + value of that constant. + + Format:: + + : MEDIA_SYM S* medium [ COMMA S* medium ]* + + STRING? # the name + + LBRACE S* ruleset* '}' S*; + + ``cssRules`` + All Rules in this media rule, a :class:`~cssutils.css.CSSRuleList`. + """ + def __init__(self, mediaText='all', name=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """constructor""" + super(CSSMediaRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@media' + + # 1. media + if mediaText: + self.media = mediaText + else: + self.media = cssutils.stylesheets.MediaList() + + self.name = name + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(mediaText=%r)" % ( + self.__class__.__name__, + self.media.mediaText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.media.mediaText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSMediaRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :Exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if a specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + # media "name"? { cssRules } + super(CSSMediaRule, self)._setCssText(cssText) + + # might be (cssText, namespaces) + cssText, namespaces = self._splitNamespacesOff(cssText) + + try: + # use parent style sheet ones if available + namespaces = self.parentStyleSheet.namespaces + except AttributeError: + pass + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.MEDIA_SYM: + self._log.error(u'CSSMediaRule: No CSSMediaRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + + else: + # save if parse goes wrong + oldMedia = self._media + oldName = self._name + oldCssRules = self._cssRules + + ok = True + + # media + mediatokens, end = self._tokensupto2(tokenizer, + mediaqueryendonly=True, + separateEnd=True) + if u'{' == self._tokenvalue(end)\ + or self._prods.STRING == self._type(end): + self.media = cssutils.stylesheets.MediaList(parentRule=self) + # TODO: remove special case + self.media.mediaText = mediatokens + ok = ok and self.media.wellformed + else: + ok = False + + # name (optional) + name = None + nameseq = self._tempSeq() + if self._prods.STRING == self._type(end): + name = self._stringtokenvalue(end) + # TODO: for now comments are lost after name + nametokens, end = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + wellformed, expected = self._parse(None, + nameseq, + nametokens, + {}) + if not wellformed: + ok = False + self._log.error(u'CSSMediaRule: Syntax Error: %s' % + self._valuestr(cssText)) + + + # check for { + if u'{' != self._tokenvalue(end): + self._log.error(u'CSSMediaRule: No "{" found: %s' % + self._valuestr(cssText)) + return + + # cssRules + cssrulestokens, braceOrEOF = self._tokensupto2(tokenizer, + mediaendonly=True, + separateEnd=True) + nonetoken = self._nexttoken(tokenizer, None) + if 'EOF' == self._type(braceOrEOF): + # HACK!!! + # TODO: Not complete, add EOF to rule and } to @media + cssrulestokens.append(braceOrEOF) + braceOrEOF = ('CHAR', '}', 0, 0) + self._log.debug(u'CSSMediaRule: Incomplete, adding "}".', + token=braceOrEOF, neverraise=True) + + if u'}' != self._tokenvalue(braceOrEOF): + self._log.error(u'CSSMediaRule: No "}" found.', + token=braceOrEOF) + elif nonetoken: + self._log.error(u'CSSMediaRule: Trailing content found.', + token=nonetoken) + else: + # for closures: must be a mutable + new = {'wellformed': True } + + def COMMENT(expected, seq, token, tokenizer=None): + self.insertRule(cssutils.css.CSSComment([token], + parentRule=self, + parentStyleSheet=self.parentStyleSheet)) + return expected + + def ruleset(expected, seq, token, tokenizer): + rule = cssutils.css.CSSStyleRule(parentRule=self, + parentStyleSheet=self.parentStyleSheet) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return expected + + def atrule(expected, seq, token, tokenizer): + # TODO: get complete rule! + tokens = self._tokensupto2(tokenizer, token) + atval = self._tokenvalue(token) + if atval in ('@charset ', '@font-face', '@import', + '@namespace', '@page', '@media', '@variables'): + self._log.error(u'CSSMediaRule: This rule is not ' + u'allowed in CSSMediaRule - ignored: ' + u'%s.' % self._valuestr(tokens), + token = token, + error=xml.dom.HierarchyRequestErr) + else: + rule = cssutils.css.CSSUnknownRule(tokens, + parentRule=self, + parentStyleSheet=self.parentStyleSheet) + if rule.wellformed: + self.insertRule(rule) + return expected + + # save for possible reset + oldCssRules = self.cssRules + + self.cssRules = cssutils.css.CSSRuleList() + seq = [] # not used really + + tokenizer = iter(cssrulestokens) + wellformed, expected = self._parse(braceOrEOF, + seq, + tokenizer, { + 'COMMENT': COMMENT, + 'CHARSET_SYM': atrule, + 'FONT_FACE_SYM': atrule, + 'IMPORT_SYM': atrule, + 'NAMESPACE_SYM': atrule, + 'PAGE_SYM': atrule, + 'MEDIA_SYM': atrule, + 'ATKEYWORD': atrule + }, + default=ruleset, + new=new) + ok = ok and wellformed + + if ok: + self.name = name + self._setSeq(nameseq) + else: + self._media = oldMedia + self._cssRules = oldCssRules + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setName(self, name): + if isinstance(name, basestring) or name is None: + # "" or '' + if not name: + name = None + + self._name = name + else: + self._log.error(u'CSSImportRule: Not a valid name: %s' % name) + + name = property(lambda self: self._name, _setName, + doc=u"An optional name for this media rule.") + + def _setMedia(self, media): + """ + :param media: + a :class:`~cssutils.stylesheets.MediaList` or string + """ + self._checkReadonly() + if isinstance(media, basestring): + self._media = cssutils.stylesheets.MediaList(mediaText=media, + parentRule=self) + else: + media._parentRule = self + self._media = media + + # NOT IN @media seq at all?! +# # update seq +# for i, item in enumerate(self.seq): +# if item.type == 'media': +# self._seq[i] = (self._media, 'media', None, None) +# break +# else: +# # insert after @media if not in seq at all +# self.seq.insert(0, +# self._media, 'media', None, None) + + media = property(lambda self: self._media, _setMedia, + doc=u"(DOM) A list of media types for this rule " + u"of type :class:`~cssutils.stylesheets.MediaList`.") + + + def insertRule(self, rule, index=None): + """Implements base ``insertRule``.""" + rule, index = self._prepareInsertRule(rule, index) + + if rule is False or rule is True: + # done or error + return + + # check hierarchy + if isinstance(rule, cssutils.css.CSSCharsetRule) or \ + isinstance(rule, cssutils.css.CSSFontFaceRule) or \ + isinstance(rule, cssutils.css.CSSImportRule) or \ + isinstance(rule, cssutils.css.CSSNamespaceRule) or \ + isinstance(rule, cssutils.css.CSSPageRule) or \ + isinstance(rule, cssutils.css.MarginRule) or \ + isinstance(rule, CSSMediaRule): + self._log.error(u'%s: This type of rule is not allowed here: %s' + % (self.__class__.__name__, rule.cssText), + error=xml.dom.HierarchyRequestErr) + return + + return self._finishInsertRule(rule, index) + + type = property(lambda self: self.MEDIA_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: self.media.wellformed) diff --git a/libs/cssutils/css/cssnamespacerule.py b/libs/cssutils/css/cssnamespacerule.py new file mode 100755 index 00000000..a2610fc2 --- /dev/null +++ b/libs/cssutils/css/cssnamespacerule.py @@ -0,0 +1,295 @@ +"""CSSNamespaceRule currently implements http://dev.w3.org/csswg/css3-namespace/ +""" +__all__ = ['CSSNamespaceRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSNamespaceRule(cssrule.CSSRule): + """ + Represents an @namespace rule within a CSS style sheet. + + The @namespace at-rule declares a namespace prefix and associates + it with a given namespace (a string). This namespace prefix can then be + used in namespace-qualified names such as those described in the + Selectors Module [SELECT] or the Values and Units module [CSS3VAL]. + + Dealing with these rules directly is not needed anymore, easier is + the use of :attr:`cssutils.css.CSSStyleSheet.namespaces`. + + Format:: + + namespace + : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* + ; + namespace_prefix + : IDENT + ; + """ + def __init__(self, namespaceURI=None, prefix=None, cssText=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """ + :Parameters: + namespaceURI + The namespace URI (a simple string!) which is bound to the + given prefix. If no prefix is set + (``CSSNamespaceRule.prefix==''``) the namespace defined by + namespaceURI is set as the default namespace + prefix + The prefix used in the stylesheet for the given + ``CSSNamespaceRule.uri``. + cssText + if no namespaceURI is given cssText must be given to set + a namespaceURI as this is readonly later on + parentStyleSheet + sheet where this rule belongs to + + Do not use as positional but as keyword parameters only! + + If readonly allows setting of properties in constructor only + + format namespace:: + + namespace + : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* + ; + namespace_prefix + : IDENT + ; + """ + super(CSSNamespaceRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@namespace' + self._prefix = u'' + self._namespaceURI = None + + if namespaceURI: + self.namespaceURI = namespaceURI + self.prefix = prefix + tempseq = self._tempSeq() + tempseq.append(self.prefix, 'prefix') + tempseq.append(self.namespaceURI, 'namespaceURI') + self._setSeq(tempseq) + + elif cssText is not None: + self.cssText = cssText + + if parentStyleSheet: + self._parentStyleSheet = parentStyleSheet + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(namespaceURI=%r, prefix=%r)" % ( + self.__class__.__name__, + self.namespaceURI, + self.prefix) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.namespaceURI, + self.prefix, + id(self)) + + def _getCssText(self): + """Return serialized property cssText""" + return cssutils.ser.do_CSSNamespaceRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: initial value for this rules cssText which is parsed + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + super(CSSNamespaceRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.NAMESPACE_SYM: + self._log.error(u'CSSNamespaceRule: No CSSNamespaceRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'keyword': self._tokenvalue(attoken), + 'prefix': u'', + 'uri': None, + 'wellformed': True + } + + def _ident(expected, seq, token, tokenizer=None): + # the namespace prefix, optional + if 'prefix or uri' == expected: + new['prefix'] = self._tokenvalue(token) + seq.append(new['prefix'], 'prefix') + return 'uri' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected ident.', token) + return expected + + def _string(expected, seq, token, tokenizer=None): + # the namespace URI as a STRING + if expected.endswith('uri'): + new['uri'] = self._stringtokenvalue(token) + seq.append(new['uri'], 'namespaceURI') + return ';' + + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected string.', token) + return expected + + def _uri(expected, seq, token, tokenizer=None): + # the namespace URI as URI which is DEPRECATED + if expected.endswith('uri'): + uri = self._uritokenvalue(token) + new['uri'] = uri + seq.append(new['uri'], 'namespaceURI') + return ';' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected URI.', token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # final ; + val = self._tokenvalue(token) + if ';' == expected and u';' == val: + return 'EOF' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected char.', token) + return expected + + # "NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*" + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='prefix or uri', + seq=newseq, tokenizer=tokenizer, + productions={'IDENT': _ident, + 'STRING': _string, + 'URI': _uri, + 'CHAR': _char}, + new=new) + + # wellformed set by parse + wellformed = wellformed and new['wellformed'] + + # post conditions + if new['uri'] is None: + wellformed = False + self._log.error(u'CSSNamespaceRule: No namespace URI found: %s' + % self._valuestr(cssText)) + + if expected != 'EOF': + wellformed = False + self._log.error(u'CSSNamespaceRule: No ";" found: %s' % + self._valuestr(cssText)) + + # set all + if wellformed: + self.atkeyword = new['keyword'] + self._prefix = new['prefix'] + self.namespaceURI = new['uri'] + self._setSeq(newseq) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setNamespaceURI(self, namespaceURI): + """ + :param namespaceURI: the initial value for this rules namespaceURI + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + (CSSRule) Raised if this rule is readonly or a namespaceURI is + already set in this rule. + """ + self._checkReadonly() + if not self._namespaceURI: + # initial setting + self._namespaceURI = namespaceURI + tempseq = self._tempSeq() + tempseq.append(namespaceURI, 'namespaceURI') + self._setSeq(tempseq) # makes seq readonly! + elif self._namespaceURI != namespaceURI: + self._log.error(u'CSSNamespaceRule: namespaceURI is readonly.', + error=xml.dom.NoModificationAllowedErr) + + namespaceURI = property(lambda self: self._namespaceURI, _setNamespaceURI, + doc="URI (handled as simple string) of the defined namespace.") + + def _replaceNamespaceURI(self, namespaceURI): + """Used during parse of new sheet only! + + :param namespaceURI: the new value for this rules namespaceURI + """ + self._namespaceURI = namespaceURI + for i, x in enumerate(self._seq): + if 'namespaceURI' == x.type: + self._seq._readonly = False + self._seq.replace(i, namespaceURI, 'namespaceURI') + self._seq._readonly = True + break + + def _setPrefix(self, prefix=None): + """ + :param prefix: the new prefix + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + if not prefix: + prefix = u'' + else: + tokenizer = self._tokenize2(prefix) + prefixtoken = self._nexttoken(tokenizer, None) + if not prefixtoken or self._type(prefixtoken) != self._prods.IDENT: + self._log.error(u'CSSNamespaceRule: No valid prefix "%s".' % + self._valuestr(prefix), + error=xml.dom.SyntaxErr) + return + else: + prefix = self._tokenvalue(prefixtoken) + # update seq + for i, x in enumerate(self._seq): + if x == self._prefix: + self._seq[i] = (prefix, 'prefix', None, None) + break + else: + # put prefix at the beginning! + self._seq[0] = (prefix, 'prefix', None, None) + + # set new prefix + self._prefix = prefix + + prefix = property(lambda self: self._prefix, _setPrefix, + doc=u"Prefix used for the defined namespace.") + + type = property(lambda self: self.NAMESPACE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: self.namespaceURI is not None) + \ No newline at end of file diff --git a/libs/cssutils/css/csspagerule.py b/libs/cssutils/css/csspagerule.py new file mode 100755 index 00000000..cc2f4516 --- /dev/null +++ b/libs/cssutils/css/csspagerule.py @@ -0,0 +1,436 @@ +"""CSSPageRule implements DOM Level 2 CSS CSSPageRule.""" +__all__ = ['CSSPageRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from itertools import chain +from cssstyledeclaration import CSSStyleDeclaration +from marginrule import MarginRule +import cssrule +import cssutils +import xml.dom + +class CSSPageRule(cssrule.CSSRuleRules): + """ + The CSSPageRule interface represents a @page rule within a CSS style + sheet. The @page rule is used to specify the dimensions, orientation, + margins, etc. of a page box for paged media. + + Format:: + + page : + PAGE_SYM S* IDENT? pseudo_page? S* + '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* + ; + + pseudo_page : + ':' [ "left" | "right" | "first" ] + ; + + margin : + margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* + ; + + margin_sym : + TOPLEFTCORNER_SYM | + TOPLEFT_SYM | + TOPCENTER_SYM | + TOPRIGHT_SYM | + TOPRIGHTCORNER_SYM | + BOTTOMLEFTCORNER_SYM | + BOTTOMLEFT_SYM | + BOTTOMCENTER_SYM | + BOTTOMRIGHT_SYM | + BOTTOMRIGHTCORNER_SYM | + LEFTTOP_SYM | + LEFTMIDDLE_SYM | + LEFTBOTTOM_SYM | + RIGHTTOP_SYM | + RIGHTMIDDLE_SYM | + RIGHTBOTTOM_SYM + ; + + `cssRules` contains a list of `MarginRule` objects. + """ + def __init__(self, selectorText=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + + :param selectorText: + type string + :param style: + CSSStyleDeclaration for this CSSStyleRule + """ + super(CSSPageRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@page' + self._specificity = (0, 0, 0) + + tempseq = self._tempSeq() + + if selectorText: + self.selectorText = selectorText + tempseq.append(self.selectorText, 'selectorText') + else: + self._selectorText = self._tempSeq() + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + tempseq.append(self.style, 'style') + + self._setSeq(tempseq) + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(selectorText=%r, style=%r)" % ( + self.__class__.__name__, + self.selectorText, + self.style.cssText) + + def __str__(self): + return (u"") % ( + self.__class__.__name__, + self.selectorText, + self.specificity, + self.style.cssText, + len(self.cssRules), + id(self)) + + def __contains__(self, margin): + """Check if margin is set in the rule.""" + return margin in self.keys() + + def keys(self): + "Return list of all set margins (MarginRule)." + return list(r.margin for r in self.cssRules) + + def __getitem__(self, margin): + """Retrieve the style (of MarginRule) + for `margin` (which must be normalized). + """ + for r in self.cssRules: + if r.margin == margin: + return r.style + + def __setitem__(self, margin, style): + """Set the style (of MarginRule) + for `margin` (which must be normalized). + """ + for i, r in enumerate(self.cssRules): + if r.margin == margin: + r.style = style + return i + else: + return self.add(MarginRule(margin, style)) + + def __delitem__(self, margin): + """Delete the style (the MarginRule) + for `margin` (which must be normalized). + """ + for r in self.cssRules: + if r.margin == margin: + self.deleteRule(r) + + def __parseSelectorText(self, selectorText): + """ + Parse `selectorText` which may also be a list of tokens + and returns (selectorText, seq). + + see _setSelectorText for details + """ + # for closures: must be a mutable + new = {'wellformed': True, 'last-S': False, + 'name': 0, 'first': 0, 'lr': 0} + specificity = (0, 0, 0) + + def _char(expected, seq, token, tokenizer=None): + # pseudo_page, :left, :right or :first + val = self._tokenvalue(token) + if not new['last-S'] and expected in ['page', ': or EOF']\ + and u':' == val: + try: + identtoken = tokenizer.next() + except StopIteration: + self._log.error( + u'CSSPageRule selectorText: No IDENT found.', token) + else: + ival, ityp = self._tokenvalue(identtoken),\ + self._type(identtoken) + if self._prods.IDENT != ityp: + self._log.error(u'CSSPageRule selectorText: Expected ' + u'IDENT but found: %r' % ival, token) + else: + if not ival in (u'first', u'left', u'right'): + self._log.warn(u'CSSPageRule: Unknown @page ' + u'selector: %r' + % (u':'+ival,), neverraise=True) + if ival == u'first': + new['first'] = 1 + else: + new['lr'] = 1 + seq.append(val + ival, 'pseudo') + return 'EOF' + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSPageRule selectorText: Unexpected CHAR: %r' + % val, token) + return expected + + def S(expected, seq, token, tokenizer=None): + "Does not raise if EOF is found." + if expected == ': or EOF': + # pseudo must directly follow IDENT if given + new['last-S'] = True + return expected + + def IDENT(expected, seq, token, tokenizer=None): + "" + val = self._tokenvalue(token) + if 'page' == expected: + if self._normalize(val) == u'auto': + self._log.error(u'CSSPageRule selectorText: Invalid pagename.', + token) + else: + new['name'] = 1 + seq.append(val, 'IDENT') + + return ': or EOF' + else: + new['wellformed'] = False + self._log.error(u'CSSPageRule selectorText: Unexpected IDENT: ' + u'%r' % val, token) + return expected + + def COMMENT(expected, seq, token, tokenizer=None): + "Does not raise if EOF is found." + seq.append(cssutils.css.CSSComment([token]), 'COMMENT') + return expected + + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='page', + seq=newseq, tokenizer=self._tokenize2(selectorText), + productions={'CHAR': _char, + 'IDENT': IDENT, + 'COMMENT': COMMENT, + 'S': S}, + new=new) + wellformed = wellformed and new['wellformed'] + + # post conditions + if expected == 'ident': + self._log.error( + u'CSSPageRule selectorText: No valid selector: %r' % + self._valuestr(selectorText)) + + return wellformed, newseq, (new['name'], new['first'], new['lr']) + + + def __parseMarginAndStyle(self, tokens): + "tokens is a list, no generator (yet)" + g = iter(tokens) + styletokens = [] + + # new rules until parse done + cssRules = [] + + for token in g: + if token[0] == 'ATKEYWORD' and \ + self._normalize(token[1]) in MarginRule.margins: + + # MarginRule + m = MarginRule(parentRule=self, + parentStyleSheet=self.parentStyleSheet) + m.cssText = chain([token], g) + + # merge if margin set more than once + for r in cssRules: + if r.margin == m.margin: + for p in m.style: + r.style.setProperty(p, replace=False) + break + else: + cssRules.append(m) + + continue + + # TODO: Properties? + styletokens.append(token) + + return cssRules, styletokens + + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSPageRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSPageRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + if self._type(self._nexttoken(tokenizer)) != self._prods.PAGE_SYM: + self._log.error(u'CSSPageRule: No CSSPageRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + selectortokens, startbrace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + styletokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + nonetoken = self._nexttoken(tokenizer) + if self._tokenvalue(startbrace) != u'{': + ok = False + self._log.error(u'CSSPageRule: No start { of style declaration ' + u'found: %r' % + self._valuestr(cssText), startbrace) + elif nonetoken: + ok = False + self._log.error(u'CSSPageRule: Trailing content found.', + token=nonetoken) + + selok, newselseq, specificity = self.__parseSelectorText(selectortokens) + ok = ok and selok + + val, type_ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error( + u'CSSPageRule: No "}" after style declaration found: %r' % + self._valuestr(cssText)) + else: + if 'EOF' == type_: + # add again as style needs it + styletokens.append(braceorEOFtoken) + + # filter pagemargin rules out first + cssRules, styletokens = self.__parseMarginAndStyle(styletokens) + + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + self._selectorText = newselseq + self._specificity = specificity + self.style = newStyle + self.cssRules = cssutils.css.CSSRuleList() + for r in cssRules: + self.cssRules.append(r) + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this rule.") + + + def _getSelectorText(self): + """Wrapper for cssutils Selector object.""" + return cssutils.ser.do_CSSPageRuleSelector(self._selectorText) + + def _setSelectorText(self, selectorText): + """Wrapper for cssutils Selector object. + + :param selectorText: + DOM String, in CSS 2.1 one of + + - :first + - :left + - :right + - empty + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # may raise SYNTAX_ERR + wellformed, newseq, specificity = self.__parseSelectorText(selectorText) + if wellformed: + self._selectorText = newseq + self._specificity = specificity + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(DOM) The parsable textual representation of " + u"the page selector for the rule.") + + def _setStyle(self, style): + """ + :param style: + a CSSStyleDeclaration or string + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set, " + u"a :class:`~cssutils.css.CSSStyleDeclaration`.") + + + def insertRule(self, rule, index=None): + """Implements base ``insertRule``.""" + rule, index = self._prepareInsertRule(rule, index) + + if rule is False or rule is True: + # done or error + return + + # check hierarchy + if isinstance(rule, cssutils.css.CSSCharsetRule) or \ + isinstance(rule, cssutils.css.CSSFontFaceRule) or \ + isinstance(rule, cssutils.css.CSSImportRule) or \ + isinstance(rule, cssutils.css.CSSNamespaceRule) or \ + isinstance(rule, CSSPageRule) or \ + isinstance(rule, cssutils.css.CSSMediaRule): + self._log.error(u'%s: This type of rule is not allowed here: %s' + % (self.__class__.__name__, rule.cssText), + error=xml.dom.HierarchyRequestErr) + return + + return self._finishInsertRule(rule, index) + + specificity = property(lambda self: self._specificity, + doc=u"""Specificity of this page rule (READONLY). +Tuple of (f, g, h) where: + + - if the page selector has a named page, f=1; else f=0 + - if the page selector has a ':first' pseudo-class, g=1; else g=0 + - if the page selector has a ':left' or ':right' pseudo-class, h=1; else h=0 +""") + + type = property(lambda self: self.PAGE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + # constant but needed: + wellformed = property(lambda self: True) \ No newline at end of file diff --git a/libs/cssutils/css/cssproperties.py b/libs/cssutils/css/cssproperties.py new file mode 100755 index 00000000..7454daff --- /dev/null +++ b/libs/cssutils/css/cssproperties.py @@ -0,0 +1,122 @@ +"""CSS2Properties (partly!) implements DOM Level 2 CSS CSS2Properties used +by CSSStyleDeclaration + +TODO: CSS2Properties + If an implementation does implement this interface, it is expected to + understand the specific syntax of the shorthand properties, and apply + their semantics; when the margin property is set, for example, the + marginTop, marginRight, marginBottom and marginLeft properties are + actually being set by the underlying implementation. + + When dealing with CSS "shorthand" properties, the shorthand properties + should be decomposed into their component longhand properties as + appropriate, and when querying for their value, the form returned + should be the shortest form exactly equivalent to the declarations made + in the ruleset. However, if there is no shorthand declaration that + could be added to the ruleset without changing in any way the rules + already declared in the ruleset (i.e., by adding longhand rules that + were previously not declared in the ruleset), then the empty string + should be returned for the shorthand property. + + For example, querying for the font property should not return + "normal normal normal 14pt/normal Arial, sans-serif", when + "14pt Arial, sans-serif" suffices. (The normals are initial values, and + are implied by use of the longhand property.) + + If the values for all the longhand properties that compose a particular + string are the initial values, then a string consisting of all the + initial values should be returned (e.g. a border-width value of + "medium" should be returned as such, not as ""). + + For some shorthand properties that take missing values from other + sides, such as the margin, padding, and border-[width|style|color] + properties, the minimum number of sides possible should be used; i.e., + "0px 10px" will be returned instead of "0px 10px 0px 10px". + + If the value of a shorthand property can not be decomposed into its + component longhand properties, as is the case for the font property + with a value of "menu", querying for the values of the component + longhand properties should return the empty string. + +TODO: CSS2Properties DOMImplementation + The interface found within this section are not mandatory. A DOM + application can use the hasFeature method of the DOMImplementation + interface to determine whether it is supported or not. The feature + string for this extended interface listed in this section is "CSS2" + and the version is "2.0". + +""" +__all__ = ['CSS2Properties'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssutils.profiles +import re + +class CSS2Properties(object): + """The CSS2Properties interface represents a convenience mechanism + for retrieving and setting properties within a CSSStyleDeclaration. + The attributes of this interface correspond to all the properties + specified in CSS2. Getting an attribute of this interface is + equivalent to calling the getPropertyValue method of the + CSSStyleDeclaration interface. Setting an attribute of this + interface is equivalent to calling the setProperty method of the + CSSStyleDeclaration interface. + + cssutils actually also allows usage of ``del`` to remove a CSS property + from a CSSStyleDeclaration. + + This is an abstract class, the following functions need to be present + in inheriting class: + + - ``_getP`` + - ``_setP`` + - ``_delP`` + """ + # actual properties are set after the class definition! + def _getP(self, CSSname): pass + def _setP(self, CSSname, value): pass + def _delP(self, CSSname): pass + + +_reCSStoDOMname = re.compile('-[a-z]', re.I) +def _toDOMname(CSSname): + """Returns DOMname for given CSSname e.g. for CSSname 'font-style' returns + 'fontStyle'. + """ + def _doCSStoDOMname2(m): return m.group(0)[1].capitalize() + return _reCSStoDOMname.sub(_doCSStoDOMname2, CSSname) + +_reDOMtoCSSname = re.compile('([A-Z])[a-z]+') +def _toCSSname(DOMname): + """Return CSSname for given DOMname e.g. for DOMname 'fontStyle' returns + 'font-style'. + """ + def _doDOMtoCSSname2(m): return '-' + m.group(0).lower() + return _reDOMtoCSSname.sub(_doDOMtoCSSname2, DOMname) + +# add list of DOMname properties to CSS2Properties +# used for CSSStyleDeclaration to check if allowed properties +# but somehow doubled, any better way? +CSS2Properties._properties = [] +for group in cssutils.profiles.properties: + for name in cssutils.profiles.properties[group]: + CSS2Properties._properties.append(_toDOMname(name)) + + +# add CSS2Properties to CSSStyleDeclaration: +def __named_property_def(DOMname): + """ + Closure to keep name known in each properties accessor function + DOMname is converted to CSSname here, so actual calls use CSSname. + """ + CSSname = _toCSSname(DOMname) + def _get(self): return self._getP(CSSname) + def _set(self, value): self._setP(CSSname, value) + def _del(self): self._delP(CSSname) + return _get, _set, _del + +# add all CSS2Properties to CSSStyleDeclaration +for DOMname in CSS2Properties._properties: + setattr(CSS2Properties, DOMname, + property(*__named_property_def(DOMname))) diff --git a/libs/cssutils/css/cssrule.py b/libs/cssutils/css/cssrule.py new file mode 100755 index 00000000..1d3ab6fb --- /dev/null +++ b/libs/cssutils/css/cssrule.py @@ -0,0 +1,304 @@ +"""CSSRule implements DOM Level 2 CSS CSSRule.""" +__all__ = ['CSSRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssutils +import xml.dom + +class CSSRule(cssutils.util.Base2): + """Abstract base interface for any type of CSS statement. This includes + both rule sets and at-rules. An implementation is expected to preserve + all rules specified in a CSS style sheet, even if the rule is not + recognized by the parser. Unrecognized rules are represented using the + :class:`CSSUnknownRule` interface. + """ + + """ + CSSRule type constants. + An integer indicating which type of rule this is. + """ + UNKNOWN_RULE = 0 + ":class:`cssutils.css.CSSUnknownRule` (not used in CSSOM anymore)" + STYLE_RULE = 1 + ":class:`cssutils.css.CSSStyleRule`" + CHARSET_RULE = 2 + ":class:`cssutils.css.CSSCharsetRule` (not used in CSSOM anymore)" + IMPORT_RULE = 3 + ":class:`cssutils.css.CSSImportRule`" + MEDIA_RULE = 4 + ":class:`cssutils.css.CSSMediaRule`" + FONT_FACE_RULE = 5 + ":class:`cssutils.css.CSSFontFaceRule`" + PAGE_RULE = 6 + ":class:`cssutils.css.CSSPageRule`" + NAMESPACE_RULE = 10 + """:class:`cssutils.css.CSSNamespaceRule`, + Value has changed in 0.9.7a3 due to a change in the CSSOM spec.""" + COMMENT = 1001 # was -1, cssutils only + """:class:`cssutils.css.CSSComment` - not in the offical spec, + Value has changed in 0.9.7a3""" + VARIABLES_RULE = 1008 + """:class:`cssutils.css.CSSVariablesRule` - experimental rule + not in the offical spec""" + + MARGIN_RULE = 1006 + """:class:`cssutils.css.MarginRule` - experimental rule + not in the offical spec""" + + _typestrings = {UNKNOWN_RULE: u'UNKNOWN_RULE', + STYLE_RULE: u'STYLE_RULE', + CHARSET_RULE: u'CHARSET_RULE', + IMPORT_RULE: u'IMPORT_RULE', + MEDIA_RULE: u'MEDIA_RULE', + FONT_FACE_RULE: u'FONT_FACE_RULE', + PAGE_RULE: u'PAGE_RULE', + NAMESPACE_RULE: u'NAMESPACE_RULE', + COMMENT: u'COMMENT', + VARIABLES_RULE: u'VARIABLES_RULE', + MARGIN_RULE: u'MARGIN_RULE' + } + + def __init__(self, parentRule=None, parentStyleSheet=None, readonly=False): + """Set common attributes for all rules.""" + super(CSSRule, self).__init__() + self._parent = parentRule + self._parentRule = parentRule + self._parentStyleSheet = parentStyleSheet + self._setSeq(self._tempSeq()) + #self._atkeyword = None + # must be set after initialization of #inheriting rule is done + self._readonly = False + + def _setAtkeyword(self, keyword): + """Check if new keyword fits the rule it is used for.""" + atkeyword = self._normalize(keyword) + if not self.atkeyword or (self.atkeyword == atkeyword): + self._atkeyword = atkeyword + self._keyword = keyword + else: + self._log.error(u'%s: Invalid atkeyword for this rule: %r' % + (self.atkeyword, keyword), + error=xml.dom.InvalidModificationErr) + + atkeyword = property(lambda self: self._atkeyword, _setAtkeyword, + doc=u"Normalized keyword of an @rule (e.g. ``@import``).") + + def _setCssText(self, cssText): + """ + :param cssText: + A parsable DOMString. + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + self._checkReadonly() + + cssText = property(lambda self: u'', _setCssText, + doc=u"(DOM) The parsable textual representation of the " + u"rule. This reflects the current state of the rule " + u"and not its initial value.") + + parent = property(lambda self: self._parent, + doc=u"The Parent Node of this CSSRule or None.") + + parentRule = property(lambda self: self._parentRule, + doc=u"If this rule is contained inside another rule " + u"(e.g. a style rule inside an @media block), this " + u"is the containing rule. If this rule is not nested " + u"inside any other rules, this returns None.") + + def _getParentStyleSheet(self): + # rules contained in other rules (@media) use that rules parent + if (self.parentRule): + return self.parentRule._parentStyleSheet + else: + return self._parentStyleSheet + + parentStyleSheet = property(_getParentStyleSheet, + doc=u"The style sheet that contains this rule.") + + type = property(lambda self: self.UNKNOWN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + typeString = property(lambda self: CSSRule._typestrings[self.type], + doc=u"Descriptive name of this rule's type.") + + wellformed = property(lambda self: False, + doc=u"If the rule is wellformed.") + + + +class CSSRuleRules(CSSRule): + """Abstract base interface for rules that contain other rules + like @media or @page. Methods may be overwritten if a rule + has specific stuff to do like checking the order of insertion like + @media does. + """ + + def __init__(self, parentRule=None, parentStyleSheet=None): + + super(CSSRuleRules, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self.cssRules = cssutils.css.CSSRuleList() + + def __iter__(self): + """Generator iterating over these rule's cssRules.""" + for rule in self._cssRules: + yield rule + + def _setCssRules(self, cssRules): + "Set new cssRules and update contained rules refs." + cssRules.append = self.insertRule + cssRules.extend = self.insertRule + cssRules.__delitem__ == self.deleteRule + + for rule in cssRules: + rule._parentRule = self + rule._parentStyleSheet = None + + self._cssRules = cssRules + + cssRules = property(lambda self: self._cssRules, _setCssRules, + "All Rules in this style sheet, a " + ":class:`~cssutils.css.CSSRuleList`.") + + def deleteRule(self, index): + """ + Delete the rule at `index` from rules ``cssRules``. + + :param index: + The `index` of the rule to be removed from the rules cssRules + list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is + raised but rules for normal Python lists are used. E.g. + ``deleteRule(-1)`` removes the last rule in cssRules. + + `index` may also be a CSSRule object which will then be removed. + + :Exceptions: + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified index does not correspond to a rule in + the media rule list. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this media rule is readonly. + """ + self._checkReadonly() + + if isinstance(index, CSSRule): + for i, r in enumerate(self.cssRules): + if index == r: + index = i + break + else: + raise xml.dom.IndexSizeErr(u"%s: Not a rule in " + u"this rule'a cssRules list: %s" + % (self.__class__.__name__, index)) + + try: + # detach + self._cssRules[index]._parentRule = None + del self._cssRules[index] + + except IndexError: + raise xml.dom.IndexSizeErr(u'%s: %s is not a valid index ' + u'in the rulelist of length %i' + % (self.__class__.__name__, + index, self._cssRules.length)) + + def _prepareInsertRule(self, rule, index=None): + "return checked `index` and optional parsed `rule`" + self._checkReadonly() + + # check index + if index is None: + index = len(self._cssRules) + + elif index < 0 or index > self._cssRules.length: + raise xml.dom.IndexSizeErr(u'%s: Invalid index %s for ' + u'CSSRuleList with a length of %s.' + % (self.__class__.__name__, + index, self._cssRules.length)) + + # check and optionally parse rule + if isinstance(rule, basestring): + tempsheet = cssutils.css.CSSStyleSheet() + tempsheet.cssText = rule + if len(tempsheet.cssRules) != 1 or (tempsheet.cssRules and + not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)): + self._log.error(u'%s: Invalid Rule: %s' % (self.__class__.__name__, + rule)) + return False, False + rule = tempsheet.cssRules[0] + + elif isinstance(rule, cssutils.css.CSSRuleList): + # insert all rules + for i, r in enumerate(rule): + self.insertRule(r, index + i) + return True, True + + elif not isinstance(rule, cssutils.css.CSSRule): + self._log.error(u'%s: Not a CSSRule: %s' % (rule, + self.__class__.__name__)) + return False, False + + return rule, index + + def _finishInsertRule(self, rule, index): + "add `rule` at `index`" + rule._parentRule = self + rule._parentStyleSheet = None + self._cssRules.insert(index, rule) + return index + + def add(self, rule): + """Add `rule` to page rule. Same as ``insertRule(rule)``.""" + return self.insertRule(rule) + + def insertRule(self, rule, index=None): + """ + Insert `rule` into the rules ``cssRules``. + + :param rule: + the parsable text representing the `rule` to be inserted. For rule + sets this contains both the selector and the style declaration. + For at-rules, this specifies both the at-identifier and the rule + content. + + cssutils also allows rule to be a valid + :class:`~cssutils.css.CSSRule` object. + + :param index: + before the `index` the specified `rule` will be inserted. + If the specified `index` is equal to the length of the rules + rule collection, the rule will be added to the end of the rule. + If index is not given or None rule will be appended to rule + list. + + :returns: + the index of the newly inserted rule. + + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the `rule` cannot be inserted at the specified `index`, + e.g., if an @import rule is inserted after a standard rule set + or other at-rule. + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified `index` is not a valid insertion point. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified `rule` has a syntax error and is + unparsable. + """ + return self._prepareInsertRule(rule, index) diff --git a/libs/cssutils/css/cssrulelist.py b/libs/cssutils/css/cssrulelist.py new file mode 100755 index 00000000..f9daff49 --- /dev/null +++ b/libs/cssutils/css/cssrulelist.py @@ -0,0 +1,53 @@ +"""CSSRuleList implements DOM Level 2 CSS CSSRuleList. +Partly also http://dev.w3.org/csswg/cssom/#the-cssrulelist.""" +__all__ = ['CSSRuleList'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +class CSSRuleList(list): + """The CSSRuleList object represents an (ordered) list of statements. + + The items in the CSSRuleList are accessible via an integral index, + starting from 0. + + Subclasses a standard Python list so theoretically all standard list + methods are available. Setting methods like ``__init__``, ``append``, + ``extend`` or ``__setslice__`` are added later on instances of this + class if so desired. + E.g. CSSStyleSheet adds ``append`` which is not available in a simple + instance of this class! + """ + def __init__(self, *ignored): + "Nothing is set as this must also be defined later." + pass + + def __notimplemented(self, *ignored): + "Implemented in class using a CSSRuleList only." + raise NotImplementedError( + 'Must be implemented by class using an instance of this class.') + + append = extend = __setitem__ = __setslice__ = __notimplemented + + def item(self, index): + """(DOM) Retrieve a CSS rule by ordinal `index`. The order in this + collection represents the order of the rules in the CSS style + sheet. If index is greater than or equal to the number of rules in + the list, this returns None. + + Returns CSSRule, the style rule at the index position in the + CSSRuleList, or None if that is not a valid index. + """ + try: + return self[index] + except IndexError: + return None + + length = property(lambda self: len(self), + doc=u"(DOM) The number of CSSRules in the list.") + + def rulesOfType(self, type): + """Yield the rules which have the given `type` only, one of the + constants defined in :class:`cssutils.css.CSSRule`.""" + for r in self: + if r.type == type: + yield r diff --git a/libs/cssutils/css/cssstyledeclaration.py b/libs/cssutils/css/cssstyledeclaration.py new file mode 100755 index 00000000..7573487a --- /dev/null +++ b/libs/cssutils/css/cssstyledeclaration.py @@ -0,0 +1,697 @@ +"""CSSStyleDeclaration implements DOM Level 2 CSS CSSStyleDeclaration and +extends CSS2Properties + +see + http://www.w3.org/TR/1998/REC-CSS2-19980512/syndata.html#parsing-errors + +Unknown properties +------------------ +User agents must ignore a declaration with an unknown property. +For example, if the style sheet is:: + + H1 { color: red; rotation: 70minutes } + +the user agent will treat this as if the style sheet had been:: + + H1 { color: red } + +Cssutils gives a message about any unknown properties but +keeps any property (if syntactically correct). + +Illegal values +-------------- +User agents must ignore a declaration with an illegal value. For example:: + + IMG { float: left } /* correct CSS2 */ + IMG { float: left here } /* "here" is not a value of 'float' */ + IMG { background: "red" } /* keywords cannot be quoted in CSS2 */ + IMG { border-width: 3 } /* a unit must be specified for length values */ + +A CSS2 parser would honor the first rule and ignore the rest, as if the +style sheet had been:: + + IMG { float: left } + IMG { } + IMG { } + IMG { } + +Cssutils again will issue a message (WARNING in this case) about invalid +CSS2 property values. + +TODO: + This interface is also used to provide a read-only access to the + computed values of an element. See also the ViewCSS interface. + + - return computed values and not literal values + - simplify unit pairs/triples/quadruples + 2px 2px 2px 2px -> 2px for border/padding... + - normalize compound properties like: + background: no-repeat left url() #fff + -> background: #fff url() no-repeat left +""" +__all__ = ['CSSStyleDeclaration', 'Property'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssproperties import CSS2Properties +from property import Property +import cssutils +import xml.dom + +class CSSStyleDeclaration(CSS2Properties, cssutils.util.Base2): + """The CSSStyleDeclaration class represents a single CSS declaration + block. This class may be used to determine the style properties + currently set in a block or to set style properties explicitly + within the block. + + While an implementation may not recognize all CSS properties within + a CSS declaration block, it is expected to provide access to all + specified properties in the style sheet through the + CSSStyleDeclaration interface. + Furthermore, implementations that support a specific level of CSS + should correctly handle CSS shorthand properties for that level. For + a further discussion of shorthand properties, see the CSS2Properties + interface. + + Additionally the CSS2Properties interface is implemented. + + $css2propertyname + All properties defined in the CSS2Properties class are available + as direct properties of CSSStyleDeclaration with their respective + DOM name, so e.g. ``fontStyle`` for property 'font-style'. + + These may be used as:: + + >>> style = CSSStyleDeclaration(cssText='color: red') + >>> style.color = 'green' + >>> print style.color + green + >>> del style.color + >>> print style.color + + + Format:: + + [Property: Value Priority?;]* [Property: Value Priority?]? + """ + def __init__(self, cssText=u'', parentRule=None, readonly=False, + validating=None): + """ + :param cssText: + Shortcut, sets CSSStyleDeclaration.cssText + :param parentRule: + The CSS rule that contains this declaration block or + None if this CSSStyleDeclaration is not attached to a CSSRule. + :param readonly: + defaults to False + :param validating: + a flag defining if this sheet should be validated on change. + Defaults to None, which means defer to the parent stylesheet. + """ + super(CSSStyleDeclaration, self).__init__() + self._parentRule = parentRule + self.validating = validating + self.cssText = cssText + self._readonly = readonly + + def __contains__(self, nameOrProperty): + """Check if a property (or a property with given name) is in style. + + :param name: + a string or Property, uses normalized name and not literalname + """ + if isinstance(nameOrProperty, Property): + name = nameOrProperty.name + else: + name = self._normalize(nameOrProperty) + return name in self.__nnames() + + def __iter__(self): + """Iterator of set Property objects with different normalized names.""" + def properties(): + for name in self.__nnames(): + yield self.getProperty(name) + return properties() + + def keys(self): + """Analoguous to standard dict returns property names which are set in + this declaration.""" + return list(self.__nnames()) + + def __getitem__(self, CSSName): + """Retrieve the value of property ``CSSName`` from this declaration. + + ``CSSName`` will be always normalized. + """ + return self.getPropertyValue(CSSName) + + def __setitem__(self, CSSName, value): + """Set value of property ``CSSName``. ``value`` may also be a tuple of + (value, priority), e.g. style['color'] = ('red', 'important') + + ``CSSName`` will be always normalized. + """ + priority = None + if isinstance(value, tuple): + value, priority = value + + return self.setProperty(CSSName, value, priority) + + def __delitem__(self, CSSName): + """Delete property ``CSSName`` from this declaration. + If property is not in this declaration return u'' just like + removeProperty. + + ``CSSName`` will be always normalized. + """ + return self.removeProperty(CSSName) + + def __setattr__(self, n, v): + """Prevent setting of unknown properties on CSSStyleDeclaration + which would not work anyway. For these + ``CSSStyleDeclaration.setProperty`` MUST be called explicitly! + + TODO: + implementation of known is not really nice, any alternative? + """ + known = ['_tokenizer', '_log', '_ttypes', + '_seq', 'seq', 'parentRule', '_parentRule', 'cssText', + 'valid', 'wellformed', 'validating', + '_readonly', '_profiles', '_validating'] + known.extend(CSS2Properties._properties) + if n in known: + super(CSSStyleDeclaration, self).__setattr__(n, v) + else: + raise AttributeError(u'Unknown CSS Property, ' + u'``CSSStyleDeclaration.setProperty("%s", ' + u'...)`` MUST be used.' % n) + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.getCssText(separator=u' ')) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.length, + len(self.getProperties(all=True)), + id(self)) + + def __nnames(self): + """Return iterator for all different names in order as set + if names are set twice the last one is used (double reverse!) + """ + names = [] + for item in reversed(self.seq): + val = item.value + if isinstance(val, Property) and not val.name in names: + names.append(val.name) + return reversed(names) + + # overwritten accessor functions for CSS2Properties' properties + def _getP(self, CSSName): + """(DOM CSS2Properties) Overwritten here and effectively the same as + ``self.getPropertyValue(CSSname)``. + + Parameter is in CSSname format ('font-style'), see CSS2Properties. + + Example:: + + >>> style = CSSStyleDeclaration(cssText='font-style:italic;') + >>> print style.fontStyle + italic + """ + return self.getPropertyValue(CSSName) + + def _setP(self, CSSName, value): + """(DOM CSS2Properties) Overwritten here and effectively the same as + ``self.setProperty(CSSname, value)``. + + Only known CSS2Properties may be set this way, otherwise an + AttributeError is raised. + For these unknown properties ``setPropertyValue(CSSname, value)`` + has to be called explicitly. + Also setting the priority of properties needs to be done with a + call like ``setPropertyValue(CSSname, value, priority)``. + + Example:: + + >>> style = CSSStyleDeclaration() + >>> style.fontStyle = 'italic' + >>> # or + >>> style.setProperty('font-style', 'italic', '!important') + + """ + self.setProperty(CSSName, value) + # TODO: Shorthand ones + + def _delP(self, CSSName): + """(cssutils only) Overwritten here and effectively the same as + ``self.removeProperty(CSSname)``. + + Example:: + + >>> style = CSSStyleDeclaration(cssText='font-style:italic;') + >>> del style.fontStyle + >>> print style.fontStyle + + + """ + self.removeProperty(CSSName) + + def children(self): + """Generator yielding any known child in this declaration including + *all* properties, comments or CSSUnknownrules. + """ + for item in self._seq: + yield item.value + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_css_CSSStyleDeclaration(self) + + def _setCssText(self, cssText): + """Setting this attribute will result in the parsing of the new value + and resetting of all the properties in the declaration block + including the removal or addition of properties. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or a property is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + self._checkReadonly() + tokenizer = self._tokenize2(cssText) + + # for closures: must be a mutable + new = {'wellformed': True} + def ident(expected, seq, token, tokenizer=None): + # a property + + tokens = self._tokensupto2(tokenizer, starttoken=token, + semicolon=True) + if self._tokenvalue(tokens[-1]) == u';': + tokens.pop() + property = Property(parent=self) + property.cssText = tokens + if property.wellformed: + seq.append(property, 'Property') + else: + self._log.error(u'CSSStyleDeclaration: Syntax Error in ' + u'Property: %s' % self._valuestr(tokens)) + # does not matter in this case + return expected + + def unexpected(expected, seq, token, tokenizer=None): + # error, find next ; or } to omit upto next property + ignored = self._tokenvalue(token) + self._valuestr( + self._tokensupto2(tokenizer, + propertyvalueendonly=True)) + self._log.error(u'CSSStyleDeclaration: Unexpected token, ignoring ' + 'upto %r.' % ignored,token) + # does not matter in this case + return expected + + def char(expected, seq, token, tokenizer=None): + # a standalone ; or error... + if self._tokenvalue(token) == u';': + self._log.info(u'CSSStyleDeclaration: Stripped standalone semicolon' + u': %s' % self._valuestr([token]), neverraise=True) + return expected + else: + return unexpected(expected, seq, token, tokenizer) + + # [Property: Value;]* Property: Value? + newseq = self._tempSeq() + wellformed, expected = self._parse(expected=None, + seq=newseq, tokenizer=tokenizer, + productions={'IDENT': ident, 'CHAR': char}, + default=unexpected) + # wellformed set by parse + + for item in newseq: + item.value._parent = self + + # do not check wellformed as invalid things are removed anyway + self._setSeq(newseq) + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) A parsable textual representation of the " + u"declaration block excluding the surrounding curly " + u"braces.") + + def getCssText(self, separator=None): + """ + :returns: + serialized property cssText, each property separated by + given `separator` which may e.g. be ``u''`` to be able to use + cssText directly in an HTML style attribute. ``;`` is part of + each property (except the last one) and **cannot** be set with + separator! + """ + return cssutils.ser.do_css_CSSStyleDeclaration(self, separator) + + def _setParentRule(self, parentRule): + self._parentRule = parentRule +# for x in self.children(): +# x.parent = self + + parentRule = property(lambda self: self._parentRule, _setParentRule, + doc="(DOM) The CSS rule that contains this declaration block or " + "None if this CSSStyleDeclaration is not attached to a CSSRule.") + + def getProperties(self, name=None, all=False): + """ + :param name: + optional `name` of properties which are requested. + Only properties with this **always normalized** `name` are returned. + If `name` is ``None`` all properties are returned (at least one for + each set name depending on parameter `all`). + :param all: + if ``False`` (DEFAULT) only the effective properties are returned. + If name is given a list with only one property is returned. + + if ``True`` all properties including properties set multiple times + with different values or priorities for different UAs are returned. + The order of the properties is fully kept as in the original + stylesheet. + :returns: + a list of :class:`~cssutils.css.Property` objects set in + this declaration. + """ + if name and not all: + # single prop but list + p = self.getProperty(name) + if p: + return [p] + else: + return [] + elif not all: + # effective Properties in name order + return [self.getProperty(name) for name in self.__nnames()] + else: + # all properties or all with this name + nname = self._normalize(name) + properties = [] + for item in self.seq: + val = item.value + if isinstance(val, Property) and ( + (bool(nname) == False) or (val.name == nname)): + properties.append(val) + return properties + + def getProperty(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the effective :class:`~cssutils.css.Property` object. + """ + nname = self._normalize(name) + found = None + for item in reversed(self.seq): + val = item.value + if isinstance(val, Property): + if (normalize and nname == val.name) or name == val.literalname: + if val.priority: + return val + elif not found: + found = val + return found + + def getPropertyCSSValue(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + :class:`~cssutils.css.CSSValue`, the value of the effective + property if it has been explicitly set for this declaration block. + + (DOM) + Used to retrieve the object representation of the value of a CSS + property if it has been explicitly set within this declaration + block. Returns None if the property has not been set. + + (This method returns None if the property is a shorthand + property. Shorthand property values can only be accessed and + modified as strings, using the getPropertyValue and setProperty + methods.) + + **cssutils currently always returns a CSSValue if the property is + set.** + + for more on shorthand properties see + http://www.dustindiaz.com/css-shorthand/ + """ + nname = self._normalize(name) + if nname in self._SHORTHANDPROPERTIES: + self._log.info(u'CSSValue for shorthand property "%s" should be ' + u'None, this may be implemented later.' % + nname, neverraise=True) + + p = self.getProperty(name, normalize) + if p: + return p.cssValue + else: + return None + + def getPropertyValue(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the value of the effective property if it has been explicitly set + for this declaration block. Returns the empty string if the + property has not been set. + """ + p = self.getProperty(name, normalize) + if p: + return p.value + else: + return u'' + + def getPropertyPriority(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the priority of the effective CSS property (e.g. the + "important" qualifier) if the property has been explicitly set in + this declaration block. The empty string if none exists. + """ + p = self.getProperty(name, normalize) + if p: + return p.priority + else: + return u'' + + def removeProperty(self, name, normalize=True): + """ + (DOM) + Used to remove a CSS property if it has been explicitly set within + this declaration block. + + :param name: + of the CSS property + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent. + The effective Property value is returned and *all* Properties + with ``Property.name == name`` are removed. + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized `name` only. Also only the + Properties with the literal name `name` are removed. + :returns: + the value of the property if it has been explicitly set for + this declaration block. Returns the empty string if the property + has not been set or the property name does not correspond to a + known CSS property + + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + r = self.getPropertyValue(name, normalize=normalize) + newseq = self._tempSeq() + if normalize: + # remove all properties with name == nname + nname = self._normalize(name) + for item in self.seq: + if not (isinstance(item.value, Property) + and item.value.name == nname): + newseq.appendItem(item) + else: + # remove all properties with literalname == name + for item in self.seq: + if not (isinstance(item.value, Property) + and item.value.literalname == name): + newseq.appendItem(item) + self._setSeq(newseq) + return r + + def setProperty(self, name, value=None, priority=u'', + normalize=True, replace=True): + """(DOM) Set a property value and priority within this declaration + block. + + :param name: + of the CSS property to set (in W3C DOM the parameter is called + "propertyName"), always lowercase (even if not normalized) + + If a property with this `name` is present it will be reset. + + cssutils also allowed `name` to be a + :class:`~cssutils.css.Property` object, all other + parameter are ignored in this case + + :param value: + the new value of the property, ignored if `name` is a Property. + :param priority: + the optional priority of the property (e.g. "important"), + ignored if `name` is a Property. + :param normalize: + if True (DEFAULT) `name` will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + :param replace: + if True (DEFAULT) the given property will replace a present + property. If False a new property will be added always. + The difference to `normalize` is that two or more properties with + the same name may be set, useful for e.g. stuff like:: + + background: red; + background: rgba(255, 0, 0, 0.5); + + which defines the same property but only capable UAs use the last + property value, older ones use the first value. + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified value has a syntax error and is + unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + + if isinstance(name, Property): + newp = name + name = newp.literalname + elif not value: + # empty string or None effectively removed property + return self.removeProperty(name) + else: + newp = Property(name, value, priority) + + if newp.wellformed: + if replace: + # check if update + nname = self._normalize(name) + properties = self.getProperties(name, all=(not normalize)) + for property in reversed(properties): + if normalize and property.name == nname: + property.cssValue = newp.cssValue.cssText + property.priority = newp.priority + return + elif property.literalname == name: + property.cssValue = newp.cssValue.cssText + property.priority = newp.priority + return + + # not yet set or forced omit replace + newp.parent = self + self.seq._readonly = False + self.seq.append(newp, 'Property') + self.seq._readonly = True + + else: + self._log.warn(u'Invalid Property: %s: %s %s' + % (name, value, priority)) + + def item(self, index): + """(DOM) Retrieve the properties that have been explicitly set in + this declaration block. The order of the properties retrieved using + this method does not have to be the order in which they were set. + This method can be used to iterate over all properties in this + declaration block. + + :param index: + of the property to retrieve, negative values behave like + negative indexes on Python lists, so -1 is the last element + + :returns: + the name of the property at this ordinal position. The + empty string if no property exists at this position. + + **ATTENTION:** + Only properties with different names are counted. If two + properties with the same name are present in this declaration + only the effective one is included. + + :meth:`item` and :attr:`length` work on the same set here. + """ + names = list(self.__nnames()) + try: + return names[index] + except IndexError: + return u'' + + length = property(lambda self: len(list(self.__nnames())), + doc=u"(DOM) The number of distinct properties that have " + u"been explicitly in this declaration block. The " + u"range of valid indices is 0 to length-1 inclusive. " + u"These are properties with a different ``name`` " + u"only. :meth:`item` and :attr:`length` work on the " + u"same set here.") + + def _getValidating(self): + try: + # CSSParser.parseX() sets validating of stylesheet + return self.parentRule.parentStyleSheet.validating + except AttributeError: + # CSSParser.parseStyle() sets validating of declaration + if self._validating is not None: + return self._validating + # default + return True + + def _setValidating(self, validating): + self._validating = validating + + validating = property(_getValidating, _setValidating, + doc=u"If ``True`` this declaration validates " + u"contained properties. The parent StyleSheet " + u"validation setting does *always* win though so " + u"even if validating is True it may not validate " + u"if the StyleSheet defines else!") diff --git a/libs/cssutils/css/cssstylerule.py b/libs/cssutils/css/cssstylerule.py new file mode 100755 index 00000000..db85b8b5 --- /dev/null +++ b/libs/cssutils/css/cssstylerule.py @@ -0,0 +1,234 @@ +"""CSSStyleRule implements DOM Level 2 CSS CSSStyleRule.""" +__all__ = ['CSSStyleRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstyledeclaration import CSSStyleDeclaration +from selectorlist import SelectorList +import cssrule +import cssutils +import xml.dom + +class CSSStyleRule(cssrule.CSSRule): + """The CSSStyleRule object represents a ruleset specified (if any) in a CSS + style sheet. It provides access to a declaration block as well as to the + associated group of selectors. + + Format:: + + : selector [ COMMA S* selector ]* + LBRACE S* declaration [ ';' S* declaration ]* '}' S* + ; + """ + def __init__(self, selectorText=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :Parameters: + selectorText + string parsed into selectorList + style + string parsed into CSSStyleDeclaration for this CSSStyleRule + readonly + if True allows setting of properties in constructor only + """ + super(CSSStyleRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self.selectorList = SelectorList() + if selectorText: + self.selectorText = selectorText + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + self._readonly = readonly + + def __repr__(self): + if self._namespaces: + st = (self.selectorText, self._namespaces) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r, style=%r)" % ( + self.__class__.__name__, st, self.style.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self.style.cssText, + self._namespaces, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSStyleRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSStyleRule, self)._setCssText(cssText) + + # might be (cssText, namespaces) + cssText, namespaces = self._splitNamespacesOff(cssText) + try: + # use parent style sheet ones if available + namespaces = self.parentStyleSheet.namespaces + except AttributeError: + pass + + tokenizer = self._tokenize2(cssText) + selectortokens = self._tokensupto2(tokenizer, blockstartonly=True) + styletokens = self._tokensupto2(tokenizer, blockendonly=True) + trail = self._nexttoken(tokenizer) + if trail: + self._log.error(u'CSSStyleRule: Trailing content: %s' % + self._valuestr(cssText), token=trail) + elif not selectortokens: + self._log.error(u'CSSStyleRule: No selector found: %r' % + self._valuestr(cssText)) + elif self._tokenvalue(selectortokens[0]).startswith(u'@'): + self._log.error(u'CSSStyleRule: No style rule: %r' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newSelectorList = SelectorList(parentRule=self) + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + bracetoken = selectortokens.pop() + if self._tokenvalue(bracetoken) != u'{': + ok = False + self._log.error( + u'CSSStyleRule: No start { of style declaration found: %r' % + self._valuestr(cssText), bracetoken) + elif not selectortokens: + ok = False + self._log.error(u'CSSStyleRule: No selector found: %r.' % + self._valuestr(cssText), bracetoken) + # SET + newSelectorList.selectorText = (selectortokens, + namespaces) + + if not styletokens: + ok = False + self._log.error( + u'CSSStyleRule: No style declaration or "}" found: %r' % + self._valuestr(cssText)) + else: + braceorEOFtoken = styletokens.pop() + val, typ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + if val != u'}' and typ != 'EOF': + ok = False + self._log.error(u'CSSStyleRule: No "}" after style ' + u'declaration found: %r' + % self._valuestr(cssText)) + else: + if 'EOF' == typ: + # add again as style needs it + styletokens.append(braceorEOFtoken) + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + self.selectorList = newSelectorList + self.style = newStyle + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def __getNamespaces(self): + """Uses children namespaces if not attached to a sheet, else the sheet's + ones.""" + try: + return self.parentStyleSheet.namespaces + except AttributeError: + return self.selectorList._namespaces + + _namespaces = property(__getNamespaces, + doc=u"If this Rule is attached to a CSSStyleSheet " + u"the namespaces of that sheet are mirrored " + u"here. While the Rule is not attached the " + u"namespaces of selectorList are used.""") + + def _setSelectorList(self, selectorList): + """ + :param selectorList: A SelectorList which replaces the current + selectorList object + """ + self._checkReadonly() + selectorList._parentRule = self + self._selectorList = selectorList + + _selectorList = None + selectorList = property(lambda self: self._selectorList, _setSelectorList, + doc=u"The SelectorList of this rule.") + + def _setSelectorText(self, selectorText): + """ + wrapper for cssutils SelectorList object + + :param selectorText: + of type string, might also be a comma separated list + of selectors + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + sl = SelectorList(selectorText=selectorText, parentRule=self) + if sl.wellformed: + self._selectorList = sl + + selectorText = property(lambda self: self._selectorList.selectorText, + _setSelectorText, + doc=u"(DOM) The textual representation of the " + u"selector for the rule set.") + + def _setStyle(self, style): + """ + :param style: A string or CSSStyleDeclaration which replaces the + current style object. + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set.") + + type = property(lambda self: self.STYLE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + "type constant.") + + wellformed = property(lambda self: self.selectorList.wellformed) diff --git a/libs/cssutils/css/cssstylesheet.py b/libs/cssutils/css/cssstylesheet.py new file mode 100755 index 00000000..88c46b8c --- /dev/null +++ b/libs/cssutils/css/cssstylesheet.py @@ -0,0 +1,804 @@ +"""CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet. + +Partly also: + - http://dev.w3.org/csswg/cssom/#the-cssstylesheet + - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/ + +TODO: + - ownerRule and ownerNode +""" +__all__ = ['CSSStyleSheet'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl +from cssrule import CSSRule +from cssvariablesdeclaration import CSSVariablesDeclaration +import cssutils.stylesheets +import xml.dom + +class CSSStyleSheet(cssutils.stylesheets.StyleSheet): + """CSSStyleSheet represents a CSS style sheet. + + Format:: + + stylesheet + : [ CHARSET_SYM S* STRING S* ';' ]? + [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* + [ namespace [S|CDO|CDC]* ]* # according to @namespace WD + [ [ ruleset | media | page ] [S|CDO|CDC]* ]* + + ``cssRules`` + All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`. + """ + def __init__(self, href=None, media=None, title=u'', disabled=None, + ownerNode=None, parentStyleSheet=None, readonly=False, + ownerRule=None, + validating=True): + """ + For parameters see :class:`~cssutils.stylesheets.StyleSheet` + """ + super(CSSStyleSheet, self).__init__( + 'text/css', href, media, title, disabled, + ownerNode, parentStyleSheet, + validating=validating) + + self._ownerRule = ownerRule + self.cssRules = cssutils.css.CSSRuleList() + self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log) + self._variables = CSSVariablesDeclaration() + self._readonly = readonly + + # used only during setting cssText by parse*() + self.__encodingOverride = None + self._fetcher = None + + def __iter__(self): + "Generator which iterates over cssRules." + for rule in self._cssRules: + yield rule + + def __repr__(self): + if self.media: + mediaText = self.media.mediaText + else: + mediaText = None + return "cssutils.css.%s(href=%r, media=%r, title=%r)" % ( + self.__class__.__name__, + self.href, mediaText, self.title) + + def __str__(self): + if self.media: + mediaText = self.media.mediaText + else: + mediaText = None + return "" % ( + self.__class__.__name__, self.encoding, self.href, + mediaText, self.title, self.namespaces.namespaces, + id(self)) + + def _cleanNamespaces(self): + "Remove all namespace rules with same namespaceURI but last." + rules = self.cssRules + namespaceitems = self.namespaces.items() + i = 0 + while i < len(rules): + rule = rules[i] + if rule.type == rule.NAMESPACE_RULE and \ + (rule.prefix, rule.namespaceURI) not in namespaceitems: + self.deleteRule(i) + else: + i += 1 + + def _getUsedURIs(self): + "Return set of URIs used in the sheet." + useduris = set() + for r1 in self: + if r1.STYLE_RULE == r1.type: + useduris.update(r1.selectorList._getUsedUris()) + elif r1.MEDIA_RULE == r1.type: + for r2 in r1: + if r2.type == r2.STYLE_RULE: + useduris.update(r2.selectorList._getUsedUris()) + return useduris + + def _setCssRules(self, cssRules): + "Set new cssRules and update contained rules refs." + cssRules.append = self.insertRule + cssRules.extend = self.insertRule + cssRules.__delitem__ = self.deleteRule + + for rule in cssRules: + rule._parentStyleSheet = self + + self._cssRules = cssRules + + cssRules = property(lambda self: self._cssRules, _setCssRules, + u"All Rules in this style sheet, a " + u":class:`~cssutils.css.CSSRuleList`.") + + def _getCssText(self): + "Textual representation of the stylesheet (a byte string)." + return cssutils.ser.do_CSSStyleSheet(self) + + def _setCssText(self, cssText): + """Parse `cssText` and overwrites the whole stylesheet. + + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + If a namespace prefix is found which is not declared. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + self._checkReadonly() + + cssText, namespaces = self._splitNamespacesOff(cssText) + tokenizer = self._tokenize2(cssText) + + def S(expected, seq, token, tokenizer=None): + # @charset must be at absolute beginning of style sheet + # or 0 for py3 + return max(1, expected or 0) + + def COMMENT(expected, seq, token, tokenizer=None): + "special: sets parent*" + self.insertRule(cssutils.css.CSSComment([token], + parentStyleSheet=self)) + # or 0 for py3 + return max(1, expected or 0) + + def charsetrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 0: + self._log.error(u'CSSStylesheet: CSSCharsetRule only allowed ' + u'at beginning of stylesheet.', + token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + + return 1 + + def importrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSImportRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 1: + self._log.error(u'CSSStylesheet: CSSImportRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + + return 1 + + def namespacerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSNamespaceRule(cssText=self._tokensupto2(tokenizer, + token), + parentStyleSheet=self) + + if expected > 2: + self._log.error(u'CSSStylesheet: CSSNamespaceRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + if rule.prefix not in self.namespaces: + # add new if not same prefix + self.insertRule(rule, _clean=False) + else: + # same prefix => replace namespaceURI + for r in self.cssRules.rulesOfType(rule.NAMESPACE_RULE): + if r.prefix == rule.prefix: + r._replaceNamespaceURI(rule.namespaceURI) + + self._namespaces[rule.prefix] = rule.namespaceURI + + return 2 + + def variablesrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSVariablesRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 2: + self._log.error(u'CSSStylesheet: CSSVariablesRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + self._updateVariables() + + return 2 + + def fontfacerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def mediarule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSMediaRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def pagerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSPageRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def unknownrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + if token[1] in cssutils.css.MarginRule.margins: + self._log.error(u'CSSStylesheet: MarginRule out CSSPageRule.', + token, neverraise=True) + rule = cssutils.css.MarginRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + else: + self._log.warn(u'CSSStylesheet: Unknown @rule found.', + token, neverraise=True) + rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if rule.wellformed: + self.insertRule(rule) + + # or 0 for py3 + return max(1, expected or 0) + + def ruleset(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSStyleRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + # save for possible reset + oldCssRules = self.cssRules + oldNamespaces = self._namespaces + + self.cssRules = cssutils.css.CSSRuleList() + # simple during parse + self._namespaces = namespaces + self._variables = CSSVariablesDeclaration() + + # not used?! + newseq = [] + + # ['CHARSET', 'IMPORT', ('VAR', NAMESPACE'), ('PAGE', 'MEDIA', ruleset)] + wellformed, expected = self._parse(0, newseq, tokenizer, + {'S': S, + 'COMMENT': COMMENT, + 'CDO': lambda *ignored: None, + 'CDC': lambda *ignored: None, + 'CHARSET_SYM': charsetrule, + 'FONT_FACE_SYM': fontfacerule, + 'IMPORT_SYM': importrule, + 'NAMESPACE_SYM': namespacerule, + 'PAGE_SYM': pagerule, + 'MEDIA_SYM': mediarule, + 'VARIABLES_SYM': variablesrule, + 'ATKEYWORD': unknownrule + }, + default=ruleset) + + if wellformed: + # use proper namespace object + self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log) + self._cleanNamespaces() + + else: + # reset + self._cssRules = oldCssRules + self._namespaces = oldNamespaces + self._updateVariables() + self._cleanNamespaces() + + cssText = property(_getCssText, _setCssText, + "Textual representation of the stylesheet (a byte string)") + + def _resolveImport(self, url): + """Read (encoding, enctype, decodedContent) from `url` for @import + sheets.""" + try: + # only available during parsing of a complete sheet + parentEncoding = self.__newEncoding + + except AttributeError: + try: + # explicit @charset + parentEncoding = self._cssRules[0].encoding + except (IndexError, AttributeError): + # default not UTF-8 but None! + parentEncoding = None + + + return _readUrl(url, fetcher=self._fetcher, + overrideEncoding=self.__encodingOverride, + parentEncoding=parentEncoding) + + def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None, + encoding=None): + """Set `cssText` but use `encodingOverride` to overwrite detected + encoding. This is used by parse and @import during setting of cssText. + + If `encoding` is given use this but do not save as `encodingOverride`. + """ + if encodingOverride: + # encoding during resolving of @import + self.__encodingOverride = encodingOverride + + if encoding: + # save for nested @import + self.__newEncoding = encoding + + self.cssText = cssText + + if encodingOverride: + # set encodingOverride explicit again! + self.encoding = self.__encodingOverride + # del? + self.__encodingOverride = None + elif encoding: + # may e.g. be httpEncoding + self.encoding = encoding + try: + del self.__newEncoding + except AttributeError, e: + pass + + def _setFetcher(self, fetcher=None): + """Set @import URL loader, if None the default is used.""" + self._fetcher = fetcher + + def _getEncoding(self): + """Encoding set in :class:`~cssutils.css.CSSCharsetRule` or if ``None`` + resulting in default ``utf-8`` encoding being used.""" + try: + return self._cssRules[0].encoding + except (IndexError, AttributeError): + return 'utf-8' + + def _setEncoding(self, encoding): + """Set `encoding` of charset rule if present in sheet or insert a new + :class:`~cssutils.css.CSSCharsetRule` with given `encoding`. + If `encoding` is None removes charsetrule if present resulting in + default encoding of utf-8. + """ + try: + rule = self._cssRules[0] + except IndexError: + rule = None + if rule and rule.CHARSET_RULE == rule.type: + if encoding: + rule.encoding = encoding + else: + self.deleteRule(0) + elif encoding: + self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0) + + encoding = property(_getEncoding, _setEncoding, + "(cssutils) Reflect encoding of an @charset rule or 'utf-8' " + "(default) if set to ``None``") + + namespaces = property(lambda self: self._namespaces, + doc="All Namespaces used in this CSSStyleSheet.") + + def _updateVariables(self): + """Updates self._variables, called when @import or @variables rules + is added to sheet. + """ + for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE): + s = r.styleSheet + if s: + for var in s.variables: + self._variables.setVariable(var, s.variables[var]) +# for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE): +# for vr in r.styleSheet.cssRules.rulesOfType(CSSRule.VARIABLES_RULE): +# for var in vr.variables: +# self._variables.setVariable(var, vr.variables[var]) + for vr in self.cssRules.rulesOfType(CSSRule.VARIABLES_RULE): + for var in vr.variables: + self._variables.setVariable(var, vr.variables[var]) + + variables = property(lambda self: self._variables, + doc=u"A :class:`cssutils.css.CSSVariablesDeclaration` " + u"containing all available variables in this " + u"CSSStyleSheet including the ones defined in " + u"imported sheets.") + + def add(self, rule): + """Add `rule` to style sheet at appropriate position. + Same as ``insertRule(rule, inOrder=True)``. + """ + return self.insertRule(rule, index=None, inOrder=True) + + def deleteRule(self, index): + """Delete rule at `index` from the style sheet. + + :param index: + The `index` of the rule to be removed from the StyleSheet's rule + list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is + raised but rules for normal Python lists are used. E.g. + ``deleteRule(-1)`` removes the last rule in cssRules. + + `index` may also be a CSSRule object which will then be removed + from the StyleSheet. + + :exceptions: + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified index does not correspond to a rule in + the style sheet's rule list. + - :exc:`~xml.dom.NamespaceErr`: + Raised if removing this rule would result in an invalid StyleSheet + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this style sheet is readonly. + """ + self._checkReadonly() + + if isinstance(index, CSSRule): + for i, r in enumerate(self.cssRules): + if index == r: + index = i + break + else: + raise xml.dom.IndexSizeErr(u"CSSStyleSheet: Not a rule in" + " this sheets'a cssRules list: %s" + % index) + + try: + rule = self._cssRules[index] + except IndexError: + raise xml.dom.IndexSizeErr( + u'CSSStyleSheet: %s is not a valid index in the rulelist of ' + u'length %i' % (index, self._cssRules.length)) + else: + if rule.type == rule.NAMESPACE_RULE: + # check all namespacerules if used + uris = [r.namespaceURI for r in self + if r.type == r.NAMESPACE_RULE] + useduris = self._getUsedURIs() + if rule.namespaceURI in useduris and\ + uris.count(rule.namespaceURI) == 1: + raise xml.dom.NoModificationAllowedErr( + u'CSSStyleSheet: NamespaceURI defined in this rule is ' + u'used, cannot remove.') + return + + rule._parentStyleSheet = None # detach + del self._cssRules[index] # delete from StyleSheet + + def insertRule(self, rule, index=None, inOrder=False, _clean=True): + """ + Used to insert a new rule into the style sheet. The new rule now + becomes part of the cascade. + + :param rule: + a parsable DOMString, in cssutils also a + :class:`~cssutils.css.CSSRule` or :class:`~cssutils.css.CSSRuleList` + :param index: + of the rule before the new rule will be inserted. + If the specified `index` is equal to the length of the + StyleSheet's rule collection, the rule will be added to the end + of the style sheet. + If `index` is not given or ``None`` rule will be appended to rule + list. + :param inOrder: + if ``True`` the rule will be put to a proper location while + ignoring `index` and without raising + :exc:`~xml.dom.HierarchyRequestErr`. + The resulting index is returned nevertheless. + :returns: The index within the style sheet's rule collection + :Exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at the specified `index` + e.g. if an @import rule is inserted after a standard rule set + or other at-rule. + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified `index` is not a valid insertion point. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this style sheet is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified rule has a syntax error and is + unparsable. + """ + self._checkReadonly() + + # check position + if index is None: + index = len(self._cssRules) + elif index < 0 or index > self._cssRules.length: + raise xml.dom.IndexSizeErr( + u'CSSStyleSheet: Invalid index %s for CSSRuleList with a ' + u'length of %s.' % (index, self._cssRules.length)) + return + + if isinstance(rule, basestring): + # init a temp sheet which has the same properties as self + tempsheet = CSSStyleSheet(href=self.href, + media=self.media, + title=self.title, + parentStyleSheet=self.parentStyleSheet, + ownerRule=self.ownerRule) + tempsheet._ownerNode = self.ownerNode + tempsheet._fetcher = self._fetcher + # prepend encoding if in this sheet to be able to use it in + # @import rules encoding resolution + # do not add if new rule startswith "@charset" (which is exact!) + if not rule.startswith(u'@charset') and (self._cssRules and + self._cssRules[0].type == self._cssRules[0].CHARSET_RULE): + # rule 0 is @charset! + newrulescount, newruleindex = 2, 1 + rule = self._cssRules[0].cssText + rule + else: + newrulescount, newruleindex = 1, 0 + + # parse the new rule(s) + tempsheet.cssText = (rule, self._namespaces) + + if len(tempsheet.cssRules) != newrulescount or (not isinstance( + tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)): + self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule) + return + rule = tempsheet.cssRules[newruleindex] + rule._parentStyleSheet = None # done later? + + # TODO: + #tempsheet._namespaces = self._namespaces + #variables? + + elif isinstance(rule, cssutils.css.CSSRuleList): + # insert all rules + for i, r in enumerate(rule): + self.insertRule(r, index + i) + return index + + if not rule.wellformed: + self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.') + return + + # CHECK HIERARCHY + # @charset + if rule.type == rule.CHARSET_RULE: + if inOrder: + index = 0 + # always first and only + if (self._cssRules + and self._cssRules[0].type == rule.CHARSET_RULE): + self._cssRules[0].encoding = rule.encoding + else: + self._cssRules.insert(0, rule) + elif index != 0 or (self._cssRules and + self._cssRules[0].type == rule.CHARSET_RULE): + self._log.error( + u'CSSStylesheet: @charset only allowed once at the' + ' beginning of a stylesheet.', + error=xml.dom.HierarchyRequestErr) + return + else: + self._cssRules.insert(index, rule) + + # @unknown or comment + elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder: + if index == 0 and self._cssRules and\ + self._cssRules[0].type == rule.CHARSET_RULE: + self._log.error( + u'CSSStylesheet: @charset must be the first rule.', + error=xml.dom.HierarchyRequestErr) + return + else: + self._cssRules.insert(index, rule) + + # @import + elif rule.type == rule.IMPORT_RULE: + if inOrder: + # automatic order + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + if self._cssRules and\ + self._cssRules[0].type in (rule.CHARSET_RULE, + rule.COMMENT): + index = 1 + else: + index = 0 + else: + # after @charset + if index == 0 and self._cssRules and\ + self._cssRules[0].type == rule.CHARSET_RULE: + self._log.error( + u'CSSStylesheet: Found @charset at index 0.', + error=xml.dom.HierarchyRequestErr) + return + # before @namespace @variables @page @font-face @media stylerule + for r in self._cssRules[:index]: + if r.type in (r.NAMESPACE_RULE, + r.VARIABLES_RULE, + r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @import here,' + ' found @namespace, @variables, @media, @page or' + ' CSSStyleRule before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + self._cssRules.insert(index, rule) + self._updateVariables() + + # @namespace + elif rule.type == rule.NAMESPACE_RULE: + if inOrder: + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + for i, r in enumerate(self._cssRules): + if r.type in (r.VARIABLES_RULE, r.MEDIA_RULE, + r.PAGE_RULE, r.STYLE_RULE, + r.FONT_FACE_RULE, r.UNKNOWN_RULE, + r.COMMENT): + index = i # before these + break + else: + # after @charset and @import + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, r.IMPORT_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @namespace here,' + ' found @charset or @import after index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + # before @variables @media @page @font-face and stylerule + for r in self._cssRules[:index]: + if r.type in (r.VARIABLES_RULE, + r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @namespace here,' + ' found @variables, @media, @page or CSSStyleRule' + ' before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + + if not (rule.prefix in self.namespaces and + self.namespaces[rule.prefix] == rule.namespaceURI): + # no doublettes + self._cssRules.insert(index, rule) + if _clean: + self._cleanNamespaces() + + + # @variables + elif rule.type == rule.VARIABLES_RULE: + if inOrder: + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + for i, r in enumerate(self._cssRules): + if r.type in (r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE, + r.UNKNOWN_RULE, + r.COMMENT): + index = i # before these + break + else: + # after @charset @import @namespace + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, + r.IMPORT_RULE, + r.NAMESPACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @variables here,' + ' found @charset, @import or @namespace after' + ' index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + # before @media @page @font-face and stylerule + for r in self._cssRules[:index]: + if r.type in (r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @variables here,' + ' found @media, @page or CSSStyleRule' + ' before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + + self._cssRules.insert(index, rule) + self._updateVariables() + + # all other where order is not important + else: + if inOrder: + # simply add to end as no specific order + self._cssRules.append(rule) + index = len(self._cssRules) - 1 + else: + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, + r.IMPORT_RULE, + r.NAMESPACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert rule here, found ' + u'@charset, @import or @namespace before index %s.' + % index, error=xml.dom.HierarchyRequestErr) + return + self._cssRules.insert(index, rule) + + # post settings + rule._parentStyleSheet = self + + if rule.IMPORT_RULE == rule.type and not rule.hrefFound: + # try loading the imported sheet which has new relative href now + rule.href = rule.href + + return index + + ownerRule = property(lambda self: self._ownerRule, + doc=u'A ref to an @import rule if it is imported, ' + u'else ``None``.') + + + @Deprecated(u'Use ``cssutils.setSerializer(serializer)`` instead.') + def setSerializer(self, cssserializer): + """Set the cssutils global Serializer used for all output.""" + if isinstance(cssserializer, cssutils.CSSSerializer): + cssutils.ser = cssserializer + else: + raise ValueError(u'Serializer must be an instance of ' + u'cssutils.CSSSerializer.') + + @Deprecated(u'Set pref in ``cssutils.ser.prefs`` instead.') + def setSerializerPref(self, pref, value): + """Set a Preference of CSSSerializer used for output. + See :class:`cssutils.serialize.Preferences` for possible + preferences to be set. + """ + cssutils.ser.prefs.__setattr__(pref, value) diff --git a/libs/cssutils/css/cssunknownrule.py b/libs/cssutils/css/cssunknownrule.py new file mode 100755 index 00000000..c9e4361f --- /dev/null +++ b/libs/cssutils/css/cssunknownrule.py @@ -0,0 +1,209 @@ +"""CSSUnknownRule implements DOM Level 2 CSS CSSUnknownRule.""" +__all__ = ['CSSUnknownRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSUnknownRule(cssrule.CSSRule): + """ + Represents an at-rule not supported by this user agent, so in + effect all other at-rules not defined in cssutils. + + Format:: + + @xxx until ';' or block {...} + """ + def __init__(self, cssText=u'', parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param cssText: + of type string + """ + super(CSSUnknownRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = None + if cssText: + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSUnknownRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSUnknownRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if not attoken or self._type(attoken) != self._prods.ATKEYWORD: + self._log.error(u'CSSUnknownRule: No CSSUnknownRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'nesting': [], # {} [] or () + 'wellformed': True + } + + def CHAR(expected, seq, token, tokenizer=None): + type_, val, line, col = token + if expected != 'EOF': + if val in u'{[(': + new['nesting'].append(val) + elif val in u'}])': + opening = {u'}': u'{', u']': u'[', u')': u'('}[val] + try: + if new['nesting'][-1] == opening: + new['nesting'].pop() + else: + raise IndexError() + except IndexError: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Wrong nesting of ' + u'{, [ or (.', token=token) + + if val in u'};' and not new['nesting']: + expected = 'EOF' + + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def FUNCTION(expected, seq, token, tokenizer=None): + # handled as opening ( + type_, val, line, col = token + val = self._tokenvalue(token) + if expected != 'EOF': + new['nesting'].append(u'(') + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def EOF(expected, seq, token, tokenizer=None): + "close all blocks and return 'EOF'" + for x in reversed(new['nesting']): + closing = {u'{': u'}', u'[': u']', u'(': u')'}[x] + seq.append(closing, closing) + new['nesting'] = [] + return 'EOF' + + def INVALID(expected, seq, token, tokenizer=None): + # makes rule invalid + self._log.error(u'CSSUnknownRule: Bad syntax.', + token=token, error=xml.dom.SyntaxErr) + new['wellformed'] = False + return expected + + def STRING(expected, seq, token, tokenizer=None): + type_, val, line, col = token + val = self._stringtokenvalue(token) + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def URI(expected, seq, token, tokenizer=None): + type_, val, line, col = token + val = self._uritokenvalue(token) + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def default(expected, seq, token, tokenizer=None): + type_, val, line, col = token + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + # unknown : ATKEYWORD S* ... ; | } + newseq = self._tempSeq() + wellformed, expected = self._parse(expected=None, + seq=newseq, tokenizer=tokenizer, + productions={'CHAR': CHAR, + 'EOF': EOF, + 'FUNCTION': FUNCTION, + 'INVALID': INVALID, + 'STRING': STRING, + 'URI': URI, + 'S': default # overwrite default default! + }, + default=default, + new=new) + + # wellformed set by parse + wellformed = wellformed and new['wellformed'] + + # post conditions + if expected != 'EOF': + wellformed = False + self._log.error(u'CSSUnknownRule: No ending ";" or "}" found: ' + u'%r' % self._valuestr(cssText)) + elif new['nesting']: + wellformed = False + self._log.error(u'CSSUnknownRule: Unclosed "{", "[" or "(": %r' + % self._valuestr(cssText)) + + # set all + if wellformed: + self.atkeyword = self._tokenvalue(attoken) + self._setSeq(newseq) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + type = property(lambda self: self.UNKNOWN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.atkeyword)) + \ No newline at end of file diff --git a/libs/cssutils/css/cssvalue.py b/libs/cssutils/css/cssvalue.py new file mode 100755 index 00000000..2f643453 --- /dev/null +++ b/libs/cssutils/css/cssvalue.py @@ -0,0 +1,1251 @@ +"""CSSValue related classes + +- CSSValue implements DOM Level 2 CSS CSSValue +- CSSPrimitiveValue implements DOM Level 2 CSS CSSPrimitiveValue +- CSSValueList implements DOM Level 2 CSS CSSValueList + +""" +__all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList', 'RGBColor', + 'CSSVariable'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +import cssutils +import cssutils.helper +import math +import re +import xml.dom + + +class CSSValue(cssutils.util._NewBase): + """The CSSValue interface represents a simple or a complex value. + A CSSValue object only occurs in a context of a CSS property. + """ + + # The value is inherited and the cssText contains "inherit". + CSS_INHERIT = 0 + # The value is a CSSPrimitiveValue. + CSS_PRIMITIVE_VALUE = 1 + # The value is a CSSValueList. + CSS_VALUE_LIST = 2 + # The value is a custom value. + CSS_CUSTOM = 3 + # The value is a CSSVariable. + CSS_VARIABLE = 4 + + _typestrings = {0: 'CSS_INHERIT' , + 1: 'CSS_PRIMITIVE_VALUE', + 2: 'CSS_VALUE_LIST', + 3: 'CSS_CUSTOM', + 4: 'CSS_VARIABLE'} + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSValue, self).__init__() + + self._cssValueType = None + self.wellformed = False + self.parent = parent + if cssText is not None: # may be 0 + if isinstance(cssText, int): + cssText = unicode(cssText) # if it is an integer + elif isinstance(cssText, float): + cssText = u'%f' % cssText # if it is a floating point number + + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.cssValueTypeString, + self.cssText, + id(self)) + + def _setCssText(self, cssText): + """ + Format:: + + unary_operator + : '-' | '+' + ; + operator + : '/' S* | ',' S* | /* empty */ + ; + expr + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* ] + | STRING S* | IDENT S* | URI S* | hexcolor | function + | UNICODE-RANGE S* + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a + different type of values than the values allowed by the CSS + property. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this value is readonly. + """ + self._checkReadonly() + + # used as operator is , / or S + nextSor = u',/' + + term = Choice(Sequence(PreDef.unary(), + Choice(PreDef.number(nextSor=nextSor), + PreDef.percentage(nextSor=nextSor), + PreDef.dimension(nextSor=nextSor))), + PreDef.string(nextSor=nextSor), + PreDef.ident(nextSor=nextSor), + PreDef.uri(nextSor=nextSor), + PreDef.hexcolor(nextSor=nextSor), + PreDef.unicode_range(nextSor=nextSor), + # special case IE only expression + Prod(name='expression', + match=lambda t, v: t == self._prods.FUNCTION and ( + cssutils.helper.normalize(v) in (u'expression(', + u'alpha(', + u'blur(', + u'chroma(', + u'dropshadow(', + u'fliph(', + u'flipv(', + u'glow(', + u'gray(', + u'invert(', + u'mask(', + u'shadow(', + u'wave(', + u'xray(') or + v.startswith(u'progid:DXImageTransform.Microsoft.') + ), + nextSor=nextSor, + toSeq=lambda t, tokens: (ExpressionValue._functionName, + ExpressionValue( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), + # CSS Variable var( + PreDef.variable(nextSor=nextSor, + toSeq=lambda t, tokens: ('CSSVariable', + CSSVariable( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), + # calc( + PreDef.calc(nextSor=nextSor, + toSeq=lambda t, tokens: (CalcValue._functionName, + CalcValue( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), +# TODO: +# # rgb/rgba( +# Prod(name='RGBColor', +# match=lambda t, v: t == self._prods.FUNCTION and ( +# cssutils.helper.normalize(v) in (u'rgb(', +# u'rgba(' +# ) +# ), +# nextSor=nextSor, +# toSeq=lambda t, tokens: (RGBColor._functionName, +# RGBColor( +# cssutils.helper.pushtoken(t, tokens), +# parent=self) +# ) +# ), + # other functions like rgb( etc + PreDef.function(nextSor=nextSor, + toSeq=lambda t, tokens: ('FUNCTION', + CSSFunction( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ) + ) + operator = Choice(PreDef.S(), + PreDef.char('comma', ',', + toSeq=lambda t, tokens: ('operator', t[1])), + PreDef.char('slash', '/', + toSeq=lambda t, tokens: ('operator', t[1])), + optional=True) + # CSSValue PRODUCTIONS + valueprods = Sequence(term, + Sequence(operator, # mayEnd this Sequence if whitespace + + # TODO: only when setting via other class + # used by variabledeclaration currently + PreDef.char('END', ';', + stopAndKeep=True, + optional=True), + + term, + minmax=lambda: (0, None))) + # parse + wellformed, seq, store, notused = ProdParser().parse(cssText, + u'CSSValue', + valueprods, + keepS=True) + if wellformed: + # - count actual values and set firstvalue which is used later on + # - combine comma separated list, e.g. font-family to a single item + # - remove S which should be an operator but is no needed + count, firstvalue = 0, () + newseq = self._tempSeq() + i, end = 0, len(seq) + while i < end: + item = seq[i] + if item.type == self._prods.S: + pass + + elif (item.value, item.type) == (u',', 'operator'): + # , separared counts as a single STRING for now + # URI or STRING value might be a single CHAR too! + newseq.appendItem(item) + count -= 1 + if firstvalue: + # list of IDENTs is handled as STRING! + if firstvalue[1] == self._prods.IDENT: + firstvalue = firstvalue[0], 'STRING' + + elif item.value == u'/': + # / separated items count as one + newseq.appendItem(item) + + elif item.value == u'-' or item.value == u'+': + # combine +- and following number or other + i += 1 + try: + next = seq[i] + except IndexError: + firstvalue = () # raised later + break + + newval = item.value + next.value + newseq.append(newval, next.type, + item.line, item.col) + if not firstvalue: + firstvalue = (newval, next.type) + count += 1 + + elif item.type != cssutils.css.CSSComment: + newseq.appendItem(item) + if not firstvalue: + firstvalue = (item.value, item.type) + count += 1 + + else: + newseq.appendItem(item) + + i += 1 + + if not firstvalue: + self._log.error( + u'CSSValue: Unknown syntax or no value: %r.' % + self._valuestr(cssText)) + else: + # ok and set + self._setSeq(newseq) + self.wellformed = wellformed + + if hasattr(self, '_value'): + # only in case of CSSPrimitiveValue, else remove! + del self._value + + if count == 1: + # inherit, primitive or variable + if isinstance(firstvalue[0], basestring) and\ + u'inherit' == cssutils.helper.normalize(firstvalue[0]): + self.__class__ = CSSValue + self._cssValueType = CSSValue.CSS_INHERIT + elif 'CSSVariable' == firstvalue[1]: + self.__class__ = CSSVariable + self._value = firstvalue + # TODO: remove major hack! + self._name = firstvalue[0]._name + else: + self.__class__ = CSSPrimitiveValue + self._value = firstvalue + + elif count > 1: + # valuelist + self.__class__ = CSSValueList + + # change items in list to specific type (primitive etc) + newseq = self._tempSeq() + commalist = [] + nexttocommalist = False + + def itemValue(item): + "Reserialized simple item.value" + if self._prods.STRING == item.type: + return cssutils.helper.string(item.value) + elif self._prods.URI == item.type: + return cssutils.helper.uri(item.value) + elif self._prods.FUNCTION == item.type or\ + 'CSSVariable' == item.type: + return item.value.cssText + else: + return item.value + + def saveifcommalist(commalist, newseq): + """ + saves items in commalist to seq and items + if anything in there + """ + if commalist: + newseq.replace(-1, + CSSPrimitiveValue(cssText=u''.join( + commalist)), + CSSPrimitiveValue, + newseq[-1].line, + newseq[-1].col) + del commalist[:] + + for i, item in enumerate(self._seq): + if issubclass(type(item.value), CSSValue): + # set parent of CSSValueList items to the lists + # parent + item.value.parent = self.parent + + if item.type in (self._prods.DIMENSION, + self._prods.FUNCTION, + self._prods.HASH, + self._prods.IDENT, + self._prods.NUMBER, + self._prods.PERCENTAGE, + self._prods.STRING, + self._prods.URI, + self._prods.UNICODE_RANGE, + 'CSSVariable'): + if nexttocommalist: + # wait until complete + commalist.append(itemValue(item)) + else: + saveifcommalist(commalist, newseq) + # append new item + if hasattr(item.value, 'cssText'): + newseq.append(item.value, + item.value.__class__, + item.line, item.col) + + else: + newseq.append(CSSPrimitiveValue( + itemValue(item)), + CSSPrimitiveValue, + item.line, item.col) + + nexttocommalist = False + + elif u',' == item.value: + if not commalist: + # save last item to commalist + commalist.append(itemValue(self._seq[i - 1])) + commalist.append(u',') + nexttocommalist = True + + else: + if nexttocommalist: + commalist.append(item.value.cssText) + else: + newseq.appendItem(item) + + saveifcommalist(commalist, newseq) + self._setSeq(newseq) + + else: + # should not happen... + self.__class__ = CSSValue + self._cssValueType = CSSValue.CSS_CUSTOM + + cssText = property(lambda self: cssutils.ser.do_css_CSSValue(self), + _setCssText, + doc="A string representation of the current value.") + + cssValueType = property(lambda self: self._cssValueType, + doc="A (readonly) code defining the type of the value.") + + cssValueTypeString = property( + lambda self: CSSValue._typestrings.get(self.cssValueType, None), + doc="(readonly) Name of cssValueType.") + + +class CSSPrimitiveValue(CSSValue): + """Represents a single CSS Value. May be used to determine the value of a + specific style property currently set in a block or to set a specific + style property explicitly within the block. Might be obtained from the + getPropertyCSSValue method of CSSStyleDeclaration. + + Conversions are allowed between absolute values (from millimeters to + centimeters, from degrees to radians, and so on) but not between + relative values. (For example, a pixel value cannot be converted to a + centimeter value.) Percentage values can't be converted since they are + relative to the parent value (or another property value). There is one + exception for color percentage values: since a color percentage value + is relative to the range 0-255, a color percentage value can be + converted to a number; (see also the RGBColor interface). + """ + # constant: type of this CSSValue class + cssValueType = CSSValue.CSS_PRIMITIVE_VALUE + + __types = cssutils.cssproductions.CSSProductions + + # An integer indicating which type of unit applies to the value. + CSS_UNKNOWN = 0 # only obtainable via cssText + CSS_NUMBER = 1 + CSS_PERCENTAGE = 2 + CSS_EMS = 3 + CSS_EXS = 4 + CSS_PX = 5 + CSS_CM = 6 + CSS_MM = 7 + CSS_IN = 8 + CSS_PT = 9 + CSS_PC = 10 + CSS_DEG = 11 + CSS_RAD = 12 + CSS_GRAD = 13 + CSS_MS = 14 + CSS_S = 15 + CSS_HZ = 16 + CSS_KHZ = 17 + CSS_DIMENSION = 18 + CSS_STRING = 19 + CSS_URI = 20 + CSS_IDENT = 21 + CSS_ATTR = 22 + CSS_COUNTER = 23 + CSS_RECT = 24 + CSS_RGBCOLOR = 25 + # NOT OFFICIAL: + CSS_RGBACOLOR = 26 + CSS_UNICODE_RANGE = 27 + + _floattypes = (CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, + CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC, + CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS, CSS_S, + CSS_HZ, CSS_KHZ, CSS_DIMENSION) + _stringtypes = (CSS_ATTR, CSS_IDENT, CSS_STRING, CSS_URI) + _countertypes = (CSS_COUNTER,) + _recttypes = (CSS_RECT,) + _rbgtypes = (CSS_RGBCOLOR, CSS_RGBACOLOR) + _lengthtypes = (CSS_NUMBER, CSS_EMS, CSS_EXS, + CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC) + + # oldtype: newType: converterfunc + _converter = { + # cm <-> mm <-> in, 1 inch is equal to 2.54 centimeters. + # pt <-> pc, the points used by CSS 2.1 are equal to 1/72nd of an inch. + # pc: picas - 1 pica is equal to 12 points + (CSS_CM, CSS_MM): lambda x: x * 10, + (CSS_MM, CSS_CM): lambda x: x / 10, + + (CSS_PT, CSS_PC): lambda x: x * 12, + (CSS_PC, CSS_PT): lambda x: x / 12, + + (CSS_CM, CSS_IN): lambda x: x / 2.54, + (CSS_IN, CSS_CM): lambda x: x * 2.54, + (CSS_MM, CSS_IN): lambda x: x / 25.4, + (CSS_IN, CSS_MM): lambda x: x * 25.4, + + (CSS_IN, CSS_PT): lambda x: x / 72, + (CSS_PT, CSS_IN): lambda x: x * 72, + (CSS_CM, CSS_PT): lambda x: x / 2.54 / 72, + (CSS_PT, CSS_CM): lambda x: x * 72 * 2.54, + (CSS_MM, CSS_PT): lambda x: x / 25.4 / 72, + (CSS_PT, CSS_MM): lambda x: x * 72 * 25.4, + + (CSS_IN, CSS_PC): lambda x: x / 72 / 12, + (CSS_PC, CSS_IN): lambda x: x * 12 * 72, + (CSS_CM, CSS_PC): lambda x: x / 2.54 / 72 / 12, + (CSS_PC, CSS_CM): lambda x: x * 12 * 72 * 2.54, + (CSS_MM, CSS_PC): lambda x: x / 25.4 / 72 / 12, + (CSS_PC, CSS_MM): lambda x: x * 12 * 72 * 25.4, + + # hz <-> khz + (CSS_KHZ, CSS_HZ): lambda x: x * 1000, + (CSS_HZ, CSS_KHZ): lambda x: x / 1000, + # s <-> ms + (CSS_S, CSS_MS): lambda x: x * 1000, + (CSS_MS, CSS_S): lambda x: x / 1000, + + (CSS_RAD, CSS_DEG): lambda x: math.degrees(x), + (CSS_DEG, CSS_RAD): lambda x: math.radians(x), + # TODO: convert grad <-> deg or rad + #(CSS_RAD, CSS_GRAD): lambda x: math.degrees(x), + #(CSS_DEG, CSS_GRAD): lambda x: math.radians(x), + #(CSS_GRAD, CSS_RAD): lambda x: math.radians(x), + #(CSS_GRAD, CSS_DEG): lambda x: math.radians(x) + } + + def __init__(self, cssText=None, parent=None, readonly=False): + """See CSSPrimitiveValue.__init__()""" + super(CSSPrimitiveValue, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.primitiveTypeString, + self.cssText, + id(self)) + + _unitnames = ['CSS_UNKNOWN', + 'CSS_NUMBER', 'CSS_PERCENTAGE', + 'CSS_EMS', 'CSS_EXS', + 'CSS_PX', + 'CSS_CM', 'CSS_MM', + 'CSS_IN', + 'CSS_PT', 'CSS_PC', + 'CSS_DEG', 'CSS_RAD', 'CSS_GRAD', + 'CSS_MS', 'CSS_S', + 'CSS_HZ', 'CSS_KHZ', + 'CSS_DIMENSION', + 'CSS_STRING', 'CSS_URI', 'CSS_IDENT', + 'CSS_ATTR', 'CSS_COUNTER', 'CSS_RECT', + 'CSS_RGBCOLOR', 'CSS_RGBACOLOR', + 'CSS_UNICODE_RANGE' + ] + + _reNumDim = re.compile(ur'([+-]?\d*\.\d+|[+-]?\d+)(.*)$', re.I | re.U | re.X) + + def _unitDIMENSION(value): + """Check val for dimension name.""" + units = {'em': 'CSS_EMS', 'ex': 'CSS_EXS', + 'px': 'CSS_PX', + 'cm': 'CSS_CM', 'mm': 'CSS_MM', + 'in': 'CSS_IN', + 'pt': 'CSS_PT', 'pc': 'CSS_PC', + 'deg': 'CSS_DEG', 'rad': 'CSS_RAD', 'grad': 'CSS_GRAD', + 'ms': 'CSS_MS', 's': 'CSS_S', + 'hz': 'CSS_HZ', 'khz': 'CSS_KHZ' + } + val, dim = CSSPrimitiveValue._reNumDim.findall(cssutils.helper.normalize(value))[0] + return units.get(dim, 'CSS_DIMENSION') + + def _unitFUNCTION(value): + """Check val for function name.""" + units = {'attr(': 'CSS_ATTR', + 'counter(': 'CSS_COUNTER', + 'rect(': 'CSS_RECT', + 'rgb(': 'CSS_RGBCOLOR', + 'rgba(': 'CSS_RGBACOLOR', + } + return units.get(re.findall(ur'^(.*?\()', + cssutils.helper.normalize(value.cssText), + re.U)[0], + 'CSS_UNKNOWN') + + __unitbytype = { + __types.NUMBER: 'CSS_NUMBER', + __types.PERCENTAGE: 'CSS_PERCENTAGE', + __types.STRING: 'CSS_STRING', + __types.UNICODE_RANGE: 'CSS_UNICODE_RANGE', + __types.URI: 'CSS_URI', + __types.IDENT: 'CSS_IDENT', + __types.HASH: 'CSS_RGBCOLOR', + __types.DIMENSION: _unitDIMENSION, + __types.FUNCTION: _unitFUNCTION + } + + def __set_primitiveType(self): + """primitiveType is readonly but is set lazy if accessed""" + # TODO: check unary and font-family STRING a, b, "c" + val, type_ = self._value + # try get by type_ + pt = self.__unitbytype.get(type_, 'CSS_UNKNOWN') + if callable(pt): + # multiple options, check value too + pt = pt(val) + self._primitiveType = getattr(self, pt) + + def _getPrimitiveType(self): + if not hasattr(self, '_primitivetype'): + self.__set_primitiveType() + return self._primitiveType + + primitiveType = property(_getPrimitiveType, + doc="(readonly) The type of the value as defined " + "by the constants in this class.") + + def _getPrimitiveTypeString(self): + return self._unitnames[self.primitiveType] + + primitiveTypeString = property(_getPrimitiveTypeString, + doc="Name of primitive type of this value.") + + def _getCSSPrimitiveTypeString(self, type): + "get TypeString by given type which may be unknown, used by setters" + try: + return self._unitnames[type] + except (IndexError, TypeError): + return u'%r (UNKNOWN TYPE)' % type + + def _getNumDim(self, value=None): + "Split self._value in numerical and dimension part." + if value is None: + value = cssutils.helper.normalize(self._value[0]) + + try: + val, dim = CSSPrimitiveValue._reNumDim.findall(value)[0] + except IndexError: + val, dim = value, u'' + try: + val = float(val) + if val == int(val): + val = int(val) + except ValueError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: No float value %r' % self._value[0]) + + return val, dim + + def getFloatValue(self, unitType=None): + """(DOM) This method is used to get a float value in a + specified unit. If this CSS value doesn't contain a float value + or can't be converted into the specified unit, a DOMException + is raised. + + :param unitType: + to get the float value. The unit code can only be a float unit type + (i.e. CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, CSS_PX, CSS_CM, + CSS_MM, CSS_IN, CSS_PT, CSS_PC, CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS, + CSS_S, CSS_HZ, CSS_KHZ, CSS_DIMENSION) or None in which case + the current dimension is used. + + :returns: + not necessarily a float but some cases just an integer + e.g. if the value is ``1px`` it return ``1`` and **not** ``1.0`` + + Conversions might return strange values like 1.000000000001 + """ + if unitType is not None and unitType not in self._floattypes: + raise xml.dom.InvalidAccessErr( + u'unitType Parameter is not a float type') + + val, dim = self._getNumDim() + + if unitType is not None and self.primitiveType != unitType: + # convert if needed + try: + val = self._converter[self.primitiveType, unitType](val) + except KeyError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(unitType))) + + if val == int(val): + val = int(val) + + return val + + def setFloatValue(self, unitType, floatValue): + """(DOM) A method to set the float value with a specified unit. + If the property attached with this value can not accept the + specified unit or the float value, the value will be unchanged and + a DOMException will be raised. + + :param unitType: + a unit code as defined above. The unit code can only be a float + unit type + :param floatValue: + the new float value which does not have to be a float value but + may simple be an int e.g. if setting:: + + setFloatValue(CSS_PX, 1) + + :exceptions: + - :exc:`~xml.dom.InvalidAccessErr`: + Raised if the attached property doesn't + support the float value or the unit type. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this property is readonly. + """ + self._checkReadonly() + if unitType not in self._floattypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: unitType %r is not a float type' % + self._getCSSPrimitiveTypeString(unitType)) + try: + val = float(floatValue) + except ValueError, e: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: floatValue %r is not a float' % + floatValue) + + oldval, dim = self._getNumDim() + if self.primitiveType != unitType: + # convert if possible + try: + val = self._converter[unitType, self.primitiveType](val) + except KeyError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(unitType))) + + if val == int(val): + val = int(val) + + self.cssText = '%s%s' % (val, dim) + + def getStringValue(self): + """(DOM) This method is used to get the string value. If the + CSS value doesn't contain a string value, a DOMException is raised. + + Some properties (like 'font-family' or 'voice-family') + convert a whitespace separated list of idents to a string. + + Only the actual value is returned so e.g. all the following return the + actual value ``a``: url(a), attr(a), "a", 'a' + """ + if self.primitiveType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue %r is not a string type' + % self.primitiveTypeString) + + if CSSPrimitiveValue.CSS_ATTR == self.primitiveType: + return self._value[0].cssText[5:-1] + else: + return self._value[0] + + def setStringValue(self, stringType, stringValue): + """(DOM) A method to set the string value with the specified + unit. If the property attached to this value can't accept the + specified unit or the string value, the value will be unchanged and + a DOMException will be raised. + + :param stringType: + a string code as defined above. The string code can only be a + string unit type (i.e. CSS_STRING, CSS_URI, CSS_IDENT, and + CSS_ATTR). + :param stringValue: + the new string value + Only the actual value is expected so for (CSS_URI, "a") the + new value will be ``url(a)``. For (CSS_STRING, "'a'") + the new value will be ``"\\'a\\'"`` as the surrounding ``'`` are + not part of the string value + + :exceptions: + - :exc:`~xml.dom.InvalidAccessErr`: + Raised if the CSS value doesn't contain a + string value or if the string value can't be converted into + the specified unit. + + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this property is readonly. + """ + self._checkReadonly() + # self not stringType + if self.primitiveType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue %r is not a string type' + % self.primitiveTypeString) + # given stringType is no StringType + if stringType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: stringType %s is not a string type' + % self._getCSSPrimitiveTypeString(stringType)) + + if self._primitiveType != stringType: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(stringType))) + + if CSSPrimitiveValue.CSS_STRING == self._primitiveType: + self.cssText = cssutils.helper.string(stringValue) + elif CSSPrimitiveValue.CSS_URI == self._primitiveType: + self.cssText = cssutils.helper.uri(stringValue) + elif CSSPrimitiveValue.CSS_ATTR == self._primitiveType: + self.cssText = u'attr(%s)' % stringValue + else: + self.cssText = stringValue + self._primitiveType = stringType + + def getCounterValue(self): + """(DOM) This method is used to get the Counter value. If + this CSS value doesn't contain a counter value, a DOMException + is raised. Modification to the corresponding style property + can be achieved using the Counter interface. + + **Not implemented.** + """ + if not self.CSS_COUNTER == self.primitiveType: + raise xml.dom.InvalidAccessErr(u'Value is not a counter type') + # TODO: use Counter class + raise NotImplementedError() + + def getRGBColorValue(self): + """(DOM) This method is used to get the RGB color. If this + CSS value doesn't contain a RGB color value, a DOMException + is raised. Modification to the corresponding style property + can be achieved using the RGBColor interface. + """ + if self.primitiveType not in self._rbgtypes: + raise xml.dom.InvalidAccessErr(u'Value is not a RGBColor value') + return RGBColor(self._value[0]) + + def getRectValue(self): + """(DOM) This method is used to get the Rect value. If this CSS + value doesn't contain a rect value, a DOMException is raised. + Modification to the corresponding style property can be achieved + using the Rect interface. + + **Not implemented.** + """ + if self.primitiveType not in self._recttypes: + raise xml.dom.InvalidAccessErr(u'value is not a Rect value') + # TODO: use Rect class + raise NotImplementedError() + + def _getCssText(self): + """Overwrites CSSValue.""" + return cssutils.ser.do_css_CSSPrimitiveValue(self) + + def _setCssText(self, cssText): + """Use CSSValue.""" + return super(CSSPrimitiveValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc="A string representation of the current value.") + + +class CSSValueList(CSSValue): + """The CSSValueList interface provides the abstraction of an ordered + collection of CSS values. + + Some properties allow an empty list into their syntax. In that case, + these properties take the none identifier. So, an empty list means + that the property has the value none. + + The items in the CSSValueList are accessible via an integral index, + starting from 0. + """ + cssValueType = CSSValue.CSS_VALUE_LIST + + def __init__(self, cssText=None, parent=None, readonly=False): + """Init a new CSSValueList""" + super(CSSValueList, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + self._items = [] + + def __iter__(self): + "CSSValueList is iterable." + for item in self.__items(): + yield item.value + + def __str__(self): + return u"" % (self.__class__.__name__, + self.cssValueTypeString, + self.cssText, + self.length, + id(self)) + + def __items(self): + return [item for item in self._seq + if isinstance(item.value, CSSValue)] + + def item(self, index): + """(DOM) Retrieve a CSSValue by ordinal `index`. The + order in this collection represents the order of the values in the + CSS style property. If `index` is greater than or equal to the number + of values in the list, this returns ``None``. + """ + try: + return self.__items()[index].value + except IndexError: + return None + + length = property(lambda self: len(self.__items()), + doc=u"(DOM attribute) The number of CSSValues in the " + u"list.") + + +class CSSFunction(CSSPrimitiveValue): + """A CSS function value like rect() etc.""" + _functionName = u'CSSFunction' + primitiveType = CSSPrimitiveValue.CSS_UNKNOWN + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + Init a new CSSFunction + + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSFunction, self).__init__(parent=parent) + self._funcType = None + self.valid = False + self.wellformed = False + if cssText is not None: + self.cssText = cssText + self._readonly = readonly + + def _productiondefinition(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + value = Sequence(PreDef.unary(), + Prod(name='PrimitiveValue', + match=lambda t, v: t in (types.DIMENSION, + types.HASH, + types.IDENT, + types.NUMBER, + types.PERCENTAGE, + types.STRING), + toSeq=lambda t, tokens: (t[0], + CSSPrimitiveValue(t[1])) + ) + ) + valueOrFunc = Choice(value, + # FUNC is actually not in spec but used in e.g. Prince + PreDef.function(toSeq=lambda t, + tokens: ('FUNCTION', + CSSFunction( + cssutils.helper.pushtoken(t, tokens)) + ) + ) + ) + funcProds = Sequence(Prod(name='FUNC', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1]))), + Choice(Sequence(valueOrFunc, + # more values starting with Comma + # should use store where colorType is saved to + # define min and may, closure? + Sequence(PreDef.comma(), + valueOrFunc, + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)), + PreDef.funcEnd(stop=True)) + ) + return funcProds + + def _setCssText(self, cssText): + self._checkReadonly() + # store: colorType, parts + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + self._functionName, + self._productiondefinition(), + keepS=True) + if wellformed: + # combine +/- and following CSSPrimitiveValue, remove S + newseq = self._tempSeq() + i, end = 0, len(seq) + while i < end: + item = seq[i] + if item.type == self._prods.S: + pass + elif item.value == u'+' or item.value == u'-': + i += 1 + next = seq[i] + newval = next.value + if isinstance(newval, CSSPrimitiveValue): + newval.setFloatValue(newval.primitiveType, + float(item.value + str(newval.getFloatValue()))) + newseq.append(newval, next.type, + item.line, item.col) + else: + # expressions only? + newseq.appendItem(item) + newseq.appendItem(next) + else: + newseq.appendItem(item) + + i += 1 + + self.wellformed = True + self._setSeq(newseq) + self._funcType = newseq[0].value + + cssText = property(lambda self: cssutils.ser.do_css_FunctionValue(self), + _setCssText) + + funcType = property(lambda self: self._funcType) + + +class RGBColor(CSSFunction): + """A CSS color like RGB, RGBA or a simple value like `#000` or `red`.""" + + _functionName = u'Function rgb()' + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + Init a new RGBColor + + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSFunction, self).__init__(parent=parent) + self._colorType = None + self.valid = False + self.wellformed = False + if cssText is not None: + try: + # if it is a Function object + cssText = cssText.cssText + except AttributeError: + pass + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.colorType, + self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + types = self._prods # rename! + valueProd = Prod(name='value', + match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE), + toSeq=lambda t, v: (CSSPrimitiveValue, CSSPrimitiveValue(v)), + toStore='parts' + ) + # COLOR PRODUCTION + funccolor = Sequence(Prod(name='FUNC', + match=lambda t, v: t == types.FUNCTION and cssutils.helper.normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla('), + toSeq=lambda t, v: (t, v),#cssutils.helper.normalize(v)), + toStore='colorType'), + PreDef.unary(), + valueProd, + # 2 or 3 more values starting with Comma + Sequence(PreDef.comma(), + PreDef.unary(), + valueProd, + minmax=lambda: (2, 3)), + PreDef.funcEnd() + ) + colorprods = Choice(funccolor, + PreDef.hexcolor('colorType'), + Prod(name='named color', + match=lambda t, v: t == types.IDENT, + toStore='colorType' + ) + ) + # store: colorType, parts + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + u'RGBColor', + colorprods, + keepS=True, + store={'parts': []}) + + if wellformed: + self.wellformed = True + if store['colorType'].type == self._prods.HASH: + self._colorType = 'HEX' + elif store['colorType'].type == self._prods.IDENT: + self._colorType = 'Named Color' + else: + self._colorType = store['colorType'].value[:-1] + #self._colorType = cssutils.helper.normalize(store['colorType'].value)[:-1] + + self._setSeq(seq) + + cssText = property(lambda self: cssutils.ser.do_css_RGBColor(self), + _setCssText) + + colorType = property(lambda self: self._colorType) + + +class CalcValue(CSSFunction): + """Calc Function""" + _functionName = u'Function calc()' + + def _productiondefinition(self): + """Return defintion used for parsing.""" + types = self._prods # rename! + + def toSeq(t, tokens): + "Do not normalize function name!" + return t[0], t[1] + + funcProds = Sequence(Prod(name='calc', + match=lambda t, v: t == types.FUNCTION, + toSeq=toSeq + ), + Sequence(Choice(Prod(name='nested function', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (CSSFunction._functionName, + CSSFunction(cssutils.helper.pushtoken(t, + tokens))) + ), + Prod(name='part', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1])), + ), + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)) + return funcProds + + def _getCssText(self): + return cssutils.ser.do_css_CalcValue(self) + + def _setCssText(self, cssText): + return super(CalcValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc=u"A string representation of the current value.") + + +class ExpressionValue(CSSFunction): + """Special IE only CSSFunction which may contain *anything*. + Used for expressions and ``alpha(opacity=100)`` currently.""" + _functionName = u'Expression (IE only)' + + def _productiondefinition(self): + """Return defintion used for parsing.""" + types = self._prods # rename! + + def toSeq(t, tokens): + "Do not normalize function name!" + return t[0], t[1] + + funcProds = Sequence(Prod(name='expression', + match=lambda t, v: t == types.FUNCTION, + toSeq=toSeq + ), + Sequence(Choice(Prod(name='nested function', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (ExpressionValue._functionName, + ExpressionValue(cssutils.helper.pushtoken(t, + tokens))) + ), + Prod(name='part', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1])), + ), + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)) + return funcProds + + def _getCssText(self): + return cssutils.ser.do_css_ExpressionValue(self) + + def _setCssText(self, cssText): + #self._log.warn(u'CSSValue: Unoffial and probably invalid MS value used!') + return super(ExpressionValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc=u"A string representation of the current value.") + + +class CSSVariable(CSSValue): + """The CSSVariable represents a call to CSS Variable.""" + + def __init__(self, cssText=None, parent=None, readonly=False): + """Init a new CSSVariable. + + :param cssText: + the parsable cssText of the value, e.g. ``var(x)`` + :param readonly: + defaults to False + """ + self._name = None + super(CSSVariable, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.name, + self.value, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + types = self._prods # rename! + + funcProds = Sequence(Prod(name='var', + match=lambda t, v: t == types.FUNCTION + ), + PreDef.ident(toStore='ident'), + PreDef.funcEnd(stop=True)) + + # store: name of variable + store = {'ident': None} + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + u'CSSVariable', + funcProds, + keepS=True) + if wellformed: + self._name = store['ident'].value + self._setSeq(seq) + self.wellformed = True + + cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self), + _setCssText, + doc=u"A string representation of the current variable.") + + cssValueType = CSSValue.CSS_VARIABLE + + # TODO: writable? check if var (value) available? + name = property(lambda self: self._name) + + def _getValue(self): + "Find contained sheet and @variables there" + try: + variables = self.parent.parent.parentRule.parentStyleSheet.variables + except AttributeError: + return None + else: + try: + return variables[self.name] + except KeyError: + return None + + value = property(_getValue) diff --git a/libs/cssutils/css/cssvariablesdeclaration.py b/libs/cssutils/css/cssvariablesdeclaration.py new file mode 100755 index 00000000..2b33f670 --- /dev/null +++ b/libs/cssutils/css/cssvariablesdeclaration.py @@ -0,0 +1,330 @@ +"""CSSVariablesDeclaration +http://disruptive-innovations.com/zoo/cssvariables/#mozTocId496530 +""" +__all__ = ['CSSVariablesDeclaration'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssstyledeclaration.py 1819 2009-08-01 20:52:43Z cthedot $' + +from cssutils.prodparser import * +from cssutils.helper import normalize +from value import PropertyValue +import cssutils +import itertools +import xml.dom + +class CSSVariablesDeclaration(cssutils.util._NewBase): + """The CSSVariablesDeclaration interface represents a single block of + variable declarations. + """ + def __init__(self, cssText=u'', parentRule=None, readonly=False): + """ + :param cssText: + Shortcut, sets CSSVariablesDeclaration.cssText + :param parentRule: + The CSS rule that contains this declaration block or + None if this CSSVariablesDeclaration is not attached to a CSSRule. + :param readonly: + defaults to False + + Format:: + + variableset + : vardeclaration [ ';' S* vardeclaration ]* S* + ; + + vardeclaration + : varname ':' S* term + ; + + varname + : IDENT S* + ; + """ + super(CSSVariablesDeclaration, self).__init__() + self._parentRule = parentRule + self._vars = {} + if cssText: + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.length, + id(self)) + + def __contains__(self, variableName): + """Check if a variable is in variable declaration block. + + :param variableName: + a string + """ + return normalize(variableName) in self.keys() + + def __getitem__(self, variableName): + """Retrieve the value of variable ``variableName`` from this + declaration. + """ + return self.getVariableValue(variableName) + + def __setitem__(self, variableName, value): + self.setVariable(variableName, value) + + def __delitem__(self, variableName): + return self.removeVariable(variableName) + + def __iter__(self): + """Iterator of names of set variables.""" + for name in self.keys(): + yield name + + def keys(self): + """Analoguous to standard dict returns variable names which are set in + this declaration.""" + return self._vars.keys() + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_css_CSSVariablesDeclaration(self) + + def _setCssText(self, cssText): + """Setting this attribute will result in the parsing of the new value + and resetting of all the properties in the declaration block + including the removal or addition of properties. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or a property is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + + Format:: + + variableset + : vardeclaration [ ';' S* vardeclaration ]* + ; + + vardeclaration + : varname ':' S* term + ; + + varname + : IDENT S* + ; + + expr + : [ VARCALL | term ] [ operator [ VARCALL | term ] ]* + ; + + """ + self._checkReadonly() + + vardeclaration = Sequence( + PreDef.ident(), + PreDef.char(u':', u':', toSeq=False), + #PreDef.S(toSeq=False, optional=True), + Prod(name=u'term', match=lambda t, v: True, + toSeq=lambda t, tokens: (u'value', + PropertyValue(itertools.chain([t], + tokens), + parent=self) + ) + ) + ) + prods = Sequence(vardeclaration, + Sequence(PreDef.S(optional=True), + PreDef.char(u';', u';', toSeq=False), + PreDef.S(optional=True), + vardeclaration, + minmax=lambda: (0, None)), + PreDef.S(optional=True), + PreDef.char(u';', u';', toSeq=False, optional=True) + ) + # parse + wellformed, seq, store, notused = \ + ProdParser().parse(cssText, + u'CSSVariableDeclaration', + prods) + if wellformed: + newseq = self._tempSeq() + newvars = {} + + # seq contains only name: value pairs plus comments etc + nameitem = None + for item in seq: + if u'IDENT' == item.type: + nameitem = item + elif u'value' == item.type: + nname = normalize(nameitem.value) + if nname in newvars: + # replace var with same name + for i, it in enumerate(newseq): + if normalize(it.value[0]) == nname: + newseq.replace(i, + (nameitem.value, item.value), + 'var', + nameitem.line, nameitem.col) + else: + # saved non normalized name for reserialization + newseq.append((nameitem.value, item.value), + 'var', + nameitem.line, nameitem.col) + +# newseq.append((nameitem.value, item.value), +# 'var', +# nameitem.line, nameitem.col) + + newvars[nname] = item.value + + else: + newseq.appendItem(item) + + self._setSeq(newseq) + self._vars = newvars + self.wellformed = True + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) A parsable textual representation of the declaration " + u"block excluding the surrounding curly braces.") + + def _setParentRule(self, parentRule): + self._parentRule = parentRule + + parentRule = property(lambda self: self._parentRule, _setParentRule, + doc=u"(DOM) The CSS rule that contains this" + u" declaration block or None if this block" + u" is not attached to a CSSRule.") + + def getVariableValue(self, variableName): + """Used to retrieve the value of a variable if it has been explicitly + set within this variable declaration block. + + :param variableName: + The name of the variable. + :returns: + the value of the variable if it has been explicitly set in this + variable declaration block. Returns the empty string if the + variable has not been set. + """ + try: + return self._vars[normalize(variableName)].cssText + except KeyError, e: + return u'' + + def removeVariable(self, variableName): + """Used to remove a variable if it has been explicitly set within this + variable declaration block. + + :param variableName: + The name of the variable. + :returns: + the value of the variable if it has been explicitly set for this + variable declaration block. Returns the empty string if the + variable has not been set. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly is readonly. + """ + normalname = variableName + try: + r = self._vars[normalname] + except KeyError, e: + return u'' + else: + self.seq._readonly = False + if normalname in self._vars: + for i, x in enumerate(self.seq): + if x.value[0] == variableName: + del self.seq[i] + self.seq._readonly = True + del self._vars[normalname] + + return r.cssText + + def setVariable(self, variableName, value): + """Used to set a variable value within this variable declaration block. + + :param variableName: + The name of the CSS variable. + :param value: + The new value of the variable, may also be a PropertyValue object. + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified value has a syntax error and is + unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + + # check name + wellformed, seq, store, unused = \ + ProdParser().parse(normalize(variableName), + u'variableName', + Sequence(PreDef.ident())) + if not wellformed: + self._log.error(u'Invalid variableName: %r: %r' + % (variableName, value)) + else: + # check value + if isinstance(value, PropertyValue): + v = value + else: + v = PropertyValue(cssText=value, parent=self) + + if not v.wellformed: + self._log.error(u'Invalid variable value: %r: %r' + % (variableName, value)) + else: + # update seq + self.seq._readonly = False + + variableName = normalize(variableName) + + if variableName in self._vars: + for i, x in enumerate(self.seq): + if x.value[0] == variableName: + self.seq.replace(i, + [variableName, v], + x.type, + x.line, + x.col) + break + else: + self.seq.append([variableName, v], 'var') + self.seq._readonly = True + self._vars[variableName] = v + + def item(self, index): + """Used to retrieve the variables that have been explicitly set in + this variable declaration block. The order of the variables + retrieved using this method does not have to be the order in which + they were set. This method can be used to iterate over all variables + in this variable declaration block. + + :param index: + of the variable name to retrieve, negative values behave like + negative indexes on Python lists, so -1 is the last element + + :returns: + The name of the variable at this ordinal position. The empty + string if no variable exists at this position. + """ + try: + return self.keys()[index] + except IndexError: + return u'' + + length = property(lambda self: len(self._vars), + doc=u"The number of variables that have been explicitly set in this" + u" variable declaration block. The range of valid indices is 0" + u" to length-1 inclusive.") diff --git a/libs/cssutils/css/cssvariablesrule.py b/libs/cssutils/css/cssvariablesrule.py new file mode 100755 index 00000000..ce5335bf --- /dev/null +++ b/libs/cssutils/css/cssvariablesrule.py @@ -0,0 +1,198 @@ +"""CSSVariables implements (and only partly) experimental +`CSS Variables `_ +""" +__all__ = ['CSSVariablesRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssfontfacerule.py 1818 2009-07-30 21:39:00Z cthedot $' + +from cssvariablesdeclaration import CSSVariablesDeclaration +import cssrule +import cssutils +import xml.dom + +class CSSVariablesRule(cssrule.CSSRule): + """ + The CSSVariablesRule interface represents a @variables rule within a CSS + style sheet. The @variables rule is used to specify variables. + + cssutils uses a :class:`~cssutils.css.CSSVariablesDeclaration` to + represent the variables. + + Format:: + + variables + VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S* + variableset* '}' S* + ; + + for variableset see :class:`cssutils.css.CSSVariablesDeclaration` + + **Media are not implemented. Reason is that cssutils is using CSS + variables in a kind of preprocessing and therefor no media information + is available at this stage. For now do not use media!** + + Example:: + + @variables { + CorporateLogoBGColor: #fe8d12; + } + + div.logoContainer { + background-color: var(CorporateLogoBGColor); + } + """ + def __init__(self, mediaText=None, variables=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + """ + super(CSSVariablesRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@variables' + + # dummy + self._media = cssutils.stylesheets.MediaList(mediaText, + readonly=readonly) + + if variables: + self.variables = variables + else: + self.variables = CSSVariablesDeclaration(parentRule=self) + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(mediaText=%r, variables=%r)" % ( + self.__class__.__name__, + self._media.mediaText, + self.variables.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self._media.mediaText, + self.variables.cssText, + self.valid, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSVariablesRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + + Format:: + + variables + : VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S* + variableset* '}' S* + ; + + variableset + : LBRACE S* vardeclaration [ ';' S* vardeclaration ]* '}' S* + ; + """ + super(CSSVariablesRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.VARIABLES_SYM: + self._log.error(u'CSSVariablesRule: No CSSVariablesRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newVariables = CSSVariablesDeclaration(parentRule=self) + ok = True + + beforetokens, brace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + if self._tokenvalue(brace) != u'{': + ok = False + self._log.error(u'CSSVariablesRule: No start { of variable ' + u'declaration found: %r' + % self._valuestr(cssText), brace) + + # parse stuff before { which should be comments and S only + new = {'wellformed': True} + newseq = self._tempSeq()#[] + + beforewellformed, expected = self._parse(expected=':', + seq=newseq, tokenizer=self._tokenize2(beforetokens), + productions={}) + ok = ok and beforewellformed and new['wellformed'] + + variablestokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + + val, type_ = self._tokenvalue(braceorEOFtoken), \ + self._type(braceorEOFtoken) + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error(u'CSSVariablesRule: No "}" after variables ' + u'declaration found: %r' + % self._valuestr(cssText)) + + nonetoken = self._nexttoken(tokenizer) + if nonetoken: + ok = False + self._log.error(u'CSSVariablesRule: Trailing content found.', + token=nonetoken) + + if 'EOF' == type_: + # add again as variables needs it + variablestokens.append(braceorEOFtoken) + # SET but may raise: + newVariables.cssText = variablestokens + + if ok: + # contains probably comments only upto { + self._setSeq(newseq) + self.variables = newVariables + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + media = property(doc=u"NOT IMPLEMENTED! As cssutils resolves variables "\ + u"during serializing media information is lost.") + + def _setVariables(self, variables): + """ + :param variables: + a CSSVariablesDeclaration or string + """ + self._checkReadonly() + if isinstance(variables, basestring): + self._variables = CSSVariablesDeclaration(cssText=variables, + parentRule=self) + else: + variables._parentRule = self + self._variables = variables + + variables = property(lambda self: self._variables, _setVariables, + doc=u"(DOM) The variables of this rule set, a " + u":class:`cssutils.css.CSSVariablesDeclaration`.") + + type = property(lambda self: self.VARIABLES_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + valid = property(lambda self: True, doc='NOT IMPLEMTED REALLY (TODO)') + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/marginrule.py b/libs/cssutils/css/marginrule.py new file mode 100755 index 00000000..0c789fad --- /dev/null +++ b/libs/cssutils/css/marginrule.py @@ -0,0 +1,215 @@ +"""MarginRule implements DOM Level 2 CSS MarginRule.""" +__all__ = ['MarginRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +from cssstyledeclaration import CSSStyleDeclaration +import cssrule +import cssutils +import xml.dom + +class MarginRule(cssrule.CSSRule): + """ + A margin at-rule consists of an ATKEYWORD that identifies the margin box + (e.g. '@top-left') and a block of declarations (said to be in the margin + context). + + Format:: + + margin : + margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* + ; + + margin_sym : + TOPLEFTCORNER_SYM | + TOPLEFT_SYM | + TOPCENTER_SYM | + TOPRIGHT_SYM | + TOPRIGHTCORNER_SYM | + BOTTOMLEFTCORNER_SYM | + BOTTOMLEFT_SYM | + BOTTOMCENTER_SYM | + BOTTOMRIGHT_SYM | + BOTTOMRIGHTCORNER_SYM | + LEFTTOP_SYM | + LEFTMIDDLE_SYM | + LEFTBOTTOM_SYM | + RIGHTTOP_SYM | + RIGHTMIDDLE_SYM | + RIGHTBOTTOM_SYM + ; + + e.g.:: + + @top-left { + content: "123"; + } + """ + margins = ['@top-left-corner', + '@top-left', + '@top-center', + '@top-right', + '@top-right-corner', + '@bottom-left-corner', + '@bottom-left', + '@bottom-center', + '@bottom-right', + '@bottom-right-corner', + '@left-top', + '@left-middle', + '@left-bottom', + '@right-top', + '@right-middle', + '@right-bottom' + ] + + def __init__(self, margin=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param atkeyword: + The margin area, e.g. '@top-left' for this rule + :param style: + CSSStyleDeclaration for this MarginRule + """ + super(MarginRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self._atkeyword = self._keyword = None + + if margin: + self.margin = margin + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration(parentRule=self) + + self._readonly = readonly + + def _setMargin(self, margin): + """Check if new keyword fits the rule it is used for.""" + n = self._normalize(margin) + + if n not in MarginRule.margins: + self._log.error(u'Invalid margin @keyword for this %s rule: %r' % + (self.margin, margin), + error=xml.dom.InvalidModificationErr) + + else: + self._atkeyword = n + self._keyword = margin + + margin = property(lambda self: self._atkeyword, _setMargin, + doc=u"Margin area of parent CSSPageRule. " + u"`margin` and `atkeyword` are both normalized " + u"@keyword of the @rule.") + + atkeyword = margin + + def __repr__(self): + return u"cssutils.css.%s(margin=%r, style=%r)" % (self.__class__.__name__, + self.margin, + self.style.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.margin, + self.style.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_MarginRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(MarginRule, self)._setCssText(cssText) + + # TEMP: all style tokens are saved in store to fill styledeclaration + # TODO: resolve when all generators + styletokens = Prod(name='styletokens', + match=lambda t, v: v != u'}', + #toSeq=False, + toStore='styletokens', + storeToken=True + ) + + prods = Sequence(Prod(name='@ margin', + match=lambda t, v: + t == 'ATKEYWORD' and + self._normalize(v) in MarginRule.margins, + toStore='margin' + # TODO? + #, exception=xml.dom.InvalidModificationErr + ), + PreDef.char('OPEN', u'{'), + Sequence(Choice(PreDef.unknownrule(toStore='@'), + styletokens), + minmax=lambda: (0, None) + ), + PreDef.char('CLOSE', u'}', stopAndKeep=True) + ) + # parse + ok, seq, store, unused = ProdParser().parse(cssText, + u'MarginRule', + prods) + + if ok: + # TODO: use seq for serializing instead of fixed stuff? + self._setSeq(seq) + + if 'margin' in store: + # may raise: + self.margin = store['margin'].value + else: + self._log.error(u'No margin @keyword for this %s rule' % + self.margin, + error=xml.dom.InvalidModificationErr) + + # new empty style + self.style = CSSStyleDeclaration(parentRule=self) + + if 'styletokens' in store: + # may raise: + self.style.cssText = store['styletokens'] + + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + def _setStyle(self, style): + """ + :param style: A string or CSSStyleDeclaration which replaces the + current style object. + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set.") + + type = property(lambda self: self.MARGIN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.atkeyword)) + \ No newline at end of file diff --git a/libs/cssutils/css/property.py b/libs/cssutils/css/property.py new file mode 100755 index 00000000..fad240c0 --- /dev/null +++ b/libs/cssutils/css/property.py @@ -0,0 +1,510 @@ +"""Property is a single CSS property in a CSSStyleDeclaration.""" +__all__ = ['Property'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from value import PropertyValue +import cssutils +import xml.dom + +class Property(cssutils.util.Base): + """A CSS property in a StyleDeclaration of a CSSStyleRule (cssutils). + + Format:: + + property = name + : IDENT S* + ; + + expr = value + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* | function ] + | STRING S* | IDENT S* | URI S* | hexcolor + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + prio + : IMPORTANT_SYM S* + ; + + """ + def __init__(self, name=None, value=None, priority=u'', + _mediaQuery=False, parent=None): + """ + :param name: + a property name string (will be normalized) + :param value: + a property value string + :param priority: + an optional priority string which currently must be u'', + u'!important' or u'important' + :param _mediaQuery: + if ``True`` value is optional (used by MediaQuery) + :param parent: + the parent object, normally a + :class:`cssutils.css.CSSStyleDeclaration` + """ + super(Property, self).__init__() + self.seqs = [[], None, []] + self.wellformed = False + self._mediaQuery = _mediaQuery + self.parent = parent + + self.__nametoken = None + self._name = u'' + self._literalname = u'' + self.seqs[1] = PropertyValue(parent=self) + if name: + self.name = name + self.propertyValue = value + + self._priority = u'' + self._literalpriority = u'' + if priority: + self.priority = priority + + def __repr__(self): + return u"cssutils.css.%s(name=%r, value=%r, priority=%r)" % ( + self.__class__.__name__, + self.literalname, + self.propertyValue.cssText, + self.priority) + + def __str__(self): + return u"<%s.%s object name=%r value=%r priority=%r valid=%r at 0x%x>" \ + % (self.__class__.__module__, + self.__class__.__name__, + self.name, + self.propertyValue.cssText, + self.priority, + self.valid, + id(self)) + + def _isValidating(self): + """Return True if validation is enabled.""" + try: + return self.parent.validating + except AttributeError: + # default (no parent) + return True + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_Property(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + # check and prepare tokenlists for setting + tokenizer = self._tokenize2(cssText) + nametokens = self._tokensupto2(tokenizer, propertynameendonly=True) + if nametokens: + wellformed = True + + valuetokens = self._tokensupto2(tokenizer, + propertyvalueendonly=True) + prioritytokens = self._tokensupto2(tokenizer, + propertypriorityendonly=True) + + if self._mediaQuery and not valuetokens: + # MediaQuery may consist of name only + self.name = nametokens + self.propertyValue = None + self.priority = None + return + + # remove colon from nametokens + colontoken = nametokens.pop() + if self._tokenvalue(colontoken) != u':': + wellformed = False + self._log.error(u'Property: No ":" after name found: %s' % + self._valuestr(cssText), colontoken) + elif not nametokens: + wellformed = False + self._log.error(u'Property: No property name found: %s' % + self._valuestr(cssText), colontoken) + + if valuetokens: + if self._tokenvalue(valuetokens[-1]) == u'!': + # priority given, move "!" to prioritytokens + prioritytokens.insert(0, valuetokens.pop(-1)) + else: + wellformed = False + self._log.error(u'Property: No property value found: %s' % + self._valuestr(cssText), colontoken) + + if wellformed: + self.wellformed = True + self.name = nametokens + self.propertyValue = valuetokens + self.priority = prioritytokens + + # also invalid values are set! + + if self._isValidating(): + self.validate() + + else: + self._log.error(u'Property: No property name found: %s' % + self._valuestr(cssText)) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc="A parsable textual representation.") + + def _setName(self, name): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified name has a syntax error and is + unparsable. + """ + # for closures: must be a mutable + new = {'literalname': None, + 'wellformed': True} + + def _ident(expected, seq, token, tokenizer=None): + # name + if 'name' == expected: + new['literalname'] = self._tokenvalue(token).lower() + seq.append(new['literalname']) + return 'EOF' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected ident.', token) + return expected + + newseq = [] + wellformed, expected = self._parse(expected='name', + seq=newseq, + tokenizer=self._tokenize2(name), + productions={'IDENT': _ident}) + wellformed = wellformed and new['wellformed'] + + # post conditions + # define a token for error logging + if isinstance(name, list): + token = name[0] + self.__nametoken = token + else: + token = None + + if not new['literalname']: + wellformed = False + self._log.error(u'Property: No name found: %s' % + self._valuestr(name), token=token) + + if wellformed: + self.wellformed = True + self._literalname = new['literalname'] + self._name = self._normalize(self._literalname) + self.seqs[0] = newseq + + # validate + if self._isValidating() and self._name not in cssutils.profile.knownNames: + # self.valid = False + self._log.warn(u'Property: Unknown Property name.', + token=token, neverraise=True) + else: + pass +# self.valid = True +# if self.propertyValue: +# self.propertyValue._propertyName = self._name +# #self.valid = self.propertyValue.valid + else: + self.wellformed = False + + name = property(lambda self: self._name, _setName, + doc="Name of this property.") + + literalname = property(lambda self: self._literalname, + doc="Readonly literal (not normalized) name " + "of this property") + + def _setPropertyValue(self, cssText): + """ + See css.PropertyValue + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a different + type of values than the values allowed by the CSS property. + """ + if self._mediaQuery and not cssText: + self.seqs[1] = PropertyValue(parent=self) + else: + self.seqs[1].cssText = cssText + self.wellformed = self.wellformed and self.seqs[1].wellformed + + propertyValue = property(lambda self: self.seqs[1], + _setPropertyValue, + doc=u"(cssutils) PropertyValue object of property") + + + def _getValue(self): + if self.propertyValue: + # value without comments + return self.propertyValue.value + else: + return u'' + + def _setValue(self, value): + self._setPropertyValue(value) + + value = property(_getValue, _setValue, + doc="The textual value of this Properties propertyValue.") + + def _setPriority(self, priority): + """ + priority + a string, currently either u'', u'!important' or u'important' + + Format:: + + prio + : IMPORTANT_SYM S* + ; + + "!"{w}"important" {return IMPORTANT_SYM;} + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified priority has a syntax error and is + unparsable. + In this case a priority not equal to None, "" or "!{w}important". + As CSSOM defines CSSStyleDeclaration.getPropertyPriority resulting + in u'important' this value is also allowed to set a Properties + priority + """ + if self._mediaQuery: + self._priority = u'' + self._literalpriority = u'' + if priority: + self._log.error(u'Property: No priority in a MediaQuery - ' + u'ignored.') + return + + if isinstance(priority, basestring) and\ + u'important' == self._normalize(priority): + priority = u'!%s' % priority + + # for closures: must be a mutable + new = {'literalpriority': u'', + 'wellformed': True} + + def _char(expected, seq, token, tokenizer=None): + # "!" + val = self._tokenvalue(token) + if u'!' == expected == val: + seq.append(val) + return 'important' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected char.', token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # "important" + val = self._tokenvalue(token) + if 'important' == expected: + new['literalpriority'] = val + seq.append(val) + return 'EOF' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected ident.', token) + return expected + + newseq = [] + wellformed, expected = self._parse(expected='!', + seq=newseq, + tokenizer=self._tokenize2(priority), + productions={'CHAR': _char, + 'IDENT': _ident}) + wellformed = wellformed and new['wellformed'] + + # post conditions + if priority and not new['literalpriority']: + wellformed = False + self._log.info(u'Property: Invalid priority: %s' % + self._valuestr(priority)) + + if wellformed: + self.wellformed = self.wellformed and wellformed + self._literalpriority = new['literalpriority'] + self._priority = self._normalize(self.literalpriority) + self.seqs[2] = newseq + # validate priority + if self._priority not in (u'', u'important'): + self._log.error(u'Property: No CSS priority value: %s' % + self._priority) + + priority = property(lambda self: self._priority, _setPriority, + doc="Priority of this property.") + + literalpriority = property(lambda self: self._literalpriority, + doc="Readonly literal (not normalized) priority of this property") + + def _setParent(self, parent): + self._parent = parent + + parent = property(lambda self: self._parent, _setParent, + doc="The Parent Node (normally a CSSStyledeclaration) of this " + "Property") + + def validate(self): + """Validate value against `profiles` which are checked dynamically. + properties in e.g. @font-face rules are checked against + ``cssutils.profile.CSS3_FONT_FACE`` only. + + For each of the following cases a message is reported: + + - INVALID (so the property is known but not valid) + ``ERROR Property: Invalid value for "{PROFILE-1[/PROFILE-2...]" + property: ...`` + + - VALID but not in given profiles or defaultProfiles + ``WARNING Property: Not valid for profile "{PROFILE-X}" but valid + "{PROFILE-Y}" property: ...`` + + - VALID in current profile + ``DEBUG Found valid "{PROFILE-1[/PROFILE-2...]" property...`` + + - UNKNOWN property + ``WARNING Unknown Property name...`` is issued + + so for example:: + + cssutils.log.setLevel(logging.DEBUG) + parser = cssutils.CSSParser() + s = parser.parseString('''body { + unknown-property: x; + color: 4; + color: rgba(1,2,3,4); + color: red + }''') + + # Log output: + + WARNING Property: Unknown Property name. [2:9: unknown-property] + ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color] + DEBUG Property: Found valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color] + DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color] + + + and when setting an explicit default profile:: + + cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2 + s = parser.parseString('''body { + unknown-property: x; + color: 4; + color: rgba(1,2,3,4); + color: red + }''') + + # Log output: + + WARNING Property: Unknown Property name. [2:9: unknown-property] + ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color] + WARNING Property: Not valid for profile "CSS Level 2.1" but valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color] + DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color] + """ + valid = False + + profiles = None + try: + # if @font-face use that profile + rule = self.parent.parentRule + except AttributeError: + pass + else: + if rule is not None: + if rule.type == rule.FONT_FACE_RULE: + profiles = [cssutils.profile.CSS3_FONT_FACE] + #TODO: same for @page + + if self.name and self.value: + + cv = self.propertyValue + # TODO +# if cv.cssValueType == cv.CSS_VARIABLE and not cv.value: +# # TODO: false alarms too! +# cssutils.log.warn(u'No value for variable "%s" found, keeping ' +# u'variable.' % cv.name, neverraise=True) + + if self.name in cssutils.profile.knownNames: + # add valid, matching, validprofiles... + valid, matching, validprofiles = \ + cssutils.profile.validateWithProfile(self.name, + self.value, + profiles) + + if not valid: + self._log.error(u'Property: Invalid value for ' + u'"%s" property: %s' + % (u'/'.join(validprofiles), self.value), + token=self.__nametoken, + neverraise=True) + + # TODO: remove logic to profiles! + elif valid and not matching:#(profiles and profiles not in validprofiles): + if not profiles: + notvalidprofiles = u'/'.join(cssutils.profile.defaultProfiles) + else: + notvalidprofiles = profiles + self._log.warn(u'Property: Not valid for profile "%s" ' + u'but valid "%s" value: %s ' + % (notvalidprofiles, u'/'.join(validprofiles), + self.value), + token = self.__nametoken, + neverraise=True) + valid = False + + elif valid: + self._log.debug(u'Property: Found valid "%s" value: %s' + % (u'/'.join(validprofiles), self.value), + token = self.__nametoken, + neverraise=True) + + if self._priority not in (u'', u'important'): + valid = False + + return valid + + valid = property(validate, doc=u"Check if value of this property is valid " + u"in the properties context.") + + + @Deprecated(u'Use ``property.propertyValue`` instead.') + def _getCSSValue(self): + return self.propertyValue + + @Deprecated(u'Use ``property.propertyValue`` instead.') + def _setCSSValue(self, cssText): + self._setPropertyValue(cssText) + + cssValue = property(_getCSSValue, _setCSSValue, + doc="(DEPRECATED) Use ``property.propertyValue`` instead.") diff --git a/libs/cssutils/css/selector.py b/libs/cssutils/css/selector.py new file mode 100755 index 00000000..87840a0d --- /dev/null +++ b/libs/cssutils/css/selector.py @@ -0,0 +1,813 @@ +"""Selector is a single Selector of a CSSStyleRule SelectorList. +Partly implements http://www.w3.org/TR/css3-selectors/. + +TODO + - .contains(selector) + - .isSubselector(selector) +""" +__all__ = ['Selector'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from cssutils.util import _SimpleNamespaces +import cssutils +import xml.dom + +class Selector(cssutils.util.Base2): + """ + (cssutils) a single selector in a :class:`~cssutils.css.SelectorList` + of a :class:`~cssutils.css.CSSStyleRule`. + + Format:: + + # implemented in SelectorList + selectors_group + : selector [ COMMA S* selector ]* + ; + + selector + : simple_selector_sequence [ combinator simple_selector_sequence ]* + ; + + combinator + /* combinators can be surrounded by white space */ + : PLUS S* | GREATER S* | TILDE S* | S+ + ; + + simple_selector_sequence + : [ type_selector | universal ] + [ HASH | class | attrib | pseudo | negation ]* + | [ HASH | class | attrib | pseudo | negation ]+ + ; + + type_selector + : [ namespace_prefix ]? element_name + ; + + namespace_prefix + : [ IDENT | '*' ]? '|' + ; + + element_name + : IDENT + ; + + universal + : [ namespace_prefix ]? '*' + ; + + class + : '.' IDENT + ; + + attrib + : '[' S* [ namespace_prefix ]? IDENT S* + [ [ PREFIXMATCH | + SUFFIXMATCH | + SUBSTRINGMATCH | + '=' | + INCLUDES | + DASHMATCH ] S* [ IDENT | STRING ] S* + ]? ']' + ; + + pseudo + /* '::' starts a pseudo-element, ':' a pseudo-class */ + /* Exceptions: :first-line, :first-letter, :before and :after. */ + /* Note that pseudo-elements are restricted to one per selector and */ + /* occur only in the last simple_selector_sequence. */ + : ':' ':'? [ IDENT | functional_pseudo ] + ; + + functional_pseudo + : FUNCTION S* expression ')' + ; + + expression + /* In CSS3, the expressions are identifiers, strings, */ + /* or of the form "an+b" */ + : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ + ; + + negation + : NOT S* negation_arg S* ')' + ; + + negation_arg + : type_selector | universal | HASH | class | attrib | pseudo + ; + + """ + def __init__(self, selectorText=None, parent=None, + readonly=False): + """ + :Parameters: + selectorText + initial value of this selector + parent + a SelectorList + readonly + default to False + """ + super(Selector, self).__init__() + + self.__namespaces = _SimpleNamespaces(log=self._log) + self._element = None + self._parent = parent + self._specificity = (0, 0, 0, 0) + + if selectorText: + self.selectorText = selectorText + + self._readonly = readonly + + def __repr__(self): + if self.__getNamespaces(): + st = (self.selectorText, self._getUsedNamespaces()) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__, + st) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self.specificity, + self._getUsedNamespaces(), + id(self)) + + def _getUsedUris(self): + "Return list of actually used URIs in this Selector." + uris = set() + for item in self.seq: + type_, val = item.type, item.value + if type_.endswith(u'-selector') or type_ == u'universal' and \ + isinstance(val, tuple) and val[0] not in (None, u'*'): + uris.add(val[0]) + return uris + + def _getUsedNamespaces(self): + "Return actually used namespaces only." + useduris = self._getUsedUris() + namespaces = _SimpleNamespaces(log=self._log) + for p, uri in self._namespaces.items(): + if uri in useduris: + namespaces[p] = uri + return namespaces + + def __getNamespaces(self): + "Use own namespaces if not attached to a sheet, else the sheet's ones." + try: + return self._parent.parentRule.parentStyleSheet.namespaces + except AttributeError: + return self.__namespaces + + _namespaces = property(__getNamespaces, + doc=u"If this Selector is attached to a " + u"CSSStyleSheet the namespaces of that sheet " + u"are mirrored here. While the Selector (or " + u"parent SelectorList or parentRule(s) of that " + u"are not attached a own dict of {prefix: " + u"namespaceURI} is used.") + + + element = property(lambda self: self._element, + doc=u"Effective element target of this selector.") + + parent = property(lambda self: self._parent, + doc=u"(DOM) The SelectorList that contains this Selector " + u"or None if this Selector is not attached to a " + u"SelectorList.") + + def _getSelectorText(self): + """Return serialized format.""" + return cssutils.ser.do_css_Selector(self) + + def _setSelectorText(self, selectorText): + """ + :param selectorText: + parsable string or a tuple of (selectorText, dict-of-namespaces). + Given namespaces are ignored if this object is attached to a + CSSStyleSheet! + + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + selectorText, namespaces = self._splitNamespacesOff(selectorText) + + try: + # uses parent stylesheets namespaces if available, + # otherwise given ones + namespaces = self.parent.parentRule.parentStyleSheet.namespaces + except AttributeError: + pass + tokenizer = self._tokenize2(selectorText) + if not tokenizer: + self._log.error(u'Selector: No selectorText given.') + else: + # prepare tokenlist: + # "*" -> type "universal" + # "*"|IDENT + "|" -> combined to "namespace_prefix" + # "|" -> type "namespace_prefix" + # "." + IDENT -> combined to "class" + # ":" + IDENT, ":" + FUNCTION -> pseudo-class + # FUNCTION "not(" -> negation + # "::" + IDENT, "::" + FUNCTION -> pseudo-element + tokens = [] + for t in tokenizer: + typ, val, lin, col = t + if val == u':' and tokens and\ + self._tokenvalue(tokens[-1]) == ':': + # combine ":" and ":" + tokens[-1] = (typ, u'::', lin, col) + + elif typ == 'IDENT' and tokens\ + and self._tokenvalue(tokens[-1]) == u'.': + # class: combine to .IDENT + tokens[-1] = ('class', u'.'+val, lin, col) + elif typ == 'IDENT' and tokens and \ + self._tokenvalue(tokens[-1]).startswith(u':') and\ + not self._tokenvalue(tokens[-1]).endswith(u'('): + # pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b" + if self._tokenvalue(tokens[-1]).startswith(u'::'): + t = 'pseudo-element' + else: + t = 'pseudo-class' + tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col) + + elif typ == 'FUNCTION' and val == u'not(' and tokens and \ + u':' == self._tokenvalue(tokens[-1]): + tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3]) + elif typ == 'FUNCTION' and tokens\ + and self._tokenvalue(tokens[-1]).startswith(u':'): + # pseudo-X: combine to :FUNCTION( or ::FUNCTION( + if self._tokenvalue(tokens[-1]).startswith(u'::'): + t = 'pseudo-element' + else: + t = 'pseudo-class' + tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col) + + elif val == u'*' and tokens and\ + self._type(tokens[-1]) == 'namespace_prefix' and\ + self._tokenvalue(tokens[-1]).endswith(u'|'): + # combine prefix|* + tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val, + lin, col) + elif val == u'*': + # universal: "*" + tokens.append(('universal', val, lin, col)) + + elif val == u'|' and tokens and\ + self._type(tokens[-1]) in (self._prods.IDENT, 'universal')\ + and self._tokenvalue(tokens[-1]).find(u'|') == -1: + # namespace_prefix: "IDENT|" or "*|" + tokens[-1] = ('namespace_prefix', + self._tokenvalue(tokens[-1])+u'|', lin, col) + elif val == u'|': + # namespace_prefix: "|" + tokens.append(('namespace_prefix', val, lin, col)) + + else: + tokens.append(t) + + tokenizer = iter(tokens) + + # for closures: must be a mutable + new = {'context': [''], # stack of: 'attrib', 'negation', 'pseudo' + 'element': None, + '_PREFIX': None, + 'specificity': [0, 0, 0, 0], # mutable, finally a tuple! + 'wellformed': True + } + # used for equality checks and setting of a space combinator + S = u' ' + + def append(seq, val, typ=None, token=None): + """ + appends to seq + + namespace_prefix, IDENT will be combined to a tuple + (prefix, name) where prefix might be None, the empty string + or a prefix. + + Saved are also: + - specificity definition: style, id, class/att, type + - element: the element this Selector is for + """ + context = new['context'][-1] + if token: + line, col = token[2], token[3] + else: + line, col = None, None + + if typ == '_PREFIX': + # SPECIAL TYPE: save prefix for combination with next + new['_PREFIX'] = val[:-1] + # handle next time + return + + if new['_PREFIX'] is not None: + # as saved from before and reset to None + prefix, new['_PREFIX'] = new['_PREFIX'], None + elif typ == 'universal' and '|' in val: + # val == *|* or prefix|* + prefix, val = val.split('|') + else: + prefix = None + + # namespace + if (typ.endswith('-selector') or typ == 'universal') and not ( + 'attribute-selector' == typ and not prefix): + # att **IS NOT** in default ns + if prefix == u'*': + # *|name: in ANY_NS + namespaceURI = cssutils._ANYNS + elif prefix is None: + # e or *: default namespace with prefix u'' + # or local-name() + namespaceURI = namespaces.get(u'', None) + elif prefix == u'': + # |name or |*: in no (or the empty) namespace + namespaceURI = u'' + else: + # explicit namespace prefix + # does not raise KeyError, see _SimpleNamespaces + namespaceURI = namespaces[prefix] + + if namespaceURI is None: + new['wellformed'] = False + self._log.error(u'Selector: No namespaceURI found ' + u'for prefix %r' % prefix, + token=token, + error=xml.dom.NamespaceErr) + return + + # val is now (namespaceprefix, name) tuple + val = (namespaceURI, val) + + # specificity + if not context or context == 'negation': + if 'id' == typ: + new['specificity'][1] += 1 + elif 'class' == typ or '[' == val: + new['specificity'][2] += 1 + elif typ in ('type-selector', 'negation-type-selector', + 'pseudo-element'): + new['specificity'][3] += 1 + if not context and typ in ('type-selector', 'universal'): + # define element + new['element'] = val + + seq.append(val, typ, line=line, col=col) + + # expected constants + simple_selector_sequence = 'type_selector universal HASH class ' \ + 'attrib pseudo negation ' + simple_selector_sequence2 = 'HASH class attrib pseudo negation ' + + element_name = 'element_name' + + negation_arg = 'type_selector universal HASH class attrib pseudo' + negationend = ')' + + attname = 'prefix attribute' + attname2 = 'attribute' + attcombinator = 'combinator ]' # optional + attvalue = 'value' # optional + attend = ']' + + expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT' + expression = expressionstart + ' )' + + combinator = ' combinator' + + def _COMMENT(expected, seq, token, tokenizer=None): + "special implementation for comment token" + append(seq, cssutils.css.CSSComment([token]), 'COMMENT', + token=token) + return expected + + def _S(expected, seq, token, tokenizer=None): + # S + context = new['context'][-1] + if context.startswith('pseudo-'): + if seq and seq[-1].value not in u'+-': + # e.g. x:func(a + b) + append(seq, S, 'S', token=token) + return expected + + elif context != 'attrib' and 'combinator' in expected: + append(seq, S, 'descendant', token=token) + return simple_selector_sequence + combinator + + else: + return expected + + def _universal(expected, seq, token, tokenizer=None): + # *|* or prefix|* + context = new['context'][-1] + val = self._tokenvalue(token) + if 'universal' in expected: + append(seq, val, 'universal', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected universal.', token=token) + return expected + + def _namespace_prefix(expected, seq, token, tokenizer=None): + # prefix| => element_name + # or prefix| => attribute_name if attrib + context = new['context'][-1] + val = self._tokenvalue(token) + if 'attrib' == context and 'prefix' in expected: + # [PREFIX|att] + append(seq, val, '_PREFIX', token=token) + return attname2 + elif 'type_selector' in expected: + # PREFIX|* + append(seq, val, '_PREFIX', token=token) + return element_name + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected namespace prefix.', token=token) + return expected + + def _pseudo(expected, seq, token, tokenizer=None): + # pseudo-class or pseudo-element :a ::a :a( ::a( + """ + /* '::' starts a pseudo-element, ':' a pseudo-class */ + /* Exceptions: :first-line, :first-letter, :before and + :after. */ + /* Note that pseudo-elements are restricted to one per selector + and */ + /* occur only in the last simple_selector_sequence. */ + """ + context = new['context'][-1] + val, typ = self._tokenvalue(token, normalize=True),\ + self._type(token) + if 'pseudo' in expected: + if val in (':first-line', + ':first-letter', + ':before', + ':after'): + # always pseudo-element ??? + typ = 'pseudo-element' + append(seq, val, typ, token=token) + + if val.endswith(u'('): + # function + # "pseudo-" "class" or "element" + new['context'].append(typ) + return expressionstart + elif 'negation' == context: + return negationend + elif 'pseudo-element' == typ: + # only one per element, check at ) also! + return combinator + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected start of pseudo.', token=token) + return expected + + def _expression(expected, seq, token, tokenizer=None): + # [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + if context.startswith('pseudo-'): + append(seq, val, typ, token=token) + return expression + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected %s.' % typ, token=token) + return expected + + def _attcombinator(expected, seq, token, tokenizer=None): + # context: attrib + # PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES | + # DASHMATCH + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + if 'attrib' == context and 'combinator' in expected: + # combinator in attrib + append(seq, val, typ.lower(), token=token) + return attvalue + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected %s.' % typ, token=token) + return expected + + def _string(expected, seq, token, tokenizer=None): + # identifier + context = new['context'][-1] + typ, val = self._type(token), self._stringtokenvalue(token) + + # context: attrib + if 'attrib' == context and 'value' in expected: + # attrib: [...=VALUE] + append(seq, val, typ, token=token) + return attend + + # context: pseudo + elif context.startswith('pseudo-'): + # :func(...) + append(seq, val, typ, token=token) + return expression + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected STRING.', token=token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # identifier + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + + # context: attrib + if 'attrib' == context and 'attribute' in expected: + # attrib: [...|ATT...] + append(seq, val, 'attribute-selector', token=token) + return attcombinator + + elif 'attrib' == context and 'value' in expected: + # attrib: [...=VALUE] + append(seq, val, 'attribute-value', token=token) + return attend + + # context: negation + elif 'negation' == context: + # negation: (prefix|IDENT) + append(seq, val, 'negation-type-selector', token=token) + return negationend + + # context: pseudo + elif context.startswith('pseudo-'): + # :func(...) + append(seq, val, typ, token=token) + return expression + + elif 'type_selector' in expected or element_name == expected: + # element name after ns or complete type_selector + append(seq, val, 'type-selector', token=token) + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected IDENT.', token=token) + return expected + + def _class(expected, seq, token, tokenizer=None): + # .IDENT + context = new['context'][-1] + val = self._tokenvalue(token) + if 'class' in expected: + append(seq, val, 'class', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected class.', token=token) + return expected + + def _hash(expected, seq, token, tokenizer=None): + # #IDENT + context = new['context'][-1] + val = self._tokenvalue(token) + if 'HASH' in expected: + append(seq, val, 'id', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected HASH.', token=token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # + > ~ ) [ ] + - + context = new['context'][-1] + val = self._tokenvalue(token) + + # context: attrib + if u']' == val and 'attrib' == context and ']' in expected: + # end of attrib + append(seq, val, 'attribute-end', token=token) + context = new['context'].pop() # attrib is done + context = new['context'][-1] + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + elif u'=' == val and 'attrib' == context\ + and 'combinator' in expected: + # combinator in attrib + append(seq, val, 'equals', token=token) + return attvalue + + # context: negation + elif u')' == val and 'negation' == context and u')' in expected: + # not(negation_arg)" + append(seq, val, 'negation-end', token=token) + new['context'].pop() # negation is done + context = new['context'][-1] + return simple_selector_sequence + combinator + + # context: pseudo (at least one expression) + elif val in u'+-' and context.startswith('pseudo-'): + # :func(+ -)" + _names = {'+': 'plus', '-': 'minus'} + if val == u'+' and seq and seq[-1].value == S: + seq.replace(-1, val, _names[val]) + else: + append(seq, val, _names[val], + token=token) + return expression + + elif u')' == val and context.startswith('pseudo-') and\ + expression == expected: + # :func(expression)" + append(seq, val, 'function-end', token=token) + new['context'].pop() # pseudo is done + if 'pseudo-element' == context: + return combinator + else: + return simple_selector_sequence + combinator + + # context: ROOT + elif u'[' == val and 'attrib' in expected: + # start of [attrib] + append(seq, val, 'attribute-start', token=token) + new['context'].append('attrib') + return attname + + elif val in u'+>~' and 'combinator' in expected: + # no other combinator except S may be following + _names = { + '>': 'child', + '+': 'adjacent-sibling', + '~': 'following-sibling'} + if seq and seq[-1].value == S: + seq.replace(-1, val, _names[val]) + else: + append(seq, val, _names[val], token=token) + return simple_selector_sequence + + elif u',' == val: + # not a selectorlist + new['wellformed'] = False + self._log.error( + u'Selector: Single selector only.', + error=xml.dom.InvalidModificationErr, + token=token) + return expected + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected CHAR.', token=token) + return expected + + def _negation(expected, seq, token, tokenizer=None): + # not( + context = new['context'][-1] + val = self._tokenvalue(token, normalize=True) + if 'negation' in expected: + new['context'].append('negation') + append(seq, val, 'negation-start', token=token) + return negation_arg + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected negation.', token=token) + return expected + + def _atkeyword(expected, seq, token, tokenizer=None): + "invalidates selector" + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected ATKEYWORD.', token=token) + return expected + + + # expected: only|not or mediatype, mediatype, feature, and + newseq = self._tempSeq() + + wellformed, expected = self._parse( + expected=simple_selector_sequence, + seq=newseq, tokenizer=tokenizer, + productions={'CHAR': _char, + 'class': _class, + 'HASH': _hash, + 'STRING': _string, + 'IDENT': _ident, + 'namespace_prefix': _namespace_prefix, + 'negation': _negation, + 'pseudo-class': _pseudo, + 'pseudo-element': _pseudo, + 'universal': _universal, + # pseudo + 'NUMBER': _expression, + 'DIMENSION': _expression, + # attribute + 'PREFIXMATCH': _attcombinator, + 'SUFFIXMATCH': _attcombinator, + 'SUBSTRINGMATCH': _attcombinator, + 'DASHMATCH': _attcombinator, + 'INCLUDES': _attcombinator, + + 'S': _S, + 'COMMENT': _COMMENT, + 'ATKEYWORD': _atkeyword}) + wellformed = wellformed and new['wellformed'] + + # post condition + if len(new['context']) > 1 or not newseq: + wellformed = False + self._log.error(u'Selector: Invalid or incomplete selector: %s' + % self._valuestr(selectorText)) + + if expected == 'element_name': + wellformed = False + self._log.error(u'Selector: No element name found: %s' + % self._valuestr(selectorText)) + + if expected == simple_selector_sequence and newseq: + wellformed = False + self._log.error(u'Selector: Cannot end with combinator: %s' + % self._valuestr(selectorText)) + + if newseq and hasattr(newseq[-1].value, 'strip') \ + and newseq[-1].value.strip() == u'': + del newseq[-1] + + # set + if wellformed: + self.__namespaces = namespaces + self._element = new['element'] + self._specificity = tuple(new['specificity']) + self._setSeq(newseq) + # filter that only used ones are kept + self.__namespaces = self._getUsedNamespaces() + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(DOM) The parsable textual representation of " + u"the selector.") + + specificity = property(lambda self: self._specificity, + doc="""Specificity of this selector (READONLY). + Tuple of (a, b, c, d) where: + + a + presence of style in document, always 0 if not used on a + document + b + number of ID selectors + c + number of .class selectors + d + number of Element (type) selectors""") + + wellformed = property(lambda self: bool(len(self.seq))) + + + @Deprecated('Use property parent instead') + def _getParentList(self): + return self.parent + + parentList = property(_getParentList, + doc="DEPRECATED, see property parent instead") diff --git a/libs/cssutils/css/selectorlist.py b/libs/cssutils/css/selectorlist.py new file mode 100755 index 00000000..2072cd53 --- /dev/null +++ b/libs/cssutils/css/selectorlist.py @@ -0,0 +1,234 @@ +"""SelectorList is a list of CSS Selector objects. + +TODO + - remove duplicate Selectors. -> CSSOM canonicalize + + - ??? CSS2 gives a special meaning to the comma (,) in selectors. + However, since it is not known if the comma may acquire other + meanings in future versions of CSS, the whole statement should be + ignored if there is an error anywhere in the selector, even though + the rest of the selector may look reasonable in CSS2. + + Illegal example(s): + + For example, since the "&" is not a valid token in a CSS2 selector, + a CSS2 user agent must ignore the whole second line, and not set + the color of H3 to red: +""" +__all__ = ['SelectorList'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from selector import Selector +import cssutils +import xml.dom + +class SelectorList(cssutils.util.Base, cssutils.util.ListSeq): + """A list of :class:`~cssutils.css.Selector` objects + of a :class:`~cssutils.css.CSSStyleRule`.""" + def __init__(self, selectorText=None, parentRule=None, + readonly=False): + """ + :Parameters: + selectorText + parsable list of Selectors + parentRule + the parent CSSRule if available + """ + super(SelectorList, self).__init__() + + self._parentRule = parentRule + + if selectorText: + self.selectorText = selectorText + + self._readonly = readonly + + def __repr__(self): + if self._namespaces: + st = (self.selectorText, self._namespaces) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__, + st) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self._namespaces, + id(self)) + + def __setitem__(self, index, newSelector): + """Overwrite ListSeq.__setitem__ + + Any duplicate Selectors are **not** removed. + """ + newSelector = self.__prepareset(newSelector) + if newSelector: + self.seq[index] = newSelector + + def __prepareset(self, newSelector, namespaces=None): + "Used by appendSelector and __setitem__" + if not namespaces: + namespaces = {} + self._checkReadonly() + if not isinstance(newSelector, Selector): + newSelector = Selector((newSelector, namespaces), + parent=self) + if newSelector.wellformed: + newSelector._parent = self # maybe set twice but must be! + return newSelector + + def __getNamespaces(self): + """Use children namespaces if not attached to a sheet, else the sheet's + ones. + """ + try: + return self.parentRule.parentStyleSheet.namespaces + except AttributeError: + namespaces = {} + for selector in self.seq: + namespaces.update(selector._namespaces) + return namespaces + + def _getUsedUris(self): + "Used by CSSStyleSheet to check if @namespace rules are needed" + uris = set() + for s in self: + uris.update(s._getUsedUris()) + return uris + + _namespaces = property(__getNamespaces, doc="""If this SelectorList is + attached to a CSSStyleSheet the namespaces of that sheet are mirrored + here. While the SelectorList (or parentRule(s) are + not attached the namespaces of all children Selectors are used.""") + + def append(self, newSelector): + "Same as :meth:`appendSelector`." + self.appendSelector(newSelector) + + def appendSelector(self, newSelector): + """ + Append `newSelector` to this list (a string will be converted to a + :class:`~cssutils.css.Selector`). + + :param newSelector: + comma-separated list of selectors (as a single string) or a tuple of + `(newSelector, dict-of-namespaces)` + :returns: New :class:`~cssutils.css.Selector` or ``None`` if + `newSelector` is not wellformed. + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + newSelector, namespaces = self._splitNamespacesOff(newSelector) + try: + # use parent's only if available + namespaces = self.parentRule.parentStyleSheet.namespaces + except AttributeError: + # use already present namespaces plus new given ones + _namespaces = self._namespaces + _namespaces.update(namespaces) + namespaces = _namespaces + + newSelector = self.__prepareset(newSelector, namespaces) + if newSelector: + seq = self.seq[:] + del self.seq[:] + for s in seq: + if s.selectorText != newSelector.selectorText: + self.seq.append(s) + self.seq.append(newSelector) + return newSelector + + def _getSelectorText(self): + "Return serialized format." + return cssutils.ser.do_css_SelectorList(self) + + def _setSelectorText(self, selectorText): + """ + :param selectorText: + comma-separated list of selectors or a tuple of + (selectorText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + selectorText, namespaces = self._splitNamespacesOff(selectorText) + try: + # use parent's only if available + namespaces = self.parentRule.parentStyleSheet.namespaces + except AttributeError: + pass + + wellformed = True + tokenizer = self._tokenize2(selectorText) + newseq = [] + + expected = True + while True: + # find all upto and including next ",", EOF or nothing + selectortokens = self._tokensupto2(tokenizer, listseponly=True) + if selectortokens: + if self._tokenvalue(selectortokens[-1]) == ',': + expected = selectortokens.pop() + else: + expected = None + + selector = Selector((selectortokens, namespaces), + parent=self) + if selector.wellformed: + newseq.append(selector) + else: + wellformed = False + self._log.error(u'SelectorList: Invalid Selector: %s' % + self._valuestr(selectortokens)) + else: + break + + # post condition + if u',' == expected: + wellformed = False + self._log.error(u'SelectorList: Cannot end with ",": %r' % + self._valuestr(selectorText)) + elif expected: + wellformed = False + self._log.error(u'SelectorList: Unknown Syntax: %r' % + self._valuestr(selectorText)) + if wellformed: + self.seq = newseq + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(cssutils) The textual representation of the " + u"selector for a rule set.") + + length = property(lambda self: len(self), + doc=u"The number of :class:`~cssutils.css.Selector` " + u"objects in the list.") + + parentRule = property(lambda self: self._parentRule, + doc=u"(DOM) The CSS rule that contains this " + u"SelectorList or ``None`` if this SelectorList " + u"is not attached to a CSSRule.") + + wellformed = property(lambda self: bool(len(self.seq))) + diff --git a/libs/cssutils/css/value.py b/libs/cssutils/css/value.py new file mode 100755 index 00000000..f9ab050a --- /dev/null +++ b/libs/cssutils/css/value.py @@ -0,0 +1,871 @@ +"""Value related classes. + +DOM Level 2 CSS CSSValue, CSSPrimitiveValue and CSSValueList are **no longer** +supported and are replaced by these new classes. +""" +__all__ = ['PropertyValue', + 'Value', + 'ColorValue', + 'DimensionValue', + 'URIValue', + 'CSSFunction', + 'CSSVariable', + 'MSValue' + ] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +import cssutils +from cssutils.helper import normalize, pushtoken +import colorsys +import math +import re +import xml.dom +import urlparse + +class PropertyValue(cssutils.util._NewBase): + """ + An unstructured list like holder for all values defined for a + :class:`~cssutils.css.Property`. Contains :class:`~cssutils.css.Value` + or subclass objects. Currently there is no access to the combinators of + the defined values which might simply be space or comma or slash. + + You may: + + - iterate over all contained Value objects (not the separators like ``,``, + ``/`` or `` `` though!) + - get a Value item by index or use ``PropertyValue[index]`` + - find out the number of values defined (unstructured) + """ + def __init__(self, cssText=None, parent=None, readonly=False): + """ + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(PropertyValue, self).__init__() + + self.parent = parent + self.wellformed = False + + if cssText is not None: # may be 0 + if isinstance(cssText, (int, float)): + cssText = unicode(cssText) # if it is a number + self.cssText = cssText + + self._readonly = readonly + + def __len__(self): + return len(list(self.__items())) + + def __getitem__(self, index): + try: + return list(self.__items())[index] + except IndexError: + return None + + def __iter__(self): + "Generator which iterates over values." + for item in self.__items(): + yield item + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.length, self.cssText, id(self)) + + def __items(self, seq=None): + "a generator of Value obects only, no , / or ' '" + if seq is None: + seq = self.seq + return (x.value for x in seq if isinstance(x.value, Value)) + + def _setCssText(self, cssText): + if isinstance(cssText, (int, float)): + cssText = unicode(cssText) # if it is a number + """ + Format:: + + unary_operator + : '-' | '+' + ; + operator + : '/' S* | ',' S* | /* empty */ + ; + expr + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* ] + | STRING S* | IDENT S* | URI S* | hexcolor | function + | UNICODE-RANGE S* + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a + different type of values than the values allowed by the CSS + property. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this value is readonly. + """ + self._checkReadonly() + + # used as operator is , / or S + nextSor = u',/' + term = Choice(_ColorProd(self, nextSor), + _DimensionProd(self, nextSor), + _URIProd(self, nextSor), + _ValueProd(self, nextSor), +# _CalcValueProd(self, nextSor), +# _Rect(self, nextSor), + # all other functions + _CSSVariableProd(self, nextSor), + _MSValueProd(self, nextSor), + _CSSFunctionProd(self, nextSor) + ) + operator = Choice(PreDef.S(toSeq=False), + PreDef.char('comma', ',', + toSeq=lambda t, tokens: ('operator', t[1])), + PreDef.char('slash', '/', + toSeq=lambda t, tokens: ('operator', t[1])), + optional=True) + prods = Sequence(term, + Sequence(# mayEnd this Sequence if whitespace + operator, + # TODO: only when setting via other class + # used by variabledeclaration currently + PreDef.char('END', ';', + stopAndKeep=True, + optional=True), + # TODO: } and !important ends too! + term, + minmax=lambda: (0, None))) + # parse + ok, seq, store, unused = ProdParser().parse(cssText, + u'PropertyValue', + prods) + # must be at least one value! + ok = ok and len(list(self.__items(seq))) > 0 + if ok: + self._setSeq(seq) + self.wellformed = True + else: + self._log.error(u'PropertyValue: Unknown syntax or no value: %s' % + self._valuestr(cssText)) + + cssText = property(lambda self: cssutils.ser.do_css_PropertyValue(self), + _setCssText, + doc="A string representation of the current value.") + + def item(self, index): + """ + The value at position `index`. Alternatively simple use + ``PropertyValue[index]``. + + :param index: + the parsable cssText of the value + :exceptions: + - :exc:`~IndexError`: + Raised if index if out of bounds + """ + return self[index] + + length = property(lambda self: len(self), + doc=u"Number of values set.") + + value = property(lambda self: cssutils.ser.do_css_PropertyValue(self, + valuesOnly=True), + doc=u"A string representation of the current value " + u"without any comments used for validation.") + + +class Value(cssutils.util._NewBase): + """ + Represents a single CSS value. For now simple values of + IDENT, STRING, or UNICODE-RANGE values are represented directly + as Value objects. Other values like e.g. FUNCTIONs are represented by + subclasses with an extended API. + """ + IDENT = u'IDENT' + STRING = u'STRING' + UNICODE_RANGE = u'UNICODE-RANGE' + URI = u'URI' + + DIMENSION = u'DIMENSION' + NUMBER = u'NUMBER' + PERCENTAGE = u'PERCENTAGE' + + COLOR_VALUE = u'COLOR_VALUE' + HASH = u'HASH' + + FUNCTION = u'FUNCTION' + VARIABLE = u'VARIABLE' + + _type = None + _value = u'' + + def __init__(self, cssText=None, parent=None, readonly=False): + super(Value, self).__init__() + + self.parent = parent + + if cssText: + self.cssText = cssText + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Choice(PreDef.hexcolor(stop=True), + PreDef.ident(stop=True), + PreDef.string(stop=True), + PreDef.unicode_range(stop=True), + ) + ok, seq, store, unused = ProdParser().parse(cssText, u'Value', prods) + if ok: + # only 1 value anyway! + self._type = seq[0].type + self._value = seq[0].value + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u'String value of this value.') + + type = property(lambda self: self._type, #_setType, + doc=u"Type of this value, for now the production type " + u"like e.g. `DIMENSION` or `STRING`. All types are " + u"defined as constants in :class:`~cssutils.css.Value`.") + + def _setValue(self, value): + # TODO: check! + self._value = value + + value = property(lambda self: self._value, _setValue, + doc=u"Actual value if possible: An int or float or else " + u" a string") + + +class ColorValue(Value): + """ + A color value like rgb(), rgba(), hsl(), hsla() or #rgb, #rrggbb + + TODO: Color Keywords + """ + from colors import COLORS + + type = Value.COLOR_VALUE + # hexcolor, FUNCTION? + _colorType = None + _red = 0 + _green = 0 + _blue = 0 + _alpha = 0 + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, + self.colorType, self.red, self.green, self.blue, self.alpha, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + types = self._prods # rename! + + component = Choice(PreDef.unary(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )), + PreDef.number(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )), + PreDef.percentage(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )) + ) + noalp = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION and + v in (u'rgb(', u'hsl('), + toSeq=lambda t, tokens: (t[0], normalize(t[1]))), + component, + Sequence(PreDef.comma(), + component, + minmax=lambda: (2, 2) + ), + PreDef.funcEnd(stop=True) + ) + witha = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION and + v in (u'rgba(', u'hsla('), + toSeq=lambda t, tokens: (t[0], + normalize(t[1])) + ), + component, + Sequence(PreDef.comma(), + component, + minmax=lambda: (3, 3) + ), + PreDef.funcEnd(stop=True) + ) + namedcolor = Prod(name='Named Color', + match=lambda t, v: t == 'IDENT' and ( + normalize(v) in self.COLORS.keys() + ), + stop=True) + + prods = Choice(PreDef.hexcolor(stop=True), + namedcolor, + noalp, + witha) + + ok, seq, store, unused = ProdParser().parse(cssText, + self.type, + prods) + if ok: + t, v = seq[0].type, seq[0].value + if u'IDENT' == t: + rgba = self.COLORS[normalize(v)] + if u'HASH' == t: + if len(v) == 4: + # HASH #rgb + rgba = (int(2*v[1], 16), + int(2*v[2], 16), + int(2*v[3], 16), + 1.0) + else: + # HASH #rrggbb + rgba = (int(v[1:3], 16), + int(v[3:5], 16), + int(v[5:7], 16), + 1.0) + + elif u'FUNCTION' == t: + functiontype, raw, check = None, [], u'' + HSL = False + + for item in seq: + try: + type_ = item.value.type + except AttributeError, e: + # type of function, e.g. rgb( + if item.type == 'FUNCTION': + functiontype = item.value + HSL = functiontype in (u'hsl(', u'hsla(') + continue + + # save components + if type_ == Value.NUMBER: + raw.append(item.value.value) + check += u'N' + elif type_ == Value.PERCENTAGE: + if HSL: + # save as percentage fraction + raw.append(item.value.value / 100.0) + else: + # save as real value of percentage of 255 + raw.append(int(255 * item.value.value / 100)) + check += u'P' + + if HSL: + # convert to rgb + # h is 360 based (circle) + h, s, l = raw[0] / 360.0, raw[1], raw[2] + # ORDER h l s !!! + r, g, b = colorsys.hls_to_rgb(h, l, s) + # back to 255 based + rgba = [int(round(r*255)), + int(round(g*255)), + int(round(b*255))] + + if len(raw) > 3: + rgba.append(raw[3]) + + else: + # rgb, rgba + rgba = raw + + if len(rgba) < 4: + rgba.append(1.0) + + # validate + checks = {u'rgb(': ('NNN', 'PPP'), + u'rgba(': ('NNNN', 'PPPN'), + u'hsl(': ('NPP',), + u'hsla(': ('NPPN',) + } + if check not in checks[functiontype]: + self._log.error(u'ColorValue has invalid %s) parameters: ' + u'%s (N=Number, P=Percentage)' % + (functiontype, check)) + + self._colorType = t + self._red, self._green, self._blue, self._alpha = tuple(rgba) + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_ColorValue(self), + _setCssText, + doc=u"String value of this value.") + + value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True), + doc=u'Same as cssText but without comments.') + + type = property(lambda self: Value.COLOR_VALUE, + doc=u"Type is fixed to Value.COLOR_VALUE.") + + def _getName(self): + for n, v in self.COLORS.items(): + if v == (self.red, self.green, self.blue, self.alpha): + return n + + colorType = property(lambda self: self._colorType, + doc=u"IDENT (red), HASH (#f00) or FUNCTION (rgb(255, 0, 0).") + + name = property(_getName, + doc=u'Name of the color if known (in ColorValue.COLORS) ' + u'else None') + + red = property(lambda self: self._red, + doc=u'red part as integer between 0 and 255') + green = property(lambda self: self._green, + doc=u'green part as integer between 0 and 255') + blue = property(lambda self: self._blue, + doc=u'blue part as integer between 0 and 255') + alpha = property(lambda self: self._alpha, + doc=u'alpha part as float between 0.0 and 1.0') + +class DimensionValue(Value): + """ + A numerical value with an optional dimenstion like e.g. "px" or "%". + + Covers DIMENSION, PERCENTAGE or NUMBER values. + """ + __reNumDim = re.compile(ur'^(\d*\.\d+|\d+)(.*)$', re.I | re.U | re.X) + _dimension = None + _sign = None + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.dimension, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Sequence(PreDef.unary(), + Choice(PreDef.dimension(stop=True), + PreDef.number(stop=True), + PreDef.percentage(stop=True) + ) + ) + ok, seq, store, unused = ProdParser().parse(cssText, + u'DimensionValue', + prods) + if ok: + sign = val = u'' + dim = type_ = None + + # find + for item in seq: + if item.value in u'+-': + sign = item.value + else: + type_ = item.type + + # number + optional dim + v, d = self.__reNumDim.findall( + normalize(item.value))[0] + if u'.' in v: + val = float(sign + v) + else: + val = int(sign + v) + if d: + dim = d + + self._sign = sign + self._value = val + self._dimension = dim + self._type = type_ + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u"String value of this value including dimension.") + + dimension = property(lambda self: self._dimension, #_setValue, + doc=u"Dimension if a DIMENSION or PERCENTAGE value, " + u"else None") +class URIValue(Value): + """ + An URI value like ``url(example.png)``. + """ + _type = Value.URI + _uri = Value._value + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.uri, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Sequence(PreDef.uri(stop=True)) + + ok, seq, store, unused = ProdParser().parse(cssText, u'URIValue', prods) + if ok: + # only 1 value only anyway + self._type = seq[0].type + self._value = seq[0].value + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u'String value of this value.') + + def _setUri(self, uri): + # TODO: check? + self._value = uri + + uri = property(lambda self: self._value, _setUri, + doc=u"Actual URL without delimiters or the empty string") + + def absoluteUri(self): + """Actual URL, made absolute if possible, else same as `uri`.""" + # Ancestry: PropertyValue, Property, CSSStyleDeclaration, CSSStyleRule, + # CSSStyleSheet + try: + # TODO: better way? + styleSheet = self.parent.parent.parent.parentRule.parentStyleSheet + except AttributeError, e: + return self.uri + else: + return urlparse.urljoin(styleSheet.href, self.uri) + + absoluteUri = property(absoluteUri, doc=absoluteUri.__doc__) + + +class CSSFunction(Value): + """ + A function value. + """ + _functionName = 'Function' + + def _productions(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + itemProd = Choice(_ColorProd(self), + _DimensionProd(self), + _URIProd(self), + _ValueProd(self), + #_CalcValueProd(self), + _CSSVariableProd(self), + _CSSFunctionProd(self) + ) + funcProds = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], + normalize(t[1]))), + Choice(Sequence(itemProd, + Sequence(PreDef.comma(), + itemProd, + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)), + PreDef.funcEnd(stop=True)) + ) + return funcProds + + def _setCssText(self, cssText): + self._checkReadonly() + ok, seq, store, unused = ProdParser().parse(cssText, + self.type, + self._productions()) + if ok: + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_CSSFunction(self), + _setCssText, + doc=u"String value of this value.") + + value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True), + doc=u'Same as cssText but without comments.') + + type = property(lambda self: Value.FUNCTION, + doc=u"Type is fixed to Value.FUNCTION.") + +class MSValue(CSSFunction): + """An IE specific Microsoft only function value which is much looser + in what is syntactically allowed.""" + _functionName = 'MSValue' + + def _productions(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + func = Prod(name='MSValue-Sub', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (MSValue._functionName, + MSValue(pushtoken(t, + tokens + ), + parent=self + ) + ) + ) + + + funcProds = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], t[1]) + ), + Sequence(Choice(_ColorProd(self), + _DimensionProd(self), + _URIProd(self), + _ValueProd(self), + _MSValueProd(self), + #_CalcValueProd(self), + _CSSVariableProd(self), + func, + #_CSSFunctionProd(self), + Prod(name='MSValuePart', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1]) + ) + ), + minmax=lambda: (0, None) + ), + PreDef.funcEnd(stop=True) + ) + return funcProds + + def _setCssText(self, cssText): + super(MSValue, self)._setCssText(cssText) + + cssText = property(lambda self: cssutils.ser.do_css_MSValue(self), + _setCssText, + doc=u"String value of this value.") + + +class CSSVariable(CSSFunction): + """The CSSVariable represents a CSS variables like ``var(varname)``. + + A variable has a (nonnormalized!) `name` and a `value` which is + tried to be resolved from any available CSSVariablesRule definition. + """ + _functionName = 'CSSVariable' + _name = None + + def __str__(self): + return u"" % ( + self.__class__.__name__, self.name, self.value, id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + types = self._prods # rename! + prods = Sequence(Prod(name='var', + match=lambda t, v: t == types.FUNCTION and + normalize(v) == u'var(' + ), + PreDef.ident(toStore='ident'), + PreDef.funcEnd(stop=True)) + + # store: name of variable + store = {'ident': None} + ok, seq, store, unused = ProdParser().parse(cssText, + u'CSSVariable', + prods) + if ok: + self._name = store['ident'].value + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self), + _setCssText, doc=u"String representation of variable.") + + # TODO: writable? check if var (value) available? + name = property(lambda self: self._name, + doc=u"The name identifier of this variable referring to " + u"a value in a " + u":class:`cssutils.css.CSSVariablesDeclaration`.") + + type = property(lambda self: Value.VARIABLE, + doc=u"Type is fixed to Value.VARIABLE.") + + def _getValue(self): + "Find contained sheet and @variables there" + rel = self + while True: + # find node which has parentRule to get to StyleSheet + if hasattr(rel, 'parent'): + rel = rel.parent + else: + break + try: + variables = rel.parentRule.parentStyleSheet.variables + except AttributeError: + return None + else: + try: + return variables[self.name] + except KeyError: + return None + + value = property(_getValue, + doc=u'The resolved actual value or None.') + + +# helper for productions +def _ValueProd(parent, nextSor=False): + return Prod(name='Value', + match=lambda t, v: t in ('IDENT', 'STRING', 'UNICODE-RANGE'), + nextSor = nextSor, + toSeq=lambda t, tokens: ('Value', Value( + pushtoken(t, + tokens), + parent=parent) + ) + ) + + +def _DimensionProd(parent, nextSor=False): + return Prod(name='Dimension', + match=lambda t, v: t in (u'DIMENSION', + u'NUMBER', + u'PERCENTAGE') or v in u'+-', + nextSor = nextSor, + toSeq=lambda t, tokens: (t[0], DimensionValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +def _URIProd(parent, nextSor=False): + return Prod(name='URIValue', + match=lambda t, v: t == 'URI', + nextSor = nextSor, + toSeq=lambda t, tokens: ('URIValue', URIValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$') + +def _ColorProd(parent, nextSor=False): + return Prod(name='ColorValue', + match=lambda t, v: + (t == 'HASH' and + reHexcolor.match(v) + ) or + (t == 'FUNCTION' and + normalize(v) in (u'rgb(', + u'rgba(', + u'hsl(', + u'hsla(') + ) or + (t == 'IDENT' and + normalize(v) in ColorValue.COLORS.keys() + ), + nextSor = nextSor, + toSeq=lambda t, tokens: ('ColorValue', ColorValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +def _CSSFunctionProd(parent, nextSor=False): + return PreDef.function(nextSor=nextSor, + toSeq=lambda t, tokens: (CSSFunction._functionName, + CSSFunction( + pushtoken(t, tokens), + parent=parent) + ) + ) + +def _CSSVariableProd(parent, nextSor=False): + return PreDef.variable(nextSor=nextSor, + toSeq=lambda t, tokens: (CSSVariable._functionName, + CSSVariable( + pushtoken(t, tokens), + parent=parent) + ) + ) + +def _MSValueProd(parent, nextSor=False): + return Prod(name=MSValue._functionName, + match=lambda t, v: (#t == self._prods.FUNCTION and ( + normalize(v) in (u'expression(', + u'alpha(', + u'blur(', + u'chroma(', + u'dropshadow(', + u'fliph(', + u'flipv(', + u'glow(', + u'gray(', + u'invert(', + u'mask(', + u'shadow(', + u'wave(', + u'xray(') or + v.startswith(u'progid:DXImageTransform.Microsoft.') + ), + nextSor=nextSor, + toSeq=lambda t, tokens: (MSValue._functionName, + MSValue(pushtoken(t, + tokens + ), + parent=parent + ) + ) + ) diff --git a/libs/cssutils/css2productions.py b/libs/cssutils/css2productions.py new file mode 100755 index 00000000..a435e71e --- /dev/null +++ b/libs/cssutils/css2productions.py @@ -0,0 +1,131 @@ +"""productions for CSS 2.1 + +CSS2_1_MACROS and CSS2_1_PRODUCTIONS are from both +http://www.w3.org/TR/CSS21/grammar.html and +http://www.w3.org/TR/css3-syntax/#grammar0 + + +""" +__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +# option case-insensitive +MACROS = { + 'h': r'[0-9a-f]', + #'nonascii': r'[\200-\377]', + 'nonascii': r'[^\0-\177]', # CSS3 + 'unicode': r'\\{h}{1,6}(\r\n|[ \t\r\n\f])?', + + 'escape': r'{unicode}|\\[^\r\n\f0-9a-f]', + 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}', + 'nmchar': r'[_a-zA-Z0-9-]|{nonascii}|{escape}', + 'string1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*\"', + 'string2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*\'", + 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*', + 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*", + 'comment': r'\/\*[^*]*\*+([^/*][^*]*\*+)*\/', + # CSS list 080725 19:43 + # \/\*([^*\\]|{escape})*\*+(([^/*\\]|{escape})[^*]*\*+)*\/ + + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + # CHANGED TO SPEC: added "-?" + 'num': r'-?[0-9]*\.[0-9]+|[0-9]+', + 'string': r'{string1}|{string2}', + 'invalid': r'{invalid1}|{invalid2}', + 'url': r'([!#$%&*-~]|{nonascii}|{escape})*', + 's': r'[ \t\r\n\f]+', + 'w': r'{s}?', + 'nl': r'\n|\r\n|\r|\f', + 'range': r'\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h})))))', + + 'A': r'a|\\0{0,4}(41|61)(\r\n|[ \t\r\n\f])?', + 'C': r'c|\\0{0,4}(43|63)(\r\n|[ \t\r\n\f])?', + 'D': r'd|\\0{0,4}(44|64)(\r\n|[ \t\r\n\f])?', + 'E': r'e|\\0{0,4}(45|65)(\r\n|[ \t\r\n\f])?', + 'F': r'f|\\0{0,4}(46|66)(\r\n|[ \t\r\n\f])?', + 'G': r'g|\\0{0,4}(47|67)(\r\n|[ \t\r\n\f])?|\\g', + 'H': r'h|\\0{0,4}(48|68)(\r\n|[ \t\r\n\f])?|\\h', + 'I': r'i|\\0{0,4}(49|69)(\r\n|[ \t\r\n\f])?|\\i', + 'K': r'k|\\0{0,4}(4b|6b)(\r\n|[ \t\r\n\f])?|\\k', + 'M': r'm|\\0{0,4}(4d|6d)(\r\n|[ \t\r\n\f])?|\\m', + 'N': r'n|\\0{0,4}(4e|6e)(\r\n|[ \t\r\n\f])?|\\n', + 'O': r'o|\\0{0,4}(51|71)(\r\n|[ \t\r\n\f])?|\\o', + 'P': r'p|\\0{0,4}(50|70)(\r\n|[ \t\r\n\f])?|\\p', + 'R': r'r|\\0{0,4}(52|72)(\r\n|[ \t\r\n\f])?|\\r', + 'S': r's|\\0{0,4}(53|73)(\r\n|[ \t\r\n\f])?|\\s', + 'T': r't|\\0{0,4}(54|74)(\r\n|[ \t\r\n\f])?|\\t', + 'X': r'x|\\0{0,4}(58|78)(\r\n|[ \t\r\n\f])?|\\x', + 'Z': r'z|\\0{0,4}(5a|7a)(\r\n|[ \t\r\n\f])?|\\z', + } + +PRODUCTIONS = [ + ('URI', r'url\({w}{string}{w}\)'), #"url("{w}{string}{w}")" {return URI;} + ('URI', r'url\({w}{url}{w}\)'), #"url("{w}{url}{w}")" {return URI;} + ('FUNCTION', r'{ident}\('), #{ident}"(" {return FUNCTION;} + + ('IMPORT_SYM', r'@{I}{M}{P}{O}{R}{T}'), #"@import" {return IMPORT_SYM;} + ('PAGE_SYM', r'@{P}{A}{G}{E}'), #"@page" {return PAGE_SYM;} + ('MEDIA_SYM', r'@{M}{E}{D}{I}{A}'), #"@media" {return MEDIA_SYM;} + ('FONT_FACE_SYM', r'@{F}{O}{N}{T}\-{F}{A}{C}{E}'), #"@font-face" {return FONT_FACE_SYM;} + + # CHANGED TO SPEC: only @charset + ('CHARSET_SYM', r'@charset '), #"@charset " {return CHARSET_SYM;} + + ('NAMESPACE_SYM', r'@{N}{A}{M}{E}{S}{P}{A}{C}{E}'), #"@namespace" {return NAMESPACE_SYM;} + + # CHANGED TO SPEC: ATKEYWORD + ('ATKEYWORD', r'\@{ident}'), + + ('IDENT', r'{ident}'), #{ident} {return IDENT;} + ('STRING', r'{string}'), #{string} {return STRING;} + ('INVALID', r'{invalid}'), # {return INVALID; /* unclosed string */} + ('HASH', r'\#{name}'), #"#"{name} {return HASH;} + ('PERCENTAGE', r'{num}%'), #{num}% {return PERCENTAGE;} + ('LENGTH', r'{num}{E}{M}'), #{num}em {return EMS;} + ('LENGTH', r'{num}{E}{X}'), #{num}ex {return EXS;} + ('LENGTH', r'{num}{P}{X}'), #{num}px {return LENGTH;} + ('LENGTH', r'{num}{C}{M}'), #{num}cm {return LENGTH;} + ('LENGTH', r'{num}{M}{M}'), #{num}mm {return LENGTH;} + ('LENGTH', r'{num}{I}{N}'), #{num}in {return LENGTH;} + ('LENGTH', r'{num}{P}{T}'), #{num}pt {return LENGTH;} + ('LENGTH', r'{num}{P}{C}'), #{num}pc {return LENGTH;} + ('ANGLE', r'{num}{D}{E}{G}'), #{num}deg {return ANGLE;} + ('ANGLE', r'{num}{R}{A}{D}'), #{num}rad {return ANGLE;} + ('ANGLE', r'{num}{G}{R}{A}{D}'), #{num}grad {return ANGLE;} + ('TIME', r'{num}{M}{S}'), #{num}ms {return TIME;} + ('TIME', r'{num}{S}'), #{num}s {return TIME;} + ('FREQ', r'{num}{H}{Z}'), #{num}Hz {return FREQ;} + ('FREQ', r'{num}{K}{H}{Z}'), #{num}kHz {return FREQ;} + ('DIMEN', r'{num}{ident}'), #{num}{ident} {return DIMEN;} + ('NUMBER', r'{num}'), #{num} {return NUMBER;} + #('UNICODERANGE', r'U\+{range}'), #U\+{range} {return UNICODERANGE;} + #('UNICODERANGE', r'U\+{h}{1,6}-{h}{1,6}'), #U\+{h}{1,6}-{h}{1,6} {return UNICODERANGE;} + # --- CSS3 --- + ('UNICODE-RANGE', r'[0-9A-F?]{1,6}(\-[0-9A-F]{1,6})?'), + ('CDO', r'\<\!\-\-'), #"" {return CDC;} + ('S', r'{s}'),# {return S;} + + # \/\*[^*]*\*+([^/*][^*]*\*+)*\/ /* ignore comments */ + # {s}+\/\*[^*]*\*+([^/*][^*]*\*+)*\/ {unput(' '); /*replace by space*/} + + ('INCLUDES', r'\~\='), #"~=" {return INCLUDES;} + ('DASHMATCH', r'\|\='), #"|=" {return DASHMATCH;} + ('LBRACE', r'\{'), #{w}"{" {return LBRACE;} + ('PLUS', r'\+'), #{w}"+" {return PLUS;} + ('GREATER', r'\>'), #{w}">" {return GREATER;} + ('COMMA', r'\,'), #{w}"," {return COMMA;} + ('IMPORTANT_SYM', r'\!({w}|{comment})*{I}{M}{P}{O}{R}{T}{A}{N}{T}'), #"!{w}important" {return IMPORTANT_SYM;} + ('COMMENT', '\/\*[^*]*\*+([^/][^*]*\*+)*\/'), # /* ignore comments */ + ('CLASS', r'\.'), #. {return *yytext;} + + # --- CSS3! --- + ('CHAR', r'[^"\']'), + ] + +class CSSProductions(object): + pass +for i, t in enumerate(PRODUCTIONS): + setattr(CSSProductions, t[0].replace('-', '_'), t[0]) \ No newline at end of file diff --git a/libs/cssutils/cssproductions.py b/libs/cssutils/cssproductions.py new file mode 100755 index 00000000..9f7b3d80 --- /dev/null +++ b/libs/cssutils/cssproductions.py @@ -0,0 +1,124 @@ +"""productions for cssutils based on a mix of CSS 2.1 and CSS 3 Syntax +productions + +- http://www.w3.org/TR/css3-syntax +- http://www.w3.org/TR/css3-syntax/#grammar0 + +open issues + - numbers contain "-" if present + - HASH: #aaa is, #000 is not anymore, + CSS2.1: 'nmchar': r'[_a-z0-9-]|{nonascii}|{escape}', + CSS3: 'nmchar': r'[_a-z-]|{nonascii}|{escape}', +""" +__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +# a complete list of css3 macros +MACROS = { + 'nonascii': r'[^\0-\177]', + 'unicode': r'\\[0-9A-Fa-f]{1,6}(?:{nl}|{s})?', + #'escape': r'{unicode}|\\[ -~\200-\777]', + 'escape': r'{unicode}|\\[^\n\r\f0-9a-f]', + 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}', + 'nmchar': r'[-_a-zA-Z0-9]|{nonascii}|{escape}', + 'string1': r'"([^\n\r\f\\"]|\\{nl}|{escape})*"', + 'string2': r"'([^\n\r\f\\']|\\{nl}|{escape})*'", + 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*', + 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*", + + 'comment': r'\/\*[^*]*\*+([^/][^*]*\*+)*\/', + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + 'num': r'[0-9]*\.[0-9]+|[0-9]+', #r'[-]?\d+|[-]?\d*\.\d+', + 'string': r'{string1}|{string2}', + # from CSS2.1 + 'invalid': r'{invalid1}|{invalid2}', + 'url': r'[\x09\x21\x23-\x26\x28\x2a-\x7E]|{nonascii}|{escape}', + + 's': r'\t|\r|\n|\f|\x20', + 'w': r'{s}*', + 'nl': r'\n|\r\n|\r|\f', + + 'A': r'A|a|\\0{0,4}(?:41|61)(?:\r\n|[ \t\r\n\f])?', + 'B': r'B|b|\\0{0,4}(?:42|62)(?:\r\n|[ \t\r\n\f])?', + 'C': r'C|c|\\0{0,4}(?:43|63)(?:\r\n|[ \t\r\n\f])?', + 'D': r'D|d|\\0{0,4}(?:44|64)(?:\r\n|[ \t\r\n\f])?', + 'E': r'E|e|\\0{0,4}(?:45|65)(?:\r\n|[ \t\r\n\f])?', + 'F': r'F|f|\\0{0,4}(?:46|66)(?:\r\n|[ \t\r\n\f])?', + 'G': r'G|g|\\0{0,4}(?:47|67)(?:\r\n|[ \t\r\n\f])?|\\G|\\g', + 'H': r'H|h|\\0{0,4}(?:48|68)(?:\r\n|[ \t\r\n\f])?|\\H|\\h', + 'I': r'I|i|\\0{0,4}(?:49|69)(?:\r\n|[ \t\r\n\f])?|\\I|\\i', + 'K': r'K|k|\\0{0,4}(?:4b|6b)(?:\r\n|[ \t\r\n\f])?|\\K|\\k', + 'L': r'L|l|\\0{0,4}(?:4c|6c)(?:\r\n|[ \t\r\n\f])?|\\L|\\l', + 'M': r'M|m|\\0{0,4}(?:4d|6d)(?:\r\n|[ \t\r\n\f])?|\\M|\\m', + 'N': r'N|n|\\0{0,4}(?:4e|6e)(?:\r\n|[ \t\r\n\f])?|\\N|\\n', + 'O': r'O|o|\\0{0,4}(?:4f|6f)(?:\r\n|[ \t\r\n\f])?|\\O|\\o', + 'P': r'P|p|\\0{0,4}(?:50|70)(?:\r\n|[ \t\r\n\f])?|\\P|\\p', + 'R': r'R|r|\\0{0,4}(?:52|72)(?:\r\n|[ \t\r\n\f])?|\\R|\\r', + 'S': r'S|s|\\0{0,4}(?:53|73)(?:\r\n|[ \t\r\n\f])?|\\S|\\s', + 'T': r'T|t|\\0{0,4}(?:54|74)(?:\r\n|[ \t\r\n\f])?|\\T|\\t', + 'U': r'U|u|\\0{0,4}(?:55|75)(?:\r\n|[ \t\r\n\f])?|\\U|\\u', + 'V': r'V|v|\\0{0,4}(?:56|76)(?:\r\n|[ \t\r\n\f])?|\\V|\\v', + 'X': r'X|x|\\0{0,4}(?:58|78)(?:\r\n|[ \t\r\n\f])?|\\X|\\x', + 'Z': r'Z|z|\\0{0,4}(?:5a|7a)(?:\r\n|[ \t\r\n\f])?|\\Z|\\z', + } + +# The following productions are the complete list of tokens +# used by cssutils, a mix of CSS3 and some CSS2.1 productions. +# The productions are **ordered**: +PRODUCTIONS = [ + # UTF8_BOM or UTF8_BOM_SIG will only be checked at beginning of CSS + ('BOM', '\xfe\xff|\xef\xbb\xbf'), + + ('S', r'{s}+'), # 1st in list of general productions + ('URI', r'{U}{R}{L}\({w}({string}|{url}*){w}\)'), + ('FUNCTION', r'{ident}\('), + ('UNICODE-RANGE', r'{U}\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?'), + ('IDENT', r'{ident}'), + ('DIMENSION', r'{num}{ident}'), + ('PERCENTAGE', r'{num}\%'), + ('NUMBER', r'{num}'), + ('HASH', r'\#{name}'), + ('COMMENT', r'{comment}'), #r'\/\*[^*]*\*+([^/][^*]*\*+)*\/'), + ('STRING', r'{string}'), + ('INVALID', r'{invalid}'), # from CSS2.1 + ('ATKEYWORD', r'@{ident}'), # other keywords are done in the tokenizer + ('INCLUDES', '\~\='), + ('DASHMATCH', r'\|\='), + ('PREFIXMATCH', r'\^\='), + ('SUFFIXMATCH', r'\$\='), + ('SUBSTRINGMATCH', r'\*\='), + ('CDO', r'\<\!\-\-'), + ('CDC', r'\-\-\>'), + ('CHAR', r'[^"\']') # MUST always be last + # valid ony at start so not checked everytime + #('CHARSET_SYM', r'@charset '), # from Errata includes ending space! + # checked specially if fullsheet is parsed + ] + + + +class CSSProductions(object): + """ + most attributes are set later + """ + EOF = True + # removed from productions as they simply are ATKEYWORD until + # tokenizing + CHARSET_SYM = u'CHARSET_SYM' + FONT_FACE_SYM = u'FONT_FACE_SYM' + MEDIA_SYM = u'MEDIA_SYM' + IMPORT_SYM = u'IMPORT_SYM' + NAMESPACE_SYM = u'NAMESPACE_SYM' + PAGE_SYM = u'PAGE_SYM' + VARIABLES_SYM = u'VARIABLES_SYM' + +for i, t in enumerate(PRODUCTIONS): + setattr(CSSProductions, t[0].replace('-', '_'), t[0]) + + +# may be enabled by settings.set +_DXImageTransform = (u'FUNCTION', + ur'progid\:DXImageTransform\.Microsoft\..+\(' + ) diff --git a/libs/cssutils/errorhandler.py b/libs/cssutils/errorhandler.py new file mode 100755 index 00000000..916adff5 --- /dev/null +++ b/libs/cssutils/errorhandler.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +"""cssutils ErrorHandler + +ErrorHandler + used as log with usual levels (debug, info, warn, error) + + if instanciated with ``raiseExceptions=True`` raises exeptions instead + of logging + +log + defaults to instance of ErrorHandler for any kind of log message from + lexerm, parser etc. + + - raiseExceptions = [False, True] + - setloglevel(loglevel) +""" +__all__ = ['ErrorHandler'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import logging +import urllib2 +import xml.dom + +class _ErrorHandler(object): + """ + handles all errors and log messages + """ + def __init__(self, log, defaultloglevel=logging.INFO, + raiseExceptions=True): + """ + inits log if none given + + log + for parse messages, default logs to sys.stderr + defaultloglevel + if none give this is logging.DEBUG + raiseExceptions + - True: Errors will be raised e.g. during building + - False: Errors will be written to the log, this is the + default behaviour when parsing + """ + # may be disabled during setting of known valid items + self.enabled = True + + if log: + self._log = log + else: + import sys + self._log = logging.getLogger('CSSUTILS') + hdlr = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter('%(levelname)s\t%(message)s') + hdlr.setFormatter(formatter) + self._log.addHandler(hdlr) + self._log.setLevel(defaultloglevel) + + self.raiseExceptions = raiseExceptions + + def __getattr__(self, name): + "use self._log items" + calls = ('debug', 'info', 'warn', 'error', 'critical', 'fatal') + other = ('setLevel', 'getEffectiveLevel', 'addHandler', 'removeHandler') + + if name in calls: + self._logcall = getattr(self._log, name) + return self.__handle + elif name in other: + return getattr(self._log, name) + else: + raise AttributeError( + '(errorhandler) No Attribute %r found' % name) + + def __handle(self, msg=u'', token=None, error=xml.dom.SyntaxErr, + neverraise=False, args=None): + """ + handles all calls + logs or raises exception + """ + if self.enabled: + if error is None: + error = xml.dom.SyntaxErr + + line, col = None, None + if token: + if isinstance(token, tuple): + value, line, col = token[1], token[2], token[3] + else: + value, line, col = token.value, token.line, token.col + msg = u'%s [%s:%s: %s]' % ( + msg, line, col, value) + + if error and self.raiseExceptions and not neverraise: + if isinstance(error, urllib2.HTTPError) or isinstance(error, urllib2.URLError): + raise + elif issubclass(error, xml.dom.DOMException): + error.line = line + error.col = col + raise error(msg) + else: + self._logcall(msg) + + def setLog(self, log): + """set log of errorhandler's log""" + self._log = log + + +class ErrorHandler(_ErrorHandler): + "Singleton, see _ErrorHandler" + instance = None + + def __init__(self, + log=None, defaultloglevel=logging.INFO, raiseExceptions=True): + + if ErrorHandler.instance is None: + ErrorHandler.instance = _ErrorHandler(log=log, + defaultloglevel=defaultloglevel, + raiseExceptions=raiseExceptions) + self.__dict__ = ErrorHandler.instance.__dict__ diff --git a/libs/cssutils/helper.py b/libs/cssutils/helper.py new file mode 100755 index 00000000..b3c55ff3 --- /dev/null +++ b/libs/cssutils/helper.py @@ -0,0 +1,137 @@ +"""cssutils helper +""" +__docformat__ = 'restructuredtext' +__version__ = '$Id: errorhandler.py 1234 2008-05-22 20:26:12Z cthedot $' + +import os +import re +import sys +import urllib + +class Deprecated(object): + """This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + + It accepts a single paramter ``msg`` which is shown with the warning. + It should contain information which function or method to use instead. + """ + def __init__(self, msg): + self.msg = msg + + def __call__(self, func): + def newFunc(*args, **kwargs): + import warnings + warnings.warn("Call to deprecated method %r. %s" % + (func.__name__, self.msg), + category=DeprecationWarning, + stacklevel=2) + return func(*args, **kwargs) + newFunc.__name__ = func.__name__ + newFunc.__doc__ = func.__doc__ + newFunc.__dict__.update(func.__dict__) + return newFunc + +# simple escapes, all non unicodes +_simpleescapes = re.compile(ur'(\\[^0-9a-fA-F])').sub +def normalize(x): + """ + normalizes x, namely: + + - remove any \ before non unicode sequences (0-9a-zA-Z) so for + x=="c\olor\" return "color" (unicode escape sequences should have + been resolved by the tokenizer already) + - lowercase + """ + if x: + def removeescape(matchobj): + return matchobj.group(0)[1:] + x = _simpleescapes(removeescape, x) + return x.lower() + else: + return x + +def path2url(path): + """Return file URL of `path`""" + return u'file:' + urllib.pathname2url(os.path.abspath(path)) + +def pushtoken(token, tokens): + """Return new generator starting with token followed by all tokens in + ``tokens``""" + # TODO: may use itertools.chain? + yield token + for t in tokens: + yield t + +def string(value): + """ + Serialize value with quotes e.g.:: + + ``a \'string`` => ``'a \'string'`` + """ + # \n = 0xa, \r = 0xd, \f = 0xc + value = value.replace(u'\n', u'\\a ').replace( + u'\r', u'\\d ').replace( + u'\f', u'\\c ').replace( + u'"', u'\\"') + + if value.endswith(u'\\'): + value = value[:-1] + u'\\\\' + + return u'"%s"' % value + +def stringvalue(string): + """ + Retrieve actual value of string without quotes. Escaped + quotes inside the value are resolved, e.g.:: + + ``'a \'string'`` => ``a 'string`` + """ + return string.replace(u'\\'+string[0], string[0])[1:-1] + +_match_forbidden_in_uri = re.compile(ur'''.*?[\(\)\s\;,'"]''', re.U).match +def uri(value): + """ + Serialize value by adding ``url()`` and with quotes if needed e.g.:: + + ``"`` => ``url("\"")`` + """ + if _match_forbidden_in_uri(value): + value = string(value) + return u'url(%s)' % value + +def urivalue(uri): + """ + Return actual content without surrounding "url(" and ")" + and removed surrounding quotes too including contained + escapes of quotes, e.g.:: + + ``url("\"")`` => ``"`` + """ + uri = uri[uri.find('(')+1:-1].strip() + if uri and (uri[0] in '\'"') and (uri[0] == uri[-1]): + return stringvalue(uri) + else: + return uri + +#def normalnumber(num): +# """ +# Return normalized number as string. +# """ +# sign = '' +# if num.startswith('-'): +# sign = '-' +# num = num[1:] +# elif num.startswith('+'): +# num = num[1:] +# +# if float(num) == 0.0: +# return '0' +# else: +# if num.find('.') == -1: +# return sign + str(int(num)) +# else: +# a, b = num.split('.') +# if not a: +# a = '0' +# return '%s%s.%s' % (sign, int(a), b) diff --git a/libs/cssutils/parse.py b/libs/cssutils/parse.py new file mode 100755 index 00000000..715a320a --- /dev/null +++ b/libs/cssutils/parse.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +"""A validating CSSParser""" +__all__ = ['CSSParser'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from helper import path2url +import codecs +import cssutils +import os +import sys +import tokenize2 +import urllib + +from cssutils import css + +if sys.version_info < (2,6): + bytes = str + +class CSSParser(object): + """Parse a CSS StyleSheet from URL, string or file and return a DOM Level 2 + CSS StyleSheet object. + + Usage:: + + parser = CSSParser() + # optionally + parser.setFetcher(fetcher) + sheet = parser.parseFile('test1.css', 'ascii') + print sheet.cssText + """ + def __init__(self, log=None, loglevel=None, raiseExceptions=None, + fetcher=None, parseComments=True, + validate=True): + """ + :param log: + logging object + :param loglevel: + logging loglevel + :param raiseExceptions: + if log should simply log (default) or raise errors during + parsing. Later while working with the resulting sheets + the setting used in cssutils.log.raiseExeptions is used + :param fetcher: + see ``setFetcher(fetcher)`` + :param parseComments: + if comments should be added to CSS DOM or simply omitted + :param validate: + if parsing should validate, may be overwritten in parse methods + """ + if log is not None: + cssutils.log.setLog(log) + if loglevel is not None: + cssutils.log.setLevel(loglevel) + + # remember global setting + self.__globalRaising = cssutils.log.raiseExceptions + if raiseExceptions: + self.__parseRaising = raiseExceptions + else: + # DEFAULT during parse + self.__parseRaising = False + + self.__tokenizer = tokenize2.Tokenizer(doComments=parseComments) + self.setFetcher(fetcher) + + self._validate = validate + + def __parseSetting(self, parse): + """during parse exceptions may be handled differently depending on + init parameter ``raiseExceptions`` + """ + if parse: + cssutils.log.raiseExceptions = self.__parseRaising + else: + cssutils.log.raiseExceptions = self.__globalRaising + + def parseStyle(self, cssText, encoding='utf-8', validate=None): + """Parse given `cssText` which is assumed to be the content of + a HTML style attribute. + + :param cssText: + CSS string to parse + :param encoding: + It will be used to decode `cssText` if given as a (byte) + string. + :param validate: + If given defines if validation is used. Uses CSSParser settings as + fallback + :returns: + :class:`~cssutils.css.CSSStyleDeclaration` + """ + self.__parseSetting(True) + if isinstance(cssText, bytes): + # TODO: use codecs.getdecoder('css') here? + cssText = cssText.decode(encoding) + if validate is None: + validate = self._validate + style = css.CSSStyleDeclaration(cssText, validating=validate) + self.__parseSetting(False) + return style + + def parseString(self, cssText, encoding=None, href=None, media=None, + title=None, + validate=None): + """Parse `cssText` as :class:`~cssutils.css.CSSStyleSheet`. + Errors may be raised (e.g. UnicodeDecodeError). + + :param cssText: + CSS string to parse + :param encoding: + If ``None`` the encoding will be read from BOM or an @charset + rule or defaults to UTF-8. + If given overrides any found encoding including the ones for + imported sheets. + It also will be used to decode `cssText` if given as a (byte) + string. + :param href: + The ``href`` attribute to assign to the parsed style sheet. + Used to resolve other urls in the parsed sheet like @import hrefs. + :param media: + The ``media`` attribute to assign to the parsed style sheet + (may be a MediaList, list or a string). + :param title: + The ``title`` attribute to assign to the parsed style sheet. + :param validate: + If given defines if validation is used. Uses CSSParser settings as + fallback + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + self.__parseSetting(True) + # TODO: py3 needs bytes here! + if isinstance(cssText, bytes): + cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0] + + if validate is None: + validate = self._validate + + sheet = cssutils.css.CSSStyleSheet(href=href, + media=cssutils.stylesheets.MediaList(media), + title=title, + validating=validate) + sheet._setFetcher(self.__fetcher) + # tokenizing this ways closes open constructs and adds EOF + sheet._setCssTextWithEncodingOverride(self.__tokenizer.tokenize(cssText, + fullsheet=True), + encodingOverride=encoding) + self.__parseSetting(False) + return sheet + + def parseFile(self, filename, encoding=None, + href=None, media=None, title=None, + validate=None): + """Retrieve content from `filename` and parse it. Errors may be raised + (e.g. IOError). + + :param filename: + of the CSS file to parse, if no `href` is given filename is + converted to a (file:) URL and set as ``href`` of resulting + stylesheet. + If `href` is given it is set as ``sheet.href``. Either way + ``sheet.href`` is used to resolve e.g. stylesheet imports via + @import rules. + :param encoding: + Value ``None`` defaults to encoding detection via BOM or an + @charset rule. + Other values override detected encoding for the sheet at + `filename` including any imported sheets. + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + if not href: + # prepend // for file URL, urllib does not do this? + #href = u'file:' + urllib.pathname2url(os.path.abspath(filename)) + href = path2url(filename) + + return self.parseString(open(filename, 'rb').read(), + encoding=encoding, # read returns a str + href=href, media=media, title=title, + validate=validate) + + def parseUrl(self, href, encoding=None, media=None, title=None, + validate=None): + """Retrieve content from URL `href` and parse it. Errors may be raised + (e.g. URLError). + + :param href: + URL of the CSS file to parse, will also be set as ``href`` of + resulting stylesheet + :param encoding: + Value ``None`` defaults to encoding detection via HTTP, BOM or an + @charset rule. + A value overrides detected encoding for the sheet at ``href`` + including any imported sheets. + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + encoding, enctype, text = cssutils.util._readUrl(href, + fetcher=self.__fetcher, + overrideEncoding=encoding) + if enctype == 5: + # do not use if defaulting to UTF-8 + encoding = None + + if text is not None: + return self.parseString(text, encoding=encoding, + href=href, media=media, title=title, + validate=validate) + + def setFetcher(self, fetcher=None): + """Replace the default URL fetch function with a custom one. + + :param fetcher: + A function which gets a single parameter + + ``url`` + the URL to read + + and must return ``(encoding, content)`` where ``encoding`` is the + HTTP charset normally given via the Content-Type header (which may + simply omit the charset in which case ``encoding`` would be + ``None``) and ``content`` being the string (or unicode) content. + + The Mimetype should be 'text/css' but this has to be checked by the + fetcher itself (the default fetcher emits a warning if encountering + a different mimetype). + + Calling ``setFetcher`` with ``fetcher=None`` resets cssutils + to use its default function. + """ + self.__fetcher = fetcher diff --git a/libs/cssutils/prodparser.py b/libs/cssutils/prodparser.py new file mode 100755 index 00000000..ac7825cc --- /dev/null +++ b/libs/cssutils/prodparser.py @@ -0,0 +1,733 @@ +# -*- coding: utf-8 -*- +"""Productions parser used by css and stylesheets classes to parse +test into a cssutils.util.Seq and at the same time retrieving +additional specific cssutils.util.Item objects for later use. + +TODO: + - ProdsParser + - handle EOF or STOP? + - handle unknown @rules + - handle S: maybe save to Seq? parameterized? + - store['_raw']: always? + + - Sequence: + - opt first(), naive impl for now + +""" +__all__ = ['ProdParser', 'Sequence', 'Choice', 'Prod', 'PreDef'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1418 2008-08-09 19:27:50Z cthedot $' + +from helper import pushtoken +import cssutils +import re +import string +import sys + + +class ParseError(Exception): + """Base Exception class for ProdParser (used internally).""" + pass + +class Done(ParseError): + """Raised if Sequence or Choice is finished and no more Prods left.""" + pass + +class Exhausted(ParseError): + """Raised if Sequence or Choice is finished but token is given.""" + pass + +class Missing(ParseError): + """Raised if Sequence or Choice is not finished but no matching token given.""" + pass + +class NoMatch(ParseError): + """Raised if nothing in Sequence or Choice does match.""" + pass + + +class Choice(object): + """A Choice of productions (Sequence or single Prod).""" + + def __init__(self, *prods, **options): + """ + *prods + Prod or Sequence objects + options: + optional=False + """ + self._prods = prods + + try: + self.optional = options['optional'] + except KeyError, e: + for p in self._prods: + if p.optional: + self.optional = True + break + else: + self.optional = False + + self.reset() + + def reset(self): + """Start Choice from zero""" + self._exhausted = False + + def matches(self, token): + """Check if token matches""" + for prod in self._prods: + if prod.matches(token): + return True + return False + + def nextProd(self, token): + """ + Return: + + - next matching Prod or Sequence + - ``None`` if any Prod or Sequence is optional and no token matched + - raise ParseError if nothing matches and all are mandatory + - raise Exhausted if choice already done + + ``token`` may be None but this occurs when no tokens left.""" + if not self._exhausted: + optional = False + for x in self._prods: + if x.matches(token): + self._exhausted = True + x.reset() + return x + elif x.optional: + optional = True + else: + if not optional: + # None matched but also None is optional + raise ParseError(u'No match in %s' % self) + elif token: + raise Exhausted(u'Extra token') + + def __str__(self): + return u'Choice(%s)' % u', '.join([str(x) for x in self._prods]) + + +class Sequence(object): + """A Sequence of productions (Choice or single Prod).""" + def __init__(self, *prods, **options): + """ + *prods + Prod or Sequence objects + **options: + minmax = lambda: (1, 1) + callback returning number of times this sequence may run + """ + self._prods = prods + try: + minmax = options['minmax'] + except KeyError: + minmax = lambda: (1, 1) + + self._min, self._max = minmax() + if self._max is None: + # unlimited + try: + # py2.6/3 + self._max = sys.maxsize + except AttributeError: + # py<2.6 + self._max = sys.maxint + + self._prodcount = len(self._prods) + self.reset() + + def matches(self, token): + """Called by Choice to try to find if Sequence matches.""" + for prod in self._prods: + if prod.matches(token): + return True + try: + if not prod.optional: + break + except AttributeError: + pass + return False + + def reset(self): + """Reset this Sequence if it is nested.""" + self._roundstarted = False + self._i = 0 + self._round = 0 + + def _currentName(self): + """Return current element of Sequence, used by name""" + # TODO: current impl first only if 1st if an prod! + for prod in self._prods[self._i:]: + if not prod.optional: + return str(prod) + else: + return 'Sequence' + + optional = property(lambda self: self._min == 0) + + def nextProd(self, token): + """Return + + - next matching Prod or Choice + - raises ParseError if nothing matches + - raises Exhausted if sequence already done + """ + while self._round < self._max: + # for this round + i = self._i + round = self._round + p = self._prods[i] + if i == 0: + self._roundstarted = False + + # for next round + self._i += 1 + if self._i == self._prodcount: + self._round += 1 + self._i = 0 + + if p.matches(token): + self._roundstarted = True + # reset nested Choice or Prod to use from start + p.reset() + return p + + elif p.optional: + continue + + elif round < self._min: + raise Missing(u'Missing token for production %s' % p) + + elif not token: + if self._roundstarted: + raise Missing(u'Missing token for production %s' % p) + else: + raise Done() + + else: + raise NoMatch(u'No matching production for token') + + if token: + raise Exhausted(u'Extra token') + + def __str__(self): + return u'Sequence(%s)' % u', '.join([str(x) for x in self._prods]) + + +class Prod(object): + """Single Prod in Sequence or Choice.""" + def __init__(self, name, match, optional=False, + toSeq=None, toStore=None, + stop=False, stopAndKeep=False, + nextSor=False, mayEnd=False, + storeToken=None, + exception=None): + """ + name + name used for error reporting + match callback + function called with parameters tokentype and tokenvalue + returning True, False or raising ParseError + toSeq callback (optional) or False + calling toSeq(token, tokens) returns (type_, val) == (token[0], token[1]) + to be appended to seq else simply unaltered (type_, val) + + if False nothing is added + + toStore (optional) + key to save util.Item to store or callback(store, util.Item) + optional = False + wether Prod is optional or not + stop = False + if True stop parsing of tokens here + stopAndKeep + if True stop parsing of tokens here but return stopping + token in unused tokens + nextSor=False + next is S or other like , or / (CSSValue) + mayEnd = False + no token must follow even defined by Sequence. + Used for operator ',/ ' currently only + + storeToken = None + if True toStore saves simple token tuple and not and Item object + to store. Old style processing, TODO: resolve + + exception = None + exception to be raised in case of error, normaly SyntaxErr + """ + self._name = name + self.match = match + self.optional = optional + self.stop = stop + self.stopAndKeep = stopAndKeep + self.nextSor = nextSor + self.mayEnd = mayEnd + self.storeToken = storeToken + self.exception = exception + + def makeToStore(key): + "Return a function used by toStore." + def toStore(store, item): + "Set or append store item." + if key in store: + _v = store[key] + if not isinstance(_v, list): + store[key] = [_v] + store[key].append(item) + else: + store[key] = item + return toStore + + if toSeq or toSeq is False: + # called: seq.append(toSeq(value)) + self.toSeq = toSeq + else: + self.toSeq = lambda t, tokens: (t[0], t[1]) + + if hasattr(toStore, '__call__'): + self.toStore = toStore + elif toStore: + self.toStore = makeToStore(toStore) + else: + # always set! + self.toStore = None + + def matches(self, token): + """Return if token matches.""" + if not token: + return False + type_, val, line, col = token + return self.match(type_, val) + + def reset(self): + pass + + def __str__(self): + return self._name + + def __repr__(self): + return "" % ( + self.__class__.__name__, self._name, id(self)) + + +# global tokenizer as there is only one! +tokenizer = cssutils.tokenize2.Tokenizer() + +class ProdParser(object): + """Productions parser.""" + def __init__(self, clear=True): + self.types = cssutils.cssproductions.CSSProductions + self._log = cssutils.log + if clear: + tokenizer.clear() + + def _texttotokens(self, text): + """Build a generator which is the only thing that is parsed! + old classes may use lists etc + """ + if isinstance(text, basestring): + # DEFAULT, to tokenize strip space + return tokenizer.tokenize(text.strip()) + + elif isinstance(text, tuple): + # OLD: (token, tokens) or a single token + if len(text) == 2: + # (token, tokens) + chain([token], tokens) + else: + # single token + return iter([text]) + + elif isinstance(text, list): + # OLD: generator from list + return iter(text) + + else: + # DEFAULT, already tokenized, assume generator + return text + + def _SorTokens(self, tokens, until=',/'): + """New tokens generator which has S tokens removed, + if followed by anything in ``until``, normally a ``,``.""" + for token in tokens: + if token[0] == self.types.S: + try: + next_ = tokens.next() + except StopIteration: + yield token + else: + if next_[1] in until: + # omit S as e.g. ``,`` has been found + yield next_ + elif next_[0] == self.types.COMMENT: + # pass COMMENT + yield next_ + else: + yield token + yield next_ + + elif token[0] == self.types.COMMENT: + # pass COMMENT + yield token + else: + yield token + break + # normal mode again + for token in tokens: + yield token + + + def parse(self, text, name, productions, keepS=False, store=None): + """ + text (or token generator) + to parse, will be tokenized if not a generator yet + + may be: + - a string to be tokenized + - a single token, a tuple + - a tuple of (token, tokensGenerator) + - already tokenized so a tokens generator + + name + used for logging + productions + used to parse tokens + keepS + if WS should be added to Seq or just be ignored + store UPDATED + If a Prod defines ``toStore`` the key defined there + is a key in store to be set or if store[key] is a list + the next Item is appended here. + + TODO: NEEDED? : + Key ``raw`` is always added and holds all unprocessed + values found + + returns + :wellformed: True or False + :seq: a filled cssutils.util.Seq object which is NOT readonly yet + :store: filled keys defined by Prod.toStore + :unusedtokens: token generator containing tokens not used yet + """ + tokens = self._texttotokens(text) + if not tokens: + self._log.error(u'No content to parse.') + # TODO: return??? + + seq = cssutils.util.Seq(readonly=False) + if not store: # store for specific values + store = {} + prods = [productions] # stack of productions + wellformed = True + + # while no real token is found any S are ignored + started = False + stopall = False + prod = None + # flag if default S handling should be done + defaultS = True + while True: + try: + token = tokens.next() + except StopIteration: + break + type_, val, line, col = token + + # default productions + if type_ == self.types.COMMENT: + # always append COMMENT + seq.append(cssutils.css.CSSComment(val), + cssutils.css.CSSComment, line, col) + elif defaultS and type_ == self.types.S: + # append S (but ignore starting ones) + if not keepS or not started: + continue + else: + seq.append(val, type_, line, col) +# elif type_ == self.types.ATKEYWORD: +# # @rule +# r = cssutils.css.CSSUnknownRule(cssText=val) +# seq.append(r, type(r), line, col) + elif type_ == self.types.INVALID: + # invalidate parse + wellformed = False + self._log.error(u'Invalid token: %r' % (token,)) + break + elif type_ == 'EOF': + # do nothing? (self.types.EOF == True!) + pass + else: + started = True # check S now + nextSor = False # reset + + try: + while True: + # find next matching production + try: + prod = prods[-1].nextProd(token) + except (Exhausted, NoMatch), e: + # try next + prod = None + if isinstance(prod, Prod): + # found actual Prod, not a Choice or Sequence + break + elif prod: + # nested Sequence, Choice + prods.append(prod) + else: + # nested exhausted, try in parent + if len(prods) > 1: + prods.pop() + else: + raise ParseError('No match') + except ParseError, e: + wellformed = False + self._log.error(u'%s: %s: %r' % (name, e, token)) + break + else: + # process prod + if prod.toSeq and not prod.stopAndKeep: + type_, val = prod.toSeq(token, tokens) + if val is not None: + seq.append(val, type_, line, col) + if prod.toStore: + if not prod.storeToken: + prod.toStore(store, seq[-1]) + else: + # workaround for now for old style token + # parsing! + # TODO: remove when all new style + prod.toStore(store, token) + + if prod.stop: # EOF? + # stop here and ignore following tokens + break + + if prod.stopAndKeep: # e.g. ; + # stop here and ignore following tokens + # but keep this token for next run + tokenizer.push(token) + stopall = True + break + + if prod.nextSor: + # following is S or other token (e.g. ",")? + # remove S if + tokens = self._SorTokens(tokens, ',/') + defaultS = False + else: + defaultS = True + + lastprod = prod + + if not stopall: + # stop immediately + while True: + # all productions exhausted? + try: + prod = prods[-1].nextProd(token=None) + except Done, e: + # ok + prod = None + + except Missing, e: + prod = None + # last was a S operator which may End a Sequence, then ok + if hasattr(lastprod, 'mayEnd') and not lastprod.mayEnd: + wellformed = False + self._log.error(u'%s: %s' % (name, e)) + + except ParseError, e: + prod = None + wellformed = False + self._log.error(u'%s: %s' % (name, e)) + + else: + if prods[-1].optional: + prod = None + elif prod and prod.optional: + # ignore optional + continue + + if prod and not prod.optional: + wellformed = False + self._log.error(u'%s: Missing token for production %r' + % (name, str(prod))) + break + elif len(prods) > 1: + # nested exhausted, next in parent + prods.pop() + else: + break + + # trim S from end + seq.rstrip() + return wellformed, seq, store, tokens + + +class PreDef(object): + """Predefined Prod definition for use in productions definition + for ProdParser instances. + """ + types = cssutils.cssproductions.CSSProductions + reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$') + + @staticmethod + def calc(toSeq=None, nextSor=False): + return Prod(name=u'calcfunction', + match=lambda t, v: u'calc(' == cssutils.helper.normalize(v), + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def char(name='char', char=u',', toSeq=None, + stop=False, stopAndKeep=False, + optional=True, nextSor=False): + "any CHAR" + return Prod(name=name, match=lambda t, v: v == char, toSeq=toSeq, + stop=stop, stopAndKeep=stopAndKeep, optional=optional, + nextSor=nextSor) + + @staticmethod + def comma(): + return PreDef.char(u'comma', u',') + + @staticmethod + def dimension(nextSor=False, stop=False): + return Prod(name=u'dimension', + match=lambda t, v: t == PreDef.types.DIMENSION, + toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def function(toSeq=None, nextSor=False): + return Prod(name=u'function', + match=lambda t, v: t == PreDef.types.FUNCTION, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def funcEnd(stop=False): + ")" + return PreDef.char(u'end FUNC ")"', u')', + stop=stop) + + @staticmethod + def hexcolor(stop=False, nextSor=False): + "#123 or #123456" + return Prod(name='HEX color', + match=lambda t, v: ( + t == PreDef.types.HASH and + PreDef.reHexcolor.match(v) + ), + stop=stop, + nextSor=nextSor) + + @staticmethod + def ident(stop=False, toStore=None, nextSor=False): + return Prod(name=u'ident', + match=lambda t, v: t == PreDef.types.IDENT, + stop=stop, + toStore=toStore, + nextSor=nextSor) + + @staticmethod + def number(stop=False, toSeq=None, nextSor=False): + return Prod(name=u'number', + match=lambda t, v: t == PreDef.types.NUMBER, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def percentage(stop=False, toSeq=None, nextSor=False): + return Prod(name=u'percentage', + match=lambda t, v: t == PreDef.types.PERCENTAGE, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def string(stop=False, nextSor=False): + "string delimiters are removed by default" + return Prod(name=u'string', + match=lambda t, v: t == PreDef.types.STRING, + toSeq=lambda t, tokens: (t[0], cssutils.helper.stringvalue(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def S(name=u'whitespace', toSeq=None, optional=False): + return Prod(name=name, + match=lambda t, v: t == PreDef.types.S, + toSeq=toSeq, + optional=optional, + mayEnd=True) + + @staticmethod + def unary(stop=False, toSeq=None, nextSor=False): + "+ or -" + return Prod(name=u'unary +-', match=lambda t, v: v in (u'+', u'-'), + optional=True, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def uri(stop=False, nextSor=False): + "'url(' and ')' are removed and URI is stripped" + return Prod(name=u'URI', + match=lambda t, v: t == PreDef.types.URI, + toSeq=lambda t, tokens: (t[0], cssutils.helper.urivalue(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def unicode_range(stop=False, nextSor=False): + "u+123456-abc normalized to lower `u`" + return Prod(name='unicode-range', + match=lambda t, v: t == PreDef.types.UNICODE_RANGE, + toSeq=lambda t, tokens: (t[0], t[1].lower()), + stop=stop, + nextSor=nextSor + ) + + @staticmethod + def variable(toSeq=None, stop=False, nextSor=False): + return Prod(name=u'variable', + match=lambda t, v: u'var(' == cssutils.helper.normalize(v), + toSeq=toSeq, + stop=stop, + nextSor=nextSor) + + # used for MarginRule for now: + @staticmethod + def unknownrule(name=u'@', toStore=None): + """@rule dummy (matches ATKEYWORD to remove unknown rule tokens from + stream:: + + @x; + @x {...} + + no nested yet! + """ + def rule(tokens): + saved = [] + for t in tokens: + saved.append(t) + if (t[1] == u'}' or t[1] == u';'): + return cssutils.css.CSSUnknownRule(saved) + + return Prod(name=name, + match=lambda t, v: t == u'ATKEYWORD', + toSeq=lambda t, tokens: (u'CSSUnknownRule', + rule(pushtoken(t, tokens)) + ), + toStore=toStore + ) diff --git a/libs/cssutils/profiles.py b/libs/cssutils/profiles.py new file mode 100755 index 00000000..57c63bd4 --- /dev/null +++ b/libs/cssutils/profiles.py @@ -0,0 +1,791 @@ +"""CSS profiles. + +Profiles is based on code by Kevin D. Smith, orginally used as cssvalues, +thanks! +""" +__all__ = ['Profiles'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssproperties.py 1116 2008-03-05 13:52:23Z cthedot $' + +import re +import types + +class NoSuchProfileException(Exception): + """Raised if no profile with given name is found""" + pass + + +# dummies, replaced in Profiles.addProfile +_fontRegexReplacements = { + '__FONT_FAMILY_SINGLE': lambda f: False, + '__FONT_WITH_1_FAMILY': lambda f: False + } + +def _fontFamilyValidator(families): + """Check if ``font-family`` value is valid, regex is too slow. + + Splits on ``,`` and checks each family separately. + Somehow naive as font-family name could contain a "," but this is unlikely. + Still should be a TODO. + """ + match = _fontRegexReplacements['__FONT_FAMILY_SINGLE'] + + for f in families.split(u','): + if not match(f.strip()): + return False + return True + +def _fontValidator(font): + """Check if font value is valid, regex is too slow. + + Checks everything before ``,`` on basic font value. Everything after should + be a valid font-family value. + """ + if u',' in font: + # split off until 1st family + font1, families2 = font.split(u',', 1) + else: + font1, families2 = font, None + + if not _fontRegexReplacements['__FONT_WITH_1_FAMILY'](font1.strip()): + return False + + if families2 and not _fontFamilyValidator(families2): + return False + + return True + + +class Profiles(object): + """ + All profiles used for validation. ``cssutils.profile`` is a + preset object of this class and used by all properties for validation. + + Predefined profiles are (use + :meth:`~cssutils.profiles.Profiles.propertiesByProfile` to + get a list of defined properties): + + :attr:`~cssutils.profiles.Profiles.CSS_LEVEL_2` + Properties defined by CSS2.1 + :attr:`~cssutils.profiles.Profiles.CSS3_BASIC_USER_INTERFACE` + Currently resize and outline properties only + :attr:`~cssutils.profiles.Profiles.CSS3_BOX` + Currently overflow related properties only + :attr:`~cssutils.profiles.Profiles.CSS3_COLOR` + CSS 3 color properties + :attr:`~cssutils.profiles.Profiles.CSS3_PAGED_MEDIA` + As defined at http://www.w3.org/TR/css3-page/ (at 090307) + + Predefined macros are: + + :attr:`~cssutils.profiles.Profiles._TOKEN_MACROS` + Macros containing the token values as defined to CSS2 + :attr:`~cssutils.profiles.Profiles._MACROS` + Additional general macros. + + If you want to redefine any of these macros do this in your custom + macros. + """ + CSS_LEVEL_2 = u'CSS Level 2.1' + CSS3_BACKGROUNDS_AND_BORDERS = u'CSS Backgrounds and Borders Module Level 3' + CSS3_BASIC_USER_INTERFACE = u'CSS3 Basic User Interface Module' + CSS3_BOX = CSS_BOX_LEVEL_3 = u'CSS Box Module Level 3' + CSS3_COLOR = CSS_COLOR_LEVEL_3 = u'CSS Color Module Level 3' + CSS3_FONTS = u'CSS Fonts Module Level 3' + CSS3_FONT_FACE = u'CSS Fonts Module Level 3 @font-face properties' + CSS3_PAGED_MEDIA = u'CSS3 Paged Media Module' + CSS3_TEXT = u'CSS Text Level 3' + + _TOKEN_MACROS = { + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + 'nmstart': r'[_a-z]|{nonascii}|{escape}', + 'nonascii': r'[^\0-\177]', + 'unicode': r'\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?', + 'escape': r'{unicode}|\\[ -~\200-\777]', + # 'escape': r'{unicode}|\\[ -~\200-\4177777]', + 'int': r'[-]?\d+', + 'nmchar': r'[\w-]|{nonascii}|{escape}', + 'num': r'[-]?\d+|[-]?\d*\.\d+', + 'positivenum': r'\d+|\d*\.\d+', + 'number': r'{num}', + 'string': r'{string1}|{string2}', + 'string1': r'"(\\\"|[^\"])*"', + 'uri': r'url\({w}({string}|(\\\)|[^\)])+){w}\)', + 'string2': r"'(\\\'|[^\'])*'", + 'nl': r'\n|\r\n|\r|\f', + 'w': r'\s*', + } + _MACROS = { + 'hexcolor': r'#[0-9a-f]{3}|#[0-9a-f]{6}', + 'rgbcolor': r'rgb\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\)|rgb\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\)', + 'namedcolor': r'(transparent|orange|maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray)', + 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)', + 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{uicolor}', + #'color': r'(maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray|ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)|#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\({w}{int}{w},{w}{int}{w},{w}{int}{w}\)|rgb\({w}{num}%{w},{w}{num}%{w},{w}{num}%{w}\)', + 'integer': r'{int}', + 'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)', + 'positivelength': r'0|{positivenum}(em|ex|px|in|cm|mm|pt|pc)', + 'angle': r'0|{num}(deg|grad|rad)', + 'time': r'0|{num}m?s', + 'frequency': r'0|{num}k?Hz', + 'percentage': r'{num}%', + 'shadow': '(inset)?{w}{length}{w}{length}{w}{length}?{w}{length}?{w}{color}?' + } + + def __init__(self, log=None): + """A few profiles are predefined.""" + self._log = log + + # macro cache + self._usedMacros = Profiles._TOKEN_MACROS.copy() + self._usedMacros.update(Profiles._MACROS.copy()) + + # to keep order, REFACTOR! + self._profileNames = [] + # for reset if macro changes + self._rawProfiles = {} + # already compiled profiles: {profile: {property: checkfunc, ...}, ...} + self._profilesProperties = {} + + self._defaultProfiles = None + + self.addProfiles([(self.CSS_LEVEL_2, + properties[self.CSS_LEVEL_2], + macros[self.CSS_LEVEL_2] + ), + (self.CSS3_BACKGROUNDS_AND_BORDERS, + properties[self.CSS3_BACKGROUNDS_AND_BORDERS], + macros[self.CSS3_BACKGROUNDS_AND_BORDERS] + ), + (self.CSS3_BASIC_USER_INTERFACE, + properties[self.CSS3_BASIC_USER_INTERFACE], + macros[self.CSS3_BASIC_USER_INTERFACE] + ), + (self.CSS3_BOX, + properties[self.CSS3_BOX], + macros[self.CSS3_BOX] + ), + (self.CSS3_COLOR, + properties[self.CSS3_COLOR], + macros[self.CSS3_COLOR] + ), + (self.CSS3_FONTS, + properties[self.CSS3_FONTS], + macros[self.CSS3_FONTS] + ), + # new object for font-face only? + (self.CSS3_FONT_FACE, + properties[self.CSS3_FONT_FACE], + macros[self.CSS3_FONTS] + ), + (self.CSS3_PAGED_MEDIA, + properties[self.CSS3_PAGED_MEDIA], + macros[self.CSS3_PAGED_MEDIA] + ), + (self.CSS3_TEXT, + properties[self.CSS3_TEXT], + macros[self.CSS3_TEXT] + ) + ]) + + self.__update_knownNames() + + def _expand_macros(self, dictionary, macros): + """Expand macros in token dictionary""" + def macro_value(m): + return '(?:%s)' % macros[m.groupdict()['macro']] + + for key, value in dictionary.items(): + if not hasattr(value, '__call__'): + while re.search(r'{[a-z][a-z0-9-]*}', value): + value = re.sub(r'{(?P[a-z][a-z0-9-]*)}', + macro_value, value) + dictionary[key] = value + + return dictionary + + def _compile_regexes(self, dictionary): + """Compile all regular expressions into callable objects""" + for key, value in dictionary.items(): + # might be a function (font-family) as regex is too slow + if not hasattr(value, '__call__') and not isinstance(value, + types.FunctionType): + value = re.compile('^(?:%s)$' % value, re.I).match + dictionary[key] = value + + return dictionary + + def __update_knownNames(self): + self._knownNames = [] + for properties in self._profilesProperties.values(): + self._knownNames.extend(properties.keys()) + + def _getDefaultProfiles(self): + "If not explicitly set same as Profiles.profiles but in reverse order." + if not self._defaultProfiles: + return self.profiles + else: + return self._defaultProfiles + + def _setDefaultProfiles(self, profiles): + "profiles may be a single or a list of profile names" + if isinstance(profiles, basestring): + self._defaultProfiles = (profiles,) + else: + self._defaultProfiles = profiles + + defaultProfiles = property(_getDefaultProfiles, + _setDefaultProfiles, + doc=u"Names of profiles to use for validation." + u"To use e.g. the CSS2 profile set " + u"``cssutils.profile.defaultProfiles = " + u"cssutils.profile.CSS_LEVEL_2``") + + profiles = property(lambda self: self._profileNames, + doc=u'Names of all profiles in order as defined.') + + knownNames = property(lambda self: self._knownNames, + doc="All known property names of all profiles.") + + def _resetProperties(self, newMacros=None): + "reset all props from raw values as changes in macros happened" + # base + macros = Profiles._TOKEN_MACROS.copy() + macros.update(Profiles._MACROS.copy()) + + # former + for profile in self._profileNames: + macros.update(self._rawProfiles[profile]['macros']) + + # new + if newMacros: + macros.update(newMacros) + + # reset properties + self._profilesProperties.clear() + for profile in self._profileNames: + properties = self._expand_macros( + # keep raw + self._rawProfiles[profile]['properties'].copy(), + macros) + self._profilesProperties[profile] = self._compile_regexes(properties) + + # save + self._usedMacros = macros + + + def addProfiles(self, profiles): + """Add a list of profiles at once. Useful as if profiles define custom + macros these are used in one go. Using `addProfile` instead my be + **very** slow instead. + """ + # add macros + for profile, properties, macros in profiles: + if macros: + self._usedMacros.update(macros) + self._rawProfiles[profile] = {'macros': macros.copy()} + + # only add new properties + for profile, properties, macros in profiles: + self.addProfile(profile, properties.copy(), None) + + + def addProfile(self, profile, properties, macros=None): + """Add a new profile with name `profile` (e.g. 'CSS level 2') + and the given `properties`. + + :param profile: + the new `profile`'s name + :param properties: + a dictionary of ``{ property-name: propery-value }`` items where + property-value is a regex which may use macros defined in given + ``macros`` or the standard macros Profiles.tokens and + Profiles.generalvalues. + + ``propery-value`` may also be a function which takes a single + argument which is the value to validate and which should return + True or False. + Any exceptions which may be raised during this custom validation + are reported or raised as all other cssutils exceptions depending + on cssutils.log.raiseExceptions which e.g during parsing normally + is False so the exceptions would be logged only. + :param macros: + may be used in the given properties definitions. There are some + predefined basic macros which may always be used in + :attr:`Profiles._TOKEN_MACROS` and :attr:`Profiles._MACROS`. + """ + if macros: + # check if known macros would change and if yes reset properties + if len(set(macros.keys()).intersection(self._usedMacros.keys())): + self._resetProperties(newMacros=macros) + + else: + # no replacement, simply continue + self._usedMacros.update(macros) + + else: + # might have been set by addProfiles before + try: + macros = self._rawProfiles[profile]['macros'] + except KeyError, e: + macros = {} + + # save name and raw props/macros if macros change to completely reset + self._profileNames.append(profile) + self._rawProfiles[profile] = {'properties': properties.copy(), + 'macros': macros.copy()} + # prepare and save properties + properties = self._expand_macros(properties, self._usedMacros) + self._profilesProperties[profile] = self._compile_regexes(properties) + + self.__update_knownNames() + + # hack for font and font-family which are too slow with regexes + if '__FONT_WITH_1_FAMILY' in properties: + _fontRegexReplacements['__FONT_WITH_1_FAMILY'] = properties['__FONT_WITH_1_FAMILY'] + if '__FONT_FAMILY_SINGLE' in properties: + _fontRegexReplacements['__FONT_FAMILY_SINGLE'] = properties['__FONT_FAMILY_SINGLE'] + + + def removeProfile(self, profile=None, all=False): + """Remove `profile` or remove `all` profiles. + + If the removed profile used custom macros all remaining profiles + are reset to reflect the macro changes. This may be quite an expensive + operation! + + :param profile: + profile name to remove + :param all: + if ``True`` removes all profiles to start with a clean state + :exceptions: + - :exc:`cssutils.profiles.NoSuchProfileException`: + If given `profile` cannot be found. + """ + if all: + self._profilesProperties.clear() + self._rawProfiles.clear() + del self._profileNames[:] + else: + reset = False + + try: + if (self._rawProfiles[profile]['macros']): + reset = True + + del self._profilesProperties[profile] + del self._rawProfiles[profile] + del self._profileNames[self._profileNames.index(profile)] + except KeyError: + raise NoSuchProfileException(u'No profile %r.' % profile) + + else: + if reset: + # reset properties as macros were removed + self._resetProperties() + + self.__update_knownNames() + + def propertiesByProfile(self, profiles=None): + """Generator: Yield property names, if no `profiles` is given all + profile's properties are used. + + :param profiles: + a single profile name or a list of names. + """ + if not profiles: + profiles = self.profiles + elif isinstance(profiles, basestring): + profiles = (profiles, ) + try: + for profile in sorted(profiles): + for name in sorted(self._profilesProperties[profile].keys()): + yield name + except KeyError, e: + raise NoSuchProfileException(e) + + def validate(self, name, value): + """Check if `value` is valid for given property `name` using **any** + profile. + + :param name: + a property name + :param value: + a CSS value (string) + :returns: + if the `value` is valid for the given property `name` in any + profile + """ + for profile in self.profiles: + if name in self._profilesProperties[profile]: + try: + # custom validation errors are caught + r = bool(self._profilesProperties[profile][name](value)) + except Exception, e: + # TODO: more specific exception? + # Validate should not be fatal though! + self._log.error(e, error=Exception) + r = False + if r: + return r + return False + + def validateWithProfile(self, name, value, profiles=None): + """Check if `value` is valid for given property `name` returning + ``(valid, profile)``. + + :param name: + a property name + :param value: + a CSS value (string) + :param profiles: + internal parameter used by Property.validate only + :returns: + ``valid, matching, profiles`` where ``valid`` is if the `value` + is valid for the given property `name` in any profile, + ``matching==True`` if it is valid in the given `profiles` + and ``profiles`` the profile names for which the value is valid + (or ``[]`` if not valid at all) + + Example:: + + >>> cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2 + >>> print cssutils.profile.validateWithProfile('color', 'rgba(1,1,1,1)') + (True, False, Profiles.CSS3_COLOR) + """ + if name not in self.knownNames: + return False, False, [] + else: + if not profiles: + profiles = self.defaultProfiles + elif isinstance(profiles, basestring): + profiles = (profiles, ) + for profilename in reversed(profiles): + # check given profiles + if name in self._profilesProperties[profilename]: + validate = self._profilesProperties[profilename][name] + try: + if validate(value): + return True, True, [profilename] + except Exception, e: + self._log.error(e, error=Exception) + + for profilename in (p for p in self._profileNames + if p not in profiles): + # check remaining profiles as well + if name in self._profilesProperties[profilename]: + validate = self._profilesProperties[profilename][name] + try: + if validate(value): + return True, False, [profilename] + except Exception, e: + self._log.error(e, error=Exception) + + names = [] + for profilename, properties in self._profilesProperties.items(): + # return profile to which name belongs + if name in properties.keys(): + names.append(profilename) + names.sort() + return False, False, names + + +properties = {} +macros = {} + + +""" +Define some regular expression fragments that will be used as +macros within the CSS property value regular expressions. +""" +macros[Profiles.CSS_LEVEL_2] = { + 'background-color': r'{color}|transparent|inherit', + 'background-image': r'{uri}|none|inherit', + #'background-position': r'({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit', + 'background-position': r'({percentage}|{length}|left|center|right)(\s*({percentage}|{length}|top|center|bottom))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit', + 'background-repeat': r'repeat|repeat-x|repeat-y|no-repeat|inherit', + 'background-attachment': r'scroll|fixed|inherit', + 'shape': r'rect\(({w}({length}|auto}){w},){3}{w}({length}|auto){w}\)', + 'counter': r'counter\({w}{ident}{w}(?:,{w}{list-style-type}{w})?\)', + 'identifier': r'{ident}', + 'family-name': r'{string}|{ident}({w}{ident})*', + 'generic-family': r'serif|sans-serif|cursive|fantasy|monospace', + 'absolute-size': r'(x?x-)?(small|large)|medium', + 'relative-size': r'smaller|larger', + + #[[ | ] [, | ]* ] | inherit + #'font-family': r'(({family-name}|{generic-family})({w},{w}({family-name}|{generic-family}))*)|inherit', + # EXTREMELY SLOW REGEX + #'font-family': r'({family-name}({w},{w}{family-name})*)|inherit', + + 'font-size': r'{absolute-size}|{relative-size}|{positivelength}|{percentage}|inherit', + 'font-style': r'normal|italic|oblique|inherit', + 'font-variant': r'normal|small-caps|inherit', + 'font-weight': r'normal|bold|bolder|lighter|[1-9]00|inherit', + 'line-height': r'normal|{number}|{length}|{percentage}|inherit', + 'list-style-image': r'{uri}|none|inherit', + 'list-style-position': r'inside|outside|inherit', + 'list-style-type': r'disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-(latin|alpha)|upper-(latin|alpha)|armenian|georgian|none|inherit', + 'margin-width': r'{length}|{percentage}|auto', + 'padding-width': r'{length}|{percentage}', + 'specific-voice': r'{ident}', + 'generic-voice': r'male|female|child', + 'content': r'{string}|{uri}|{counter}|attr\({w}{ident}{w}\)|open-quote|close-quote|no-open-quote|no-close-quote', + 'background-attrs': r'{background-color}|{background-image}|{background-repeat}|{background-attachment}|{background-position}', + 'list-attrs': r'{list-style-type}|{list-style-position}|{list-style-image}', + 'font-attrs': r'{font-style}|{font-variant}|{font-weight}', + 'text-attrs': r'underline|overline|line-through|blink', + 'overflow': r'visible|hidden|scroll|auto|inherit', +} + +""" +Define the regular expressions for validation all CSS values +""" +properties[Profiles.CSS_LEVEL_2] = { + 'azimuth': r'{angle}|(behind\s+)?(left-side|far-left|left|center-left|center|center-right|right|far-right|right-side)(\s+behind)?|behind|leftwards|rightwards|inherit', + 'background-attachment': r'{background-attachment}', + 'background-color': r'{background-color}', + 'background-image': r'{background-image}', + 'background-position': r'{background-position}', + 'background-repeat': r'{background-repeat}', + # Each piece should only be allowed one time + 'background': r'{background-attrs}(\s+{background-attrs})*|inherit', + 'border-collapse': r'collapse|separate|inherit', + 'border-spacing': r'{length}(\s+{length})?|inherit', + 'bottom': r'{length}|{percentage}|auto|inherit', + 'caption-side': r'top|bottom|inherit', + 'clear': r'none|left|right|both|inherit', + 'clip': r'{shape}|auto|inherit', + 'color': r'{color}|inherit', + 'content': r'none|normal|{content}(\s+{content})*|inherit', + 'counter-increment': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit', + 'counter-reset': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit', + 'cue-after': r'{uri}|none|inherit', + 'cue-before': r'{uri}|none|inherit', + 'cue': r'({uri}|none|inherit){1,2}|inherit', + #'cursor': r'((({uri}{w},{w})*)?(auto|crosshair|default|pointer|move|(e|ne|nw|n|se|sw|s|w)-resize|text|wait|help|progress))|inherit', + 'direction': r'ltr|rtl|inherit', + 'display': r'inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit', + 'elevation': r'{angle}|below|level|above|higher|lower|inherit', + 'empty-cells': r'show|hide|inherit', + 'float': r'left|right|none|inherit', + + # regex too slow: + # 'font-family': r'{font-family}', + 'font-family': _fontFamilyValidator, + '__FONT_FAMILY_SINGLE': r'{family-name}', + + 'font-size': r'{font-size}', + 'font-style': r'{font-style}', + 'font-variant': r'{font-variant}', + 'font-weight': r'{font-weight}', + + # regex too slow and wrong too: + # 'font': r'({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{font-family}|caption|icon|menu|message-box|small-caption|status-bar|inherit', + 'font': _fontValidator, + '__FONT_WITH_1_FAMILY': r'(({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{family-name})|caption|icon|menu|message-box|small-caption|status-bar|inherit', + + 'height': r'{length}|{percentage}|auto|inherit', + 'left': r'{length}|{percentage}|auto|inherit', + 'letter-spacing': r'normal|{length}|inherit', + 'line-height': r'{line-height}', + 'list-style-image': r'{list-style-image}', + 'list-style-position': r'{list-style-position}', + 'list-style-type': r'{list-style-type}', + 'list-style': r'{list-attrs}(\s+{list-attrs})*|inherit', + 'margin-right': r'{margin-width}|inherit', + 'margin-left': r'{margin-width}|inherit', + 'margin-top': r'{margin-width}|inherit', + 'margin-bottom': r'{margin-width}|inherit', + 'margin': r'{margin-width}(\s+{margin-width}){0,3}|inherit', + 'max-height': r'{length}|{percentage}|none|inherit', + 'max-width': r'{length}|{percentage}|none|inherit', + 'min-height': r'{length}|{percentage}|none|inherit', + 'min-width': r'{length}|{percentage}|none|inherit', + 'orphans': r'{integer}|inherit', + 'overflow': r'{overflow}', + 'padding-top': r'{padding-width}|inherit', + 'padding-right': r'{padding-width}|inherit', + 'padding-bottom': r'{padding-width}|inherit', + 'padding-left': r'{padding-width}|inherit', + 'padding': r'{padding-width}(\s+{padding-width}){0,3}|inherit', + 'page-break-after': r'auto|always|avoid|left|right|inherit', + 'page-break-before': r'auto|always|avoid|left|right|inherit', + 'page-break-inside': r'avoid|auto|inherit', + 'pause-after': r'{time}|{percentage}|inherit', + 'pause-before': r'{time}|{percentage}|inherit', + 'pause': r'({time}|{percentage}){1,2}|inherit', + 'pitch-range': r'{number}|inherit', + 'pitch': r'{frequency}|x-low|low|medium|high|x-high|inherit', + 'play-during': r'{uri}(\s+(mix|repeat))*|auto|none|inherit', + 'position': r'static|relative|absolute|fixed|inherit', + 'quotes': r'({string}\s+{string})(\s+{string}\s+{string})*|none|inherit', + 'richness': r'{number}|inherit', + 'right': r'{length}|{percentage}|auto|inherit', + 'speak-header': r'once|always|inherit', + 'speak-numeral': r'digits|continuous|inherit', + 'speak-punctuation': r'code|none|inherit', + 'speak': r'normal|none|spell-out|inherit', + 'speech-rate': r'{number}|x-slow|slow|medium|fast|x-fast|faster|slower|inherit', + 'stress': r'{number}|inherit', + 'table-layout': r'auto|fixed|inherit', + 'text-align': r'left|right|center|justify|inherit', + 'text-decoration': r'none|{text-attrs}(\s+{text-attrs})*|inherit', + 'text-indent': r'{length}|{percentage}|inherit', + 'text-transform': r'capitalize|uppercase|lowercase|none|inherit', + 'top': r'{length}|{percentage}|auto|inherit', + 'unicode-bidi': r'normal|embed|bidi-override|inherit', + 'vertical-align': r'baseline|sub|super|top|text-top|middle|bottom|text-bottom|{percentage}|{length}|inherit', + 'visibility': r'visible|hidden|collapse|inherit', + 'voice-family': r'({specific-voice}|{generic-voice}{w},{w})*({specific-voice}|{generic-voice})|inherit', + 'volume': r'{number}|{percentage}|silent|x-soft|soft|medium|loud|x-loud|inherit', + 'white-space': r'normal|pre|nowrap|pre-wrap|pre-line|inherit', + 'widows': r'{integer}|inherit', + 'width': r'{length}|{percentage}|auto|inherit', + 'word-spacing': r'normal|{length}|inherit', + 'z-index': r'auto|{integer}|inherit', +} + + +macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = { + 'border-style': 'none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset', + 'border-width': '{length}|thin|medium|thick', + 'b1': r'{border-width}?({w}{border-style})?({w}{color})?', + 'b2': r'{border-width}?({w}{color})?({w}{border-style})?', + 'b3': r'{border-style}?({w}{border-width})?({w}{color})?', + 'b4': r'{border-style}?({w}{color})?({w}{border-width})?', + 'b5': r'{color}?({w}{border-style})?({w}{border-width})?', + 'b6': r'{color}?({w}{border-width})?({w}{border-style})?', + 'border-attrs': r'{b1}|{b2}|{b3}|{b4}|{b5}|{b6}', + 'border-radius-part': '({length}|{percentage})(\s+({length}|{percentage}))?' + } +properties[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = { + 'border-color': r'({color}|transparent)(\s+({color}|transparent)){0,3}|inherit', + 'border-style': r'{border-style}(\s+{border-style}){0,3}|inherit', + 'border-top': r'{border-attrs}|inherit', + 'border-right': r'{border-attrs}|inherit', + 'border-bottom': r'{border-attrs}|inherit', + 'border-left': r'{border-attrs}|inherit', + 'border-top-color': r'{color}|transparent|inherit', + 'border-right-color': r'{color}|transparent|inherit', + 'border-bottom-color': r'{color}|transparent|inherit', + 'border-left-color': r'{color}|transparent|inherit', + 'border-top-style': r'{border-style}|inherit', + 'border-right-style': r'{border-style}|inherit', + 'border-bottom-style': r'{border-style}|inherit', + 'border-left-style': r'{border-style}|inherit', + 'border-top-width': r'{border-width}|inherit', + 'border-right-width': r'{border-width}|inherit', + 'border-bottom-width': r'{border-width}|inherit', + 'border-left-width': r'{border-width}|inherit', + 'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit', + 'border': r'{border-attrs}|inherit', + 'border-top-right-radius': '{border-radius-part}', + 'border-bottom-right-radius': '{border-radius-part}', + 'border-bottom-left-radius': '{border-radius-part}', + 'border-top-left-radius': '{border-radius-part}', + 'border-radius': '({length}{w}|{percentage}{w}){1,4}(/{w}({length}{w}|{percentage}{w}){1,4})?', + 'box-shadow': 'none|{shadow}({w},{w}{shadow})*', + } + +# CSS3 Basic User Interface Module +macros[Profiles.CSS3_BASIC_USER_INTERFACE] = { + 'border-style': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-style'], + 'border-width': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-width'], + 'outline-1': r'{outline-color}(\s+{outline-style})?(\s+{outline-width})?', + 'outline-2': r'{outline-color}(\s+{outline-width})?(\s+{outline-style})?', + 'outline-3': r'{outline-style}(\s+{outline-color})?(\s+{outline-width})?', + 'outline-4': r'{outline-style}(\s+{outline-width})?(\s+{outline-color})?', + 'outline-5': r'{outline-width}(\s+{outline-color})?(\s+{outline-style})?', + 'outline-6': r'{outline-width}(\s+{outline-style})?(\s+{outline-color})?', + 'outline-color': r'{color}|invert|inherit', + 'outline-style': r'auto|{border-style}|inherit', + 'outline-width': r'{border-width}|inherit', + } +properties[Profiles.CSS3_BASIC_USER_INTERFACE] = { + 'box-sizing': r'content-box|border-box', + 'cursor': r'((({uri}{w}({number}{w}{number}{w})?,{w})*)?(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|(e|n|ne|nw|s|se|sw|w|ew|ns|nesw|nwse|col|row)-resize|all-scroll))|inherit', + 'nav-index': r'auto|{number}|inherit', + 'outline-color': r'{outline-color}', + 'outline-style': r'{outline-style}', + 'outline-width': r'{outline-width}', + 'outline-offset': r'{length}|inherit', + #'outline': r'{outline-attrs}(\s+{outline-attrs})*|inherit', + 'outline': r'{outline-1}|{outline-2}|{outline-3}|{outline-4}|{outline-5}|{outline-6}|inherit', + 'resize': 'none|both|horizontal|vertical|inherit', + } + +# CSS Box Module Level 3 +macros[Profiles.CSS3_BOX] = { + 'overflow': macros[Profiles.CSS_LEVEL_2]['overflow'] + } +properties[Profiles.CSS3_BOX] = { + 'overflow': '{overflow}{w}{overflow}?|inherit', + 'overflow-x': '{overflow}|inherit', + 'overflow-y': '{overflow}|inherit' + } + +# CSS Color Module Level 3 +macros[Profiles.CSS3_COLOR] = { + # orange and transparent in CSS 2.1 + 'namedcolor': r'(currentcolor|transparent|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)', + # orange? + 'rgbacolor': r'rgba\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\,{w}{num}{w}\)|rgba\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)', + 'hslcolor': r'hsl\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\)|hsla\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)', + 'x11color': r'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen', + 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)', + 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{rgbacolor}|{hslcolor}|{x11color}|inherit', + } +properties[Profiles.CSS3_COLOR] = { + 'opacity': r'{num}|inherit', + } + +# CSS Fonts Module Level 3 http://www.w3.org/TR/css3-fonts/ +macros[Profiles.CSS3_FONTS] = { + #'family-name': r'{string}|{ident}', + 'family-name': r'{string}|{ident}({w}{ident})*', + 'font-face-name': 'local\({w}{family-name}{w}\)', + 'font-stretch-names': r'(ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)', + 'unicode-range': r'[uU]\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?' + } +properties[Profiles.CSS3_FONTS] = { + 'font-size-adjust': r'{number}|none|inherit', + 'font-stretch': r'normal|wider|narrower|{font-stretch-names}|inherit' + } +properties[Profiles.CSS3_FONT_FACE] = { + 'font-family': '{family-name}', + 'font-stretch': r'{font-stretch-names}', + 'font-style': r'normal|italic|oblique', + 'font-weight': r'normal|bold|[1-9]00', + 'src': r'({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name})({w},{w}({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name}))*', + 'unicode-range': '{unicode-range}({w},{w}{unicode-range})*' + } + +# CSS3 Paged Media +macros[Profiles.CSS3_PAGED_MEDIA] = { + 'page-size': 'a5|a4|a3|b5|b4|letter|legal|ledger', + 'page-orientation': 'portrait|landscape', + 'page-1': '{page-size}(?:{w}{page-orientation})?', + 'page-2': '{page-orientation}(?:{w}{page-size})?', + 'page-size-orientation': '{page-1}|{page-2}', + 'pagebreak': 'auto|always|avoid|left|right' + } +properties[Profiles.CSS3_PAGED_MEDIA] = { + 'fit': 'fill|hidden|meet|slice', + 'fit-position': r'auto|(({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?))', + 'image-orientation': 'auto|{angle}', + 'orphans': r'{integer}|inherit', + 'page': 'auto|{ident}', + 'page-break-before': '{pagebreak}|inherit', + 'page-break-after': '{pagebreak}|inherit', + 'page-break-inside': 'auto|avoid|inherit', + 'size': '({length}{w}){1,2}|auto|{page-size-orientation}', + 'widows': r'{integer}|inherit' + } + +macros[Profiles.CSS3_TEXT] = { + } +properties[Profiles.CSS3_TEXT] = { + 'text-shadow': 'none|{shadow}({w},{w}{shadow})*', + } diff --git a/libs/cssutils/sac.py b/libs/cssutils/sac.py new file mode 100755 index 00000000..d46a0b70 --- /dev/null +++ b/libs/cssutils/sac.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +"""A validating CSSParser""" +__all__ = ['CSSParser'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1754 2009-05-30 14:50:13Z cthedot $' + +import helper +import codecs +import errorhandler +import os +import tokenize2 +import urllib +import sys + + +class ErrorHandler(object): + """Basic class for CSS error handlers. + + This class class provides a default implementation ignoring warnings and + recoverable errors and throwing a SAXParseException for fatal errors. + + If a CSS application needs to implement customized error handling, it must + extend this class and then register an instance with the CSS parser + using the parser's setErrorHandler method. The parser will then report all + errors and warnings through this interface. + + The parser shall use this class instead of throwing an exception: it is + up to the application whether to throw an exception for different types of + errors and warnings. Note, however, that there is no requirement that the + parser continue to provide useful information after a call to fatalError + (in other words, a CSS driver class could catch an exception and report a + fatalError). + """ + def __init__(self): + self._log = errorhandler.ErrorHandler() + + def error(self, exception, token=None): + self._log.error(exception, token, neverraise=True) + + def fatal(self, exception, token=None): + self._log.fatal(exception, token) + + def warn(self, exception, token=None): + self._log.warn(exception, token, neverraise=True) + + +class DocumentHandler(object): + """ + void endFontFace() + Receive notification of the end of a font face statement. + void endMedia(SACMediaList media) + Receive notification of the end of a media statement. + void endPage(java.lang.String name, java.lang.String pseudo_page) + Receive notification of the end of a media statement. + void importStyle(java.lang.String uri, SACMediaList media, java.lang.String defaultNamespaceURI) + Receive notification of a import statement in the style sheet. + void startFontFace() + Receive notification of the beginning of a font face statement. + void startMedia(SACMediaList media) + Receive notification of the beginning of a media statement. + void startPage(java.lang.String name, java.lang.String pseudo_page) + Receive notification of the beginning of a page statement. + """ + def __init__(self): + def log(msg): + sys.stderr.write('INFO\t%s\n' % msg) + self._log = log + + def comment(self, text, line=None, col=None): + "Receive notification of a comment." + self._log("comment %r at [%s, %s]" % (text, line, col)) + + def startDocument(self, encoding): + "Receive notification of the beginning of a style sheet." + # source + self._log("startDocument encoding=%s" % encoding) + + def endDocument(self, source=None, line=None, col=None): + "Receive notification of the end of a document." + self._log("endDocument EOF") + + def importStyle(self, uri, media, name, line=None, col=None): + "Receive notification of a import statement in the style sheet." + # defaultNamespaceURI??? + self._log("importStyle at [%s, %s]" % (line, col)) + + def namespaceDeclaration(self, prefix, uri, line=None, col=None): + "Receive notification of an unknown rule t-rule not supported by this parser." + # prefix might be None! + self._log("namespaceDeclaration at [%s, %s]" % (line, col)) + + def startSelector(self, selectors=None, line=None, col=None): + "Receive notification of the beginning of a rule statement." + # TODO selectorList! + self._log("startSelector at [%s, %s]" % (line, col)) + + def endSelector(self, selectors=None, line=None, col=None): + "Receive notification of the end of a rule statement." + self._log("endSelector at [%s, %s]" % (line, col)) + + def property(self, name, value='TODO', important=False, line=None, col=None): + "Receive notification of a declaration." + # TODO: value is LexicalValue? + self._log("property %r at [%s, %s]" % (name, line, col)) + + def ignorableAtRule(self, atRule, line=None, col=None): + "Receive notification of an unknown rule t-rule not supported by this parser." + self._log("ignorableAtRule %r at [%s, %s]" % (atRule, line, col)) + + + +class EchoHandler(DocumentHandler): + "Echos all input to property `out`" + def __init__(self): + super(EchoHandler, self).__init__() + self._out = [] + + out = property(lambda self: u''.join(self._out)) + + def startDocument(self, encoding): + super(EchoHandler, self).startDocument(encoding) + if u'utf-8' != encoding: + self._out.append(u'@charset "%s";\n' % encoding) + +# def comment(self, text, line=None, col=None): +# self._out.append(u'/*%s*/' % text) + + def importStyle(self, uri, media, name, line=None, col=None): + "Receive notification of a import statement in the style sheet." + # defaultNamespaceURI??? + super(EchoHandler, self).importStyle(uri, media, name, line, col) + self._out.append(u'@import %s%s%s;\n' % (helper.string(uri), + u'%s ' % media if media else u'', + u'%s ' % name if name else u'') + ) + + + def namespaceDeclaration(self, prefix, uri, line=None, col=None): + super(EchoHandler, self).namespaceDeclaration(prefix, uri, line, col) + self._out.append(u'@namespace %s%s;\n' % (u'%s ' % prefix if prefix else u'', + helper.string(uri))) + + def startSelector(self, selectors=None, line=None, col=None): + super(EchoHandler, self).startSelector(selectors, line, col) + if selectors: + self._out.append(u', '.join(selectors)) + self._out.append(u' {\n') + + def endSelector(self, selectors=None, line=None, col=None): + self._out.append(u' }') + + def property(self, name, value, important=False, line=None, col=None): + super(EchoHandler, self).property(name, value, line, col) + self._out.append(u' %s: %s%s;\n' % (name, value, + u' !important' if important else u'')) + + +class Parser(object): + """ + java.lang.String getParserVersion() + Returns a string about which CSS language is supported by this parser. + boolean parsePriority(InputSource source) + Parse a CSS priority value (e.g. + LexicalUnit parsePropertyValue(InputSource source) + Parse a CSS property value. + void parseRule(InputSource source) + Parse a CSS rule. + SelectorList parseSelectors(InputSource source) + Parse a comma separated list of selectors. + void parseStyleDeclaration(InputSource source) + Parse a CSS style declaration (without '{' and '}'). + void parseStyleSheet(InputSource source) + Parse a CSS document. + void parseStyleSheet(java.lang.String uri) + Parse a CSS document from a URI. + void setConditionFactory(ConditionFactory conditionFactory) + + void setDocumentHandler(DocumentHandler handler) + Allow an application to register a document event handler. + void setErrorHandler(ErrorHandler handler) + Allow an application to register an error event handler. + void setLocale(java.util.Locale locale) + Allow an application to request a locale for errors and warnings. + void setSelectorFactory(SelectorFactory selectorFactory) + """ + def __init__(self, documentHandler=None, errorHandler=None): + self._tokenizer = tokenize2.Tokenizer() + if documentHandler: + self.setDocumentHandler(documentHandler) + else: + self.setDocumentHandler(DocumentHandler()) + + if errorHandler: + self.setErrorHandler(errorHandler) + else: + self.setErrorHandler(ErrorHandler()) + + def parseString(self, cssText, encoding=None): + if isinstance(cssText, str): + cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0] + + tokens = self._tokenizer.tokenize(cssText, fullsheet=True) + + def COMMENT(val, line, col): + self._handler.comment(val[2:-2], line, col) + + def EOF(val, line, col): + self._handler.endDocument(val, line, col) + + def simple(t): + map = {'COMMENT': COMMENT, + 'S': lambda val, line, col: None, + 'EOF': EOF} + type_, val, line, col = t + if type_ in map: + map[type_](val, line, col) + return True + else: + return False + + # START PARSING + t = tokens.next() + type_, val, line, col = t + + encoding = 'utf-8' + if 'CHARSET_SYM' == type_: + # @charset "encoding"; + # S + encodingtoken = tokens.next() + semicolontoken = tokens.next() + if 'STRING' == type_: + encoding = helper.stringvalue(val) + # ; + if 'STRING' == encodingtoken[0] and semicolontoken: + encoding = helper.stringvalue(encodingtoken[1]) + else: + self._errorHandler.fatal(u'Invalid @charset') + + t = tokens.next() + type_, val, line, col = t + + self._handler.startDocument(encoding) + + while True: + start = (line, col) + try: + if simple(t): + pass + + elif 'ATKEYWORD' == type_ or type_ in ('PAGE_SYM', 'MEDIA_SYM', 'FONT_FACE_SYM'): + atRule = [val] + braces = 0 + while True: + # read till end ; + # TODO: or {} + t = tokens.next() + type_, val, line, col = t + atRule.append(val) + if u';' == val and not braces: + break + elif u'{' == val: + braces += 1 + elif u'}' == val: + braces -= 1 + if braces == 0: + break + + self._handler.ignorableAtRule(u''.join(atRule), *start) + + elif 'IMPORT_SYM' == type_: + # import URI or STRING media? name? + uri, media, name = None, None, None + while True: + t = tokens.next() + type_, val, line, col = t + if 'STRING' == type_: + uri = helper.stringvalue(val) + elif 'URI' == type_: + uri = helper.urivalue(val) + elif u';' == val: + break + + if uri: + self._handler.importStyle(uri, media, name) + else: + self._errorHandler.error(u'Invalid @import' + u' declaration at %r' + % (start,)) + + elif 'NAMESPACE_SYM' == type_: + prefix, uri = None, None + while True: + t = tokens.next() + type_, val, line, col = t + if 'IDENT' == type_: + prefix = val + elif 'STRING' == type_: + uri = helper.stringvalue(val) + elif 'URI' == type_: + uri = helper.urivalue(val) + elif u';' == val: + break + if uri: + self._handler.namespaceDeclaration(prefix, uri, *start) + else: + self._errorHandler.error(u'Invalid @namespace' + u' declaration at %r' + % (start,)) + + else: + # CSSSTYLERULE + selector = [] + selectors = [] + while True: + # selectors[, selector]* { + if 'S' == type_: + selector.append(u' ') + elif simple(t): + pass + elif u',' == val: + selectors.append(u''.join(selector).strip()) + selector = [] + elif u'{' == val: + selectors.append(u''.join(selector).strip()) + self._handler.startSelector(selectors, *start) + break + else: + selector.append(val) + + t = tokens.next() + type_, val, line, col = t + + end = None + while True: + # name: value [!important][;name: value [!important]]*;? + name, value, important = None, [], False + + while True: + # name: + t = tokens.next() + type_, val, line, col = t + if 'S' == type_: + pass + elif simple(t): + pass + elif 'IDENT' == type_: + if name: + self._errorHandler.error('more than one property name', t) + else: + name = val + elif u':' == val: + if not name: + self._errorHandler.error('no property name', t) + break + elif u';' == val: + self._errorHandler.error('premature end of property', t) + end = val + break + elif u'}' == val: + if name: + self._errorHandler.error('premature end of property', t) + end = val + break + else: + self._errorHandler.error('unexpected property name token %r' % val, t) + + while not u';' == end and not u'}' == end: + # value !;} + t = tokens.next() + type_, val, line, col = t + + if 'S' == type_: + value.append(u' ') + elif simple(t): + pass + elif u'!' == val or u';' == val or u'}' == val: + value = ''.join(value).strip() + if not value: + self._errorHandler.error('premature end of property (no value)', t) + end = val + break + else: + value.append(val) + + while u'!' == end: + # !important + t = tokens.next() + type_, val, line, col = t + + if simple(t): + pass + elif u'IDENT' == type_ and not important: + important = True + elif u';' == val or u'}' == val: + end = val + break + else: + self._errorHandler.error('unexpected priority token %r' % val) + + if name and value: + self._handler.property(name, value, important) + + if u'}' == end: + self._handler.endSelector(selectors, line=line, col=col) + break + else: + # reset + end = None + + else: + self._handler.endSelector(selectors, line=line, col=col) + + t = tokens.next() + type_, val, line, col = t + + except StopIteration: + break + + + + def setDocumentHandler(self, handler): + "Allow an application to register a document event `handler`." + self._handler = handler + + def setErrorHandler(self, handler): + "TODO" + self._errorHandler = handler + \ No newline at end of file diff --git a/libs/cssutils/script.py b/libs/cssutils/script.py new file mode 100755 index 00000000..56c1c6e2 --- /dev/null +++ b/libs/cssutils/script.py @@ -0,0 +1,362 @@ +"""classes and functions used by cssutils scripts +""" +__all__ = ['CSSCapture', 'csscombine'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1323 2008-07-06 18:13:57Z cthedot $' + +import HTMLParser +import codecs +import cssutils +import errno +import logging +import os +import sys +import urllib2 +import urlparse + +try: + import cssutils.encutils as encutils +except ImportError: + try: + import encutils + except ImportError: + sys.exit("You need encutils from http://cthedot.de/encutils/") + +# types of sheets in HTML +LINK = 0 # +STYLE = 1 # + +class CSSCaptureHTMLParser(HTMLParser.HTMLParser): + """CSSCapture helper: Parse given data for link and style elements""" + curtag = u'' + sheets = [] # (type, [atts, cssText]) + + def _loweratts(self, atts): + return dict([(a.lower(), v.lower()) for a, v in atts]) + + def handle_starttag(self, tag, atts): + if tag == u'link': + atts = self._loweratts(atts) + if u'text/css' == atts.get(u'type', u''): + self.sheets.append((LINK, atts)) + elif tag == u'style': + # also get content of style + atts = self._loweratts(atts) + if u'text/css' == atts.get(u'type', u''): + self.sheets.append((STYLE, [atts, u''])) + self.curtag = tag + else: + # close as only intersting