This commit is contained in:
Kovid Goyal 2008-09-19 10:01:15 -07:00
parent 308b128089
commit fc934f1f66
46 changed files with 10981 additions and 9996 deletions

View File

@ -23,3 +23,8 @@ installer/windows/calibre/build.log
src/calibre/translations/.errors src/calibre/translations/.errors
src/calibre/plugins/* src/calibre/plugins/*
src/calibre/gui2/pictureflow/.build src/calibre/gui2/pictureflow/.build
src/cssutils/.svn/
src/cssutils/_todo/
src/cssutils/scripts/
src/cssutils/css/.svn/
src/cssutils/stylesheets/.svn/

View File

@ -11,6 +11,20 @@ from calibre.utils.config import Config, StringConfig
from calibre.utils.zipfile import ZipFile, ZIP_STORED from calibre.utils.zipfile import ZipFile, ZIP_STORED
from calibre.ebooks.html import config as common_config from calibre.ebooks.html import config as common_config
class DefaultProfile(object):
flow_size = sys.maxint
class PRS505(DefaultProfile):
flow_size = 300000
PROFILES = {
'PRS505' : PRS505,
'None' : DefaultProfile,
}
def initialize_container(path_to_container, opf_name='metadata.opf'): def initialize_container(path_to_container, opf_name='metadata.opf'):
''' '''
Create an empty EPUB document, with a default skeleton. Create an empty EPUB document, with a default skeleton.
@ -43,7 +57,8 @@ def config(defaults=None):
c.add_opt('output', ['-o', '--output'], default=None, c.add_opt('output', ['-o', '--output'], default=None,
help=_('The output EPUB file. If not specified, it is derived from the input file name.')) help=_('The output EPUB file. If not specified, it is derived from the input file name.'))
c.add_opt('profile', ['--profile'], default='PRS505', choices=list(PROFILES.keys()),
help=_('Profile of the target device this EPUB is meant for. Set to None to create a device independent EPUB. The profile is used for device specific restrictions on the EPUB. Choices are: ')+str(list(PROFILES.keys())))
structure = c.add_group('structure detection', _('Control auto-detection of document structure.')) structure = c.add_group('structure detection', _('Control auto-detection of document structure.'))
structure('chapter', ['--chapter'], default="//*[re:match(name(), 'h[1-2]') and re:test(., 'chapter|book|section', 'i')]", structure('chapter', ['--chapter'], default="//*[re:match(name(), 'h[1-2]') and re:test(., 'chapter|book|section', 'i')]",

View File

@ -17,7 +17,7 @@ from calibre.ebooks.epub import config as common_config
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.epub import initialize_container from calibre.ebooks.epub import initialize_container, PROFILES
class HTMLProcessor(Processor): class HTMLProcessor(Processor):
@ -38,6 +38,13 @@ class HTMLProcessor(Processor):
self.split() self.split()
def save(self):
file = Processor.save(self)
with open(file, 'rb') as f:
f.seek(0, 2)
size = f.tell()
if size > self.opts.profile.flow_size:
self.split()
def collect_font_statistics(self): def collect_font_statistics(self):
@ -87,6 +94,7 @@ def convert(htmlfile, opts, notification=None):
htmlfile = os.path.abspath(htmlfile) htmlfile = os.path.abspath(htmlfile)
if opts.output is None: if opts.output is None:
opts.output = os.path.splitext(os.path.basename(htmlfile))[0] + '.epub' opts.output = os.path.splitext(os.path.basename(htmlfile))[0] + '.epub'
opts.profile = PROFILES[opts.profile]
opts.output = os.path.abspath(opts.output) opts.output = os.path.abspath(opts.output)
if htmlfile.lower().endswith('.opf'): if htmlfile.lower().endswith('.opf'):
opf = OPFReader(htmlfile, os.path.dirname(os.path.abspath(htmlfile))) opf = OPFReader(htmlfile, os.path.dirname(os.path.abspath(htmlfile)))

View File

@ -438,7 +438,7 @@ class Processor(Parser):
style = etree.SubElement(head, 'style', attrib={'type':'text/css'}) style = etree.SubElement(head, 'style', attrib={'type':'text/css'})
style.text='\n'+self.css style.text='\n'+self.css
style.tail = '\n\n' style.tail = '\n\n'
Parser.save(self) return Parser.save(self)
def populate_toc(self, toc): def populate_toc(self, toc):
if self.level >= self.opts.max_toc_recursion: if self.level >= self.opts.max_toc_recursion:

View File

@ -1,4 +0,0 @@
from csscombine import csscombine
__all__ = ["csscapture", "csscombine", "cssparse"]

View File

@ -1,69 +0,0 @@
#!/usr/bin/env python
"""Retrieve all CSS stylesheets including embedded for a given URL.
Retrieve as StyleSheetList or save to disk - raw, parsed or minified version.
TODO:
- maybe use DOM 3 load/save?
- logger class which handles all cases when no log is given...
- saveto: why does urllib2 hang?
"""
__all__ = ['CSSCapture']
__docformat__ = 'restructuredtext'
__version__ = '$Id: csscapture.py 1332 2008-07-09 13:12:56Z cthedot $'
import logging
import optparse
import sys
from cssutils.script import CSSCapture
def main(args=None):
usage = "usage: %prog [options] URL"
parser = optparse.OptionParser(usage=usage)
parser.add_option('-d', '--debug', action='store_true', dest='debug',
help='show debug messages during capturing')
parser.add_option('-m', '--minified', action='store_true', dest='minified',
help='saves minified version of captured files')
parser.add_option('-n', '--notsave', action='store_true', dest='notsave',
help='if given files are NOT saved, only log is written')
# parser.add_option('-r', '--saveraw', action='store_true', dest='saveraw',
# help='if given saves raw css otherwise cssutils\' parsed files')
parser.add_option('-s', '--saveto', action='store', dest='saveto',
help='saving retrieved files to "saveto", defaults to "_CSSCapture_SAVED"')
parser.add_option('-u', '--useragent', action='store', dest='ua',
help='useragent to use for request of URL, default is urllib2s default')
options, url = parser.parse_args()
# TODO:
options.saveraw = False
if not url:
parser.error('no URL given')
else:
url = url[0]
if options.debug:
level = logging.DEBUG
else:
level = logging.INFO
# START
c = CSSCapture(ua=options.ua, defaultloglevel=level)
stylesheetlist = c.capture(url)
if options.notsave is None or not options.notsave:
if options.saveto:
saveto = options.saveto
else:
saveto = u'_CSSCapture_SAVED'
c.saveto(saveto, saveraw=options.saveraw, minified=options.minified)
else:
for i, s in enumerate(stylesheetlist):
print u'''%s.
encoding: %r
title: %r
href: %r''' % (i + 1, s.encoding, s.title, s.href)
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,90 +0,0 @@
#!/usr/bin/env python
"""Combine sheets referred to by @import rules in a given CSS proxy sheet
into a single new sheet.
- proxy currently is a path (no URI!)
- in @import rules only relative paths do work for now but should be used
anyway
- currently no nested @imports are resolved
- messages are send to stderr
- output to stdout.
Example::
csscombine sheets\csscombine-proxy.css -m -t ascii -s utf-8
1>combined.css 2>log.txt
results in log.txt::
COMBINING sheets/csscombine-proxy.css
USING SOURCE ENCODING: css
* PROCESSING @import sheets\csscombine-1.css
* PROCESSING @import sheets\csscombine-2.css
INFO Nested @imports are not combined: @import "1.css";
SETTING TARGET ENCODING: ascii
and combined.css::
@charset "ascii";@import"1.css";@namespaces2"uri";s2|sheet-1{top:1px}s2|sheet-2{top:2px}proxy{top:3px}
or without option -m::
@charset "ascii";
@import "1.css";
@namespace s2 "uri";
@namespace other "other";
/* proxy sheet were imported sheets should be combined */
/* non-ascii chars: \F6 \E4 \FC */
/* @import "csscombine-1.css"; */
/* combined sheet 1 */
s2|sheet-1 {
top: 1px
}
/* @import url(csscombine-2.css); */
/* combined sheet 2 */
s2|sheet-2 {
top: 2px
}
proxy {
top: 3px
}
TODO
- URL or file hrefs? URI should be default
- no nested @imports are resolved yet
- maybe add a config file which is used?
"""
__all__ = ['csscombine']
__docformat__ = 'restructuredtext'
__version__ = '$Id: csscombine.py 1332 2008-07-09 13:12:56Z cthedot $'
import optparse
import sys
from cssutils.script import csscombine
def main(args=None):
usage = "usage: %prog [options] path"
parser = optparse.OptionParser(usage=usage)
parser.add_option('-s', '--sourceencoding', action='store',
dest='sourceencoding',
help='encoding of input, defaulting to "css". If given overwrites other encoding information like @charset declarations')
parser.add_option('-t', '--targetencoding', action='store',
dest='targetencoding',
help='encoding of output, defaulting to "UTF-8"', default='utf-8')
parser.add_option('-m', '--minify', action='store_true', dest='minify',
default=False,
help='saves minified version of combined files, defaults to False')
options, path = parser.parse_args()
if not path:
parser.error('no path given')
else:
path = path[0]
print csscombine(path, options.sourceencoding, options.targetencoding,
options.minify)
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,59 +0,0 @@
#!/usr/bin/env python
"""utility script to parse given filenames or string
"""
__docformat__ = 'restructuredtext'
__version__ = '$Id: cssparse.py 1327 2008-07-08 21:17:12Z cthedot $'
import cssutils
import logging
import optparse
import sys
def main(args=None):
"""
Parses given filename(s) or string (using optional encoding) and prints
the parsed style sheet to stdout.
Redirect stdout to save CSS. Redirect stderr to save parser log infos.
"""
usage = """usage: %prog [options] filename1.css [filename2.css ...]
[>filename_combined.css] [2>parserinfo.log] """
p = optparse.OptionParser(usage=usage)
p.add_option('-e', '--encoding', action='store', dest='encoding',
help='encoding of the file')
p.add_option('-d', '--debug', action='store_true', dest='debug',
help='activate debugging output')
p.add_option('-m', '--minify', action='store_true', dest='minify',
help='minify parsed CSS', default=False)
p.add_option('-s', '--string', action='store_true', dest='string',
help='parse given string')
(options, params) = p.parse_args(args)
if not params:
p.error("no filename given")
if options.debug:
p = cssutils.CSSParser(loglevel=logging.DEBUG)
else:
p = cssutils.CSSParser()
if options.minify:
cssutils.ser.prefs.useMinified()
if options.string:
sheet = p.parseString(u''.join(params), encoding=options.encoding)
print sheet.cssText
print
sys.stderr.write('\n')
else:
for filename in params:
sys.stderr.write('=== CSS FILE: "%s" ===\n' % filename)
sheet = p.parseFile(filename, encoding=options.encoding)
print sheet.cssText
print
sys.stderr.write('\n')
if __name__ == "__main__":
sys.exit(main())

View File

@ -70,7 +70,7 @@ Usage may be::
__all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer'] __all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer']
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__author__ = 'Christof Hoeke with contributions by Walter Doerwald' __author__ = 'Christof Hoeke with contributions by Walter Doerwald'
__date__ = '$LastChangedDate:: 2008-08-11 20:11:23 +0200 #$:' __date__ = '$LastChangedDate:: 2008-08-11 11:11:23 -0700 #$:'
VERSION = '0.9.5.1' VERSION = '0.9.5.1'

View File

@ -61,7 +61,7 @@ simply called like functions.)
""" """
__all__ = ['CSS2Properties', 'cssvalues'] __all__ = ['CSS2Properties', 'cssvalues']
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__version__ = '$Id: cssproperties.py 1116 2008-03-05 13:52:23Z cthedot $' __version__ = '$Id: cssproperties.py 1469 2008-09-15 19:06:00Z cthedot $'
import re import re
@ -168,7 +168,7 @@ cssvalues = {
'border-top-width': r'{border-width}|inherit', 'border-top-width': r'{border-width}|inherit',
'border-right-width': r'{border-width}|inherit', 'border-right-width': r'{border-width}|inherit',
'border-bottom-width': r'{border-width}|inherit', 'border-bottom-width': r'{border-width}|inherit',
'border-right-width': r'{border-width}|inherit', 'border-left-width': r'{border-width}|inherit',
'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit', 'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit',
'border': r'{border-attrs}(\s+{border-attrs})*|inherit', 'border': r'{border-attrs}(\s+{border-attrs})*|inherit',
'bottom': r'{length}|{percentage}|auto|inherit', 'bottom': r'{length}|{percentage}|auto|inherit',

View File

@ -5,16 +5,18 @@
- CSSValueList implements DOM Level 2 CSS CSSValueList - CSSValueList implements DOM Level 2 CSS CSSValueList
""" """
__all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList'] __all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList', 'CSSColor']
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__version__ = '$Id: cssvalue.py 1228 2008-05-19 19:59:50Z cthedot $' __version__ = '$Id: cssvalue.py 1473 2008-09-15 21:15:54Z cthedot $'
import re import re
import xml.dom import xml.dom
import cssutils import cssutils
import cssproperties from cssutils.profiles import profiles
from cssutils.prodparser import *
class CSSValue(cssutils.util.Base):
class CSSValue(cssutils.util.Base2):
""" """
The CSSValue interface represents a simple or a complex value. The CSSValue interface represents a simple or a complex value.
A CSSValue object only occurs in a context of a CSS property A CSSValue object only occurs in a context of a CSS property
@ -73,7 +75,7 @@ class CSSValue(cssutils.util.Base):
""" """
super(CSSValue, self).__init__() super(CSSValue, self).__init__()
self.seq = [] #self.seq = []
self.valid = False self.valid = False
self.wellformed = False self.wellformed = False
self._valueValue = u'' self._valueValue = u''
@ -88,14 +90,26 @@ class CSSValue(cssutils.util.Base):
self._readonly = readonly self._readonly = readonly
def _getValue(self): def _getValue(self):
# TODO:
v = [] v = []
for x in self.seq: for item in self.seq:
if isinstance(x, cssutils.css.CSSComment): type_, val = item.type, item.value
if isinstance(val, cssutils.css.CSSComment):
# only value here
continue continue
elif isinstance(x, basestring): elif 'STRING' == type_:
v.append(x) v.append(cssutils.ser._string(val))
else: # maybe CSSPrimitiveValue elif 'URI' == type_:
v.append(x.cssText) v.append(cssutils.ser._uri(val))
elif u',' == val:
# list of items
v.append(u' ')
v.append(val)
elif isinstance(val, basestring):
v.append(val)
else:
# maybe CSSPrimitiveValue
v.append(val.cssText)
if v and u'' == v[-1].strip(): if v and u'' == v[-1].strip():
# simple strip of joined string does not work for escaped spaces # simple strip of joined string does not work for escaped spaces
del v[-1] del v[-1]
@ -158,15 +172,17 @@ class CSSValue(cssutils.util.Base):
self._checkReadonly() self._checkReadonly()
# for closures: must be a mutable # for closures: must be a mutable
new = {'values': [], new = {'rawvalues': [], # used for validation
'values': [],
'commas': 0, 'commas': 0,
'valid': True, 'valid': True,
'wellformed': True } 'wellformed': True }
def _S(expected, seq, token, tokenizer=None): def _S(expected, seq, token, tokenizer=None):
val = self._tokenvalue(token) type_, val, line, col = token
if expected.endswith('operator'): new['rawvalues'].append(u' ')
seq.append(u' ') if expected == 'operator': #expected.endswith('operator'):
seq.append(u' ', 'separator', line=line, col=col)
return 'term or operator' return 'term or operator'
elif expected.endswith('S'): elif expected.endswith('S'):
return 'term or S' return 'term or S'
@ -174,60 +190,69 @@ class CSSValue(cssutils.util.Base):
return expected return expected
def _char(expected, seq, token, tokenizer=None): def _char(expected, seq, token, tokenizer=None):
val = self._tokenvalue(token) type_, val, line, col = token
new['rawvalues'].append(val)
if 'funcend' == expected and u')' == val: if 'funcend' == expected and u')' == val:
# end of FUNCTION # end of FUNCTION
seq[-1] += val seq.appendToVal(val)
new['values'].append(seq[-1]) new['values'].append(seq[-1])
return 'operator' return 'operator'
elif expected in (')', ']', '}') and expected == val: elif expected in (')', ']', '}') and expected == val:
# end of any block: (), [], {} # end of any block: (), [], {}
seq[-1] += val seq.appendToVal(val)
return 'operator' return 'operator'
elif expected in ('funcend', ')', ']', '}'): elif expected in ('funcend', ')', ']', '}'):
# content of func or block: (), [], {} # content of func or block: (), [], {}
seq[-1] += val seq.appendToVal(val)
return expected return expected
elif expected.endswith('operator') and ',' == val: elif expected.endswith('operator') and ',' == val:
# term , term # term, term; remove all WS between terms!!!
new['commas'] += 1 new['commas'] += 1
if seq and seq[-1] == u' ': if seq and seq[-1].type == 'separator':
seq[-1] = val seq.replace(-1, val, type_, line=line, col=col)
else: else:
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'term or S' return 'term or S'
elif expected.endswith('operator') and '/' == val: elif expected.endswith('operator') and '/' == val:
# term / term # term / term
if seq and seq[-1] == u' ': if seq and seq[-1].value == u' ':
seq[-1] = val old = seq[-1]
seq.replace(-1, val, old.type, old.line, old.col)
#seq[-1] = val
else: else:
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'term or S' return 'term or S'
elif expected.startswith('term') and u'(' == val: elif expected.startswith('term') and u'(' == val:
# start of ( any* ) block # start of ( any* ) block
seq.append(val) seq.append(val, type_, line=line, col=col)
return ')' return ')'
elif expected.startswith('term') and u'[' == val: elif expected.startswith('term') and u'[' == val:
# start of [ any* ] block # start of [ any* ] block
seq.append(val) seq.append(val, type_, line=line, col=col)
return ']' return ']'
elif expected.startswith('term') and u'{' == val: elif expected.startswith('term') and u'{' == val:
# start of { any* } block # start of { any* } block
seq.append(val) seq.append(val, type_, line=line, col=col)
return '}' return '}'
elif expected.startswith('term') and u'-' == val or u'+' == 'val': elif expected.startswith('term') and u'+' == val:
# unary operator # unary operator "+"
seq.append(val) seq.append(val, type_, line=line, col=col)
new['values'].append(val)
return 'number percentage dimension'
elif expected.startswith('term') and u'-' == val:
# unary "-" operator
seq.append(val, type_, line=line, col=col)
new['values'].append(val) new['values'].append(val)
return 'number percentage dimension' return 'number percentage dimension'
elif expected.startswith('term') and u'/' == val: elif expected.startswith('term') and u'/' == val:
# font-size/line-height separator # font-size/line-height separator
seq.append(val) seq.append(val, type_, line=line, col=col)
new['values'].append(val) new['values'].append(val)
return 'number percentage dimension' return 'number percentage dimension'
else: else:
@ -237,71 +262,91 @@ class CSSValue(cssutils.util.Base):
def _number_percentage_dimension(expected, seq, token, tokenizer=None): def _number_percentage_dimension(expected, seq, token, tokenizer=None):
# NUMBER PERCENTAGE DIMENSION after -/+ or operator # NUMBER PERCENTAGE DIMENSION after -/+ or operator
type_, val, line, col = token
new['rawvalues'].append(val)
if expected.startswith('term') or expected == 'number percentage dimension': if expected.startswith('term') or expected == 'number percentage dimension':
# normal value # normal value
val = self._tokenvalue(token)
if new['values'] and new['values'][-1] in (u'-', u'+'): if new['values'] and new['values'][-1] in (u'-', u'+'):
new['values'][-1] += val new['values'][-1] += val
else: else:
new['values'].append(val) new['values'].append(val)
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'operator' return 'operator'
elif 'operator' == expected: elif 'operator' == expected:
# expected S but token which is ok # expected S but token which is ok
val = self._tokenvalue(token)
if new['values'] and new['values'][-1] in (u'-', u'+'): if new['values'] and new['values'][-1] in (u'-', u'+'):
new['values'][-1] += val new['values'][-1] += val
else: else:
new['values'].append(u' ') new['values'].append(u' ')
seq.append(u' ') seq.append(u' ', 'separator') # self._prods.S
new['values'].append(val) new['values'].append(val)
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'operator' return 'operator'
elif expected in ('funcend', ')', ']', '}'): elif expected in ('funcend', ')', ']', '}'):
# a block # a block
seq[-1] += self._tokenvalue(token) seq.appendToVal(val)
return expected return expected
else: else:
new['wellformed'] = False new['wellformed'] = False
self._log.error(u'CSSValue: Unexpected token.', token) self._log.error(u'CSSValue: Unexpected token.', token)
return expected return expected
def _string_ident_uri_hexcolor(expected, seq, token, tokenizer=None): def _string_ident_uri(expected, seq, token, tokenizer=None):
# STRING IDENT URI HASH # STRING IDENT URI
type_, val, line, col = token
new['rawvalues'].append(val)
if expected.startswith('term'): if expected.startswith('term'):
# normal value # normal value
if self._prods.STRING == type_:
# TODO: use actual values, probably needs Base2 for this val = self._stringtokenvalue(token)
typ = self._type(token) elif self._prods.URI == type_:
if self._prods.STRING == typ: val = self._uritokenvalue(token)
val = u'"%s"' % self._stringtokenvalue(token)
# elif 'URI' == typ:
# val = u'url(%s)' % self._uritokenvalue(token)
else:
val = self._tokenvalue(token)
new['values'].append(val) new['values'].append(val)
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'operator' return 'operator'
elif 'operator' == expected: elif 'operator' == expected:
# expected S but still ok # expected S but still ok
if self._prods.STRING == type_:
# TODO: use actual values, probably needs Base2 for this val = self._stringtokenvalue(token)
typ = self._type(token) elif self._prods.URI == type_:
if self._prods.STRING == typ: val = self._uritokenvalue(token)
val = u'"%s"' % self._stringtokenvalue(token)
# elif 'URI' == typ:
# val = u'url(%s)' % self._uritokenvalue(token)
else:
val = self._tokenvalue(token)
new['values'].append(u' ') new['values'].append(u' ')
new['values'].append(val) new['values'].append(val)
seq.append(u' ') seq.append(u' ', 'separator') # self._prods.S
seq.append(val) seq.append(val, type_, line=line, col=col)
return 'operator' return 'operator'
elif expected in ('funcend', ')', ']', '}'): elif expected in ('funcend', ')', ']', '}'):
# a block # a block
seq[-1] += self._tokenvalue(token) seq.appendToVal(val)
return expected
else:
new['wellformed'] = False
self._log.error(u'CSSValue: Unexpected token.', token)
return expected
def _hash(expected, seq, token, tokenizer=None):
# HASH
type_, val, line, col = token
new['rawvalues'].append(val)
val = CSSColor(cssText=token)
type_ = type(val)
if expected.startswith('term'):
# normal value
new['values'].append(val)
seq.append(val, type_, line=line, col=col)
return 'operator'
elif 'operator' == expected:
# expected S but still ok
new['values'].append(u' ')
new['values'].append(val)
seq.append(u' ', 'separator') # self._prods.S
seq.append(val, type_, line=line, col=col)
return 'operator'
elif expected in ('funcend', ')', ']', '}'):
# a block
seq.appendToVal(val)
return expected return expected
else: else:
new['wellformed'] = False new['wellformed'] = False
@ -309,19 +354,32 @@ class CSSValue(cssutils.util.Base):
return expected return expected
def _function(expected, seq, token, tokenizer=None): def _function(expected, seq, token, tokenizer=None):
# FUNCTION # FUNCTION incl colors
type_, val, line, col = token
if self._normalize(val) in ('rgb(', 'rgba(', 'hsl(', 'hsla('):
# a CSSColor
val = CSSColor(cssText=(token, tokenizer))
type_ = type(val)
seq.append(val, type_, line=line, col=col)
new['values'].append(val)
new['rawvalues'].append(val.cssText)
return 'operator'
new['rawvalues'].append(val)
if expected.startswith('term'): if expected.startswith('term'):
# normal value but add if funcend if found # normal value but add if funcend is found
seq.append(self._tokenvalue(token)) seq.append(val, type_, line=line, col=col)
return 'funcend' return 'funcend'
elif 'operator' == expected: elif 'operator' == expected:
# normal value but add if funcend if found # normal value but add if funcend is found
seq.append(u' ') seq.append(u' ', 'separator') # self._prods.S
seq.append(self._tokenvalue(token)) seq.append(val, type_, line=line, col=col)
return 'funcend' return 'funcend'
elif expected in ('funcend', ')', ']', '}'): elif expected in ('funcend', ')', ']', '}'):
# a block # a block
seq[-1] += self._tokenvalue(token) seq.appendToVal(val)
return expected return expected
else: else:
new['wellformed'] = False new['wellformed'] = False
@ -335,11 +393,9 @@ class CSSValue(cssutils.util.Base):
self._log.error(u'CSSValue: Unknown syntax or no value: %r.' % self._log.error(u'CSSValue: Unknown syntax or no value: %r.' %
self._valuestr(cssText)) self._valuestr(cssText))
else: else:
# TODO: not very efficient tokenizing twice! newseq = self._tempSeq() # []
tokenizer = self._tokenize2(cssText)
newseq = []
wellformed, expected = self._parse(expected='term', wellformed, expected = self._parse(expected='term',
seq=newseq, tokenizer=tokenizer, seq=newseq, tokenizer=tokenizer,initialtoken=linetoken,
productions={'S': _S, productions={'S': _S,
'CHAR': _char, 'CHAR': _char,
@ -347,11 +403,11 @@ class CSSValue(cssutils.util.Base):
'PERCENTAGE': _number_percentage_dimension, 'PERCENTAGE': _number_percentage_dimension,
'DIMENSION': _number_percentage_dimension, 'DIMENSION': _number_percentage_dimension,
'STRING': _string_ident_uri_hexcolor, 'STRING': _string_ident_uri,
'IDENT': _string_ident_uri_hexcolor, 'IDENT': _string_ident_uri,
'URI': _string_ident_uri_hexcolor, 'URI': _string_ident_uri,
'HASH': _string_ident_uri_hexcolor, 'HASH': _hash,
'UNICODE-RANGE': _string_ident_uri_hexcolor, #? 'UNICODE-RANGE': _string_ident_uri, #?
'FUNCTION': _function 'FUNCTION': _function
}) })
@ -359,7 +415,16 @@ class CSSValue(cssutils.util.Base):
wellformed = wellformed and new['wellformed'] wellformed = wellformed and new['wellformed']
# post conditions # post conditions
if expected.startswith('term') and newseq and newseq[-1] != u' ' or ( def lastseqvalue(seq):
"""find last actual value in seq, not COMMENT!"""
for i, item in enumerate(reversed(seq)):
if 'COMMENT' != item.type:
return len(seq)-1-i, item.value
else:
return 0, None
lastpos, lastval = lastseqvalue(newseq)
if expected.startswith('term') and lastval != u' ' or (
expected in ('funcend', ')', ']', '}')): expected in ('funcend', ')', ']', '}')):
wellformed = False wellformed = False
self._log.error(u'CSSValue: Incomplete value: %r.' % self._log.error(u'CSSValue: Incomplete value: %r.' %
@ -371,11 +436,14 @@ class CSSValue(cssutils.util.Base):
self._valuestr(cssText)) self._valuestr(cssText))
else: else:
self._linetoken = linetoken # used for line report # remove last token if 'separator'
self.seq = newseq if lastval == u' ':
self.valid = False del newseq[lastpos]
self._validate() self._linetoken = linetoken # used for line report
self._setSeq(newseq)
self.valid = self._validate(u''.join(new['rawvalues']))
if len(new['values']) == 1 and new['values'][0] == u'inherit': if len(new['values']) == 1 and new['values'][0] == u'inherit':
self._value = u'inherit' self._value = u'inherit'
@ -419,23 +487,42 @@ class CSSValue(cssutils.util.Base):
cssValueTypeString = property(_getCssValueTypeString, cssValueTypeString = property(_getCssValueTypeString,
doc="cssutils: Name of cssValueType of this CSSValue (readonly).") doc="cssutils: Name of cssValueType of this CSSValue (readonly).")
def _validate(self): def _validate(self, value=None, profile=None):
""" """
validates value against _propertyName context if given validates value against _propertyName context if given
""" """
valid = False
if self._value: if self._value:
if self._propertyName in cssproperties.cssvalues: if self._propertyName and self._propertyName in profiles.propertiesByProfile():
if cssproperties.cssvalues[self._propertyName](self._value): valid, validprofile = \
self.valid = True profiles.validateWithProfile(self._propertyName,
else: self._normalize(self._value))
self.valid = False if not validprofile:
validprofile = u''
if not valid:
self._log.warn( self._log.warn(
u'CSSValue: Invalid value for CSS2 property %r: %r' % u'CSSValue: Invalid value for %s property "%s: %s".' %
(self._propertyName, self._value), neverraise=True) (validprofile, self._propertyName,
self._value), neverraise=True)
elif profile and validprofile != profile:
self._log.warn(
u'CSSValue: Invalid value for %s property "%s: %s" but valid %s property.' %
(profile, self._propertyName, self._value,
validprofile), neverraise=True)
else:
self._log.debug(
u'CSSValue: Found valid %s property "%s: %s".' %
(validprofile, self._propertyName, self._value),
neverraise=True)
else: else:
self._log.debug( self._log.debug(u'CSSValue: Unable to validate as no or unknown property context set for value: %r'
u'CSSValue: Unable to validate as no or unknown property context set for this value: %r' % self._value, neverraise=True)
% self._value, neverraise=True)
if not value:
# if value is given this should not be saved
self.valid = valid
return valid
def _get_propertyName(self): def _get_propertyName(self):
return self.__propertyName return self.__propertyName
@ -608,70 +695,80 @@ class CSSPrimitiveValue(CSSValue):
no value is given as self._value is used no value is given as self._value is used
""" """
primitiveType = self.CSS_UNKNOWN primitiveType = self.CSS_UNKNOWN
_floatType = False # if unary expect NUMBER DIMENSION or PERCENTAGE
tokenizer = self._tokenize2(self._value)
t = self._nexttoken(tokenizer)
if not t:
self._log.error(u'CSSPrimitiveValue: No value.')
# unary operator: for item in self.seq:
if self._tokenvalue(t) in (u'-', u'+'): if item.type == self._prods.URI:
primitiveType = self.CSS_URI
break
elif item.type == self._prods.STRING:
primitiveType = self.CSS_STRING
break
else:
_floatType = False # if unary expect NUMBER DIMENSION or PERCENTAGE
tokenizer = self._tokenize2(self._value)
t = self._nexttoken(tokenizer) t = self._nexttoken(tokenizer)
if not t: if not t:
self._log.error(u'CSSPrimitiveValue: No value.') self._log.error(u'CSSPrimitiveValue: No value.')
_floatType = True # unary operator:
if self._tokenvalue(t) in (u'-', u'+'):
t = self._nexttoken(tokenizer)
if not t:
self._log.error(u'CSSPrimitiveValue: No value.')
# check for font1, "font2" etc which is treated as ONE string _floatType = True
fontstring = 0 # should be at leayst 2
expected = 'ident or string' # check for font1, "font2" etc which is treated as ONE string
tokenizer = self._tokenize2(self._value) # add used tokens again fontstring = 0 # should be at leayst 2
for token in tokenizer: expected = 'ident or string'
val, typ = self._tokenvalue(token, normalize=True), self._type(token) tokenizer = self._tokenize2(self._value) # add used tokens again
if expected == 'ident or string' and typ in ( for token in tokenizer:
self._prods.IDENT, self._prods.STRING): val, typ = self._tokenvalue(token, normalize=True), self._type(token)
expected = 'comma' if expected == 'ident or string' and typ in (
fontstring += 1 self._prods.IDENT, self._prods.STRING):
elif expected == 'comma' and val == ',': expected = 'comma'
expected = 'ident or string' fontstring += 1
fontstring += 1 elif expected == 'comma' and val == ',':
elif typ in (self._prods.S, self._prods.COMMENT): expected = 'ident or string'
continue fontstring += 1
elif typ in ('separator', self._prods.S, self._prods.COMMENT):
continue
else:
fontstring = False
break
if fontstring > 2:
# special case: e.g. for font-family: a, b; only COMMA IDENT and STRING
primitiveType = CSSPrimitiveValue.CSS_STRING
elif self._type(t) == self._prods.HASH:
# special case, maybe should be converted to rgb in any case?
primitiveType = CSSPrimitiveValue.CSS_RGBCOLOR
else: else:
fontstring = False for i, (name, tokentype, search) in enumerate(self._unitinfos):
break val, typ = self._tokenvalue(t, normalize=True), self._type(t)
if typ == tokentype:
if typ == self._prods.DIMENSION:
if not search:
primitiveType = i
break
elif re.match(ur'^[^a-z]*(%s)$' % search, val):
primitiveType = i
break
elif typ == self._prods.FUNCTION:
if not search:
primitiveType = i
break
elif val.startswith(search):
primitiveType = i
break
else:
primitiveType = i
break
if fontstring > 2: if _floatType and primitiveType not in self._floattypes:
# special case: e.g. for font-family: a, b; only COMMA IDENT and STRING # - or + only expected before floattype
primitiveType = CSSPrimitiveValue.CSS_STRING primitiveType = self.CSS_UNKNOWN
elif self._type(t) == self._prods.HASH:
# special case, maybe should be converted to rgb in any case?
primitiveType = CSSPrimitiveValue.CSS_RGBCOLOR
else:
for i, (name, tokentype, search) in enumerate(self._unitinfos):
val, typ = self._tokenvalue(t, normalize=True), self._type(t)
if typ == tokentype:
if typ == self._prods.DIMENSION:
if not search:
primitiveType = i
break
elif re.match(ur'^[^a-z]*(%s)$' % search, val):
primitiveType = i
break
elif typ == self._prods.FUNCTION:
if not search:
primitiveType = i
break
elif val.startswith(search):
primitiveType = i
break
else:
primitiveType = i
break
if _floatType and primitiveType not in self._floattypes:
# - or + only expected before floattype
primitiveType = self.CSS_UNKNOWN
self._primitiveType = primitiveType self._primitiveType = primitiveType
@ -711,7 +808,7 @@ class CSSPrimitiveValue(CSSValue):
return val, dim return val, dim
def getFloatValue(self, unitType): def getFloatValue(self, unitType=None):
""" """
(DOM method) This method is used to get a float value in a (DOM method) This method is used to get a float value in a
specified unit. If this CSS value doesn't contain a float value specified unit. If this CSS value doesn't contain a float value
@ -722,20 +819,22 @@ class CSSPrimitiveValue(CSSValue):
to get the float value. The unit code can only be a float unit type 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, (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_MM, CSS_IN, CSS_PT, CSS_PC, CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS,
CSS_S, CSS_HZ, CSS_KHZ, CSS_DIMENSION). 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 int 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`` e.g. if the value is ``1px`` it return ``1`` and **not** ``1.0``
conversions might return strange values like 1.000000000001 conversions might return strange values like 1.000000000001
""" """
if unitType not in self._floattypes: if unitType is not None and unitType not in self._floattypes:
raise xml.dom.InvalidAccessErr( raise xml.dom.InvalidAccessErr(
u'unitType Parameter is not a float type') u'unitType Parameter is not a float type')
val, dim = self.__getValDim() val, dim = self.__getValDim()
if self.primitiveType != unitType: if unitType is not None and self.primitiveType != unitType:
# convert if needed
try: try:
val = self._converter[self.primitiveType, unitType](val) val = self._converter[self.primitiveType, unitType](val)
except KeyError: except KeyError:
@ -937,6 +1036,17 @@ class CSSPrimitiveValue(CSSValue):
# TODO: use Rect class # TODO: use Rect class
raise NotImplementedError() raise NotImplementedError()
def _getCssText(self):
"""overwritten from CSSValue"""
return cssutils.ser.do_css_CSSPrimitiveValue(self)
def _setCssText(self, cssText):
"""use CSSValue's implementation"""
return super(CSSPrimitiveValue, self)._setCssText(cssText)
cssText = property(_getCssText, _setCssText,
doc="A string representation of the current value.")
def __str__(self): def __str__(self):
return "<cssutils.css.%s object primitiveType=%s cssText=%r _propertyName=%r valid=%r at 0x%x>" % ( return "<cssutils.css.%s object primitiveType=%s cssText=%r _propertyName=%r valid=%r at 0x%x>" % (
self.__class__.__name__, self.primitiveTypeString, self.__class__.__name__, self.primitiveTypeString,
@ -972,24 +1082,24 @@ class CSSValueList(CSSValue):
ivalueseq, valueseq = 0, self._SHORTHANDPROPERTIES.get( ivalueseq, valueseq = 0, self._SHORTHANDPROPERTIES.get(
self._propertyName, []) self._propertyName, [])
self._items = [] self._items = []
newseq = [] newseq = self._tempSeq(False)
i, max = 0, len(self.seq) i, max = 0, len(self.seq)
minus = None minus = None
while i < max: while i < max:
v = self.seq[i] item = self.seq[i]
type_, val, line, col = item.type, item.value, item.line, item.col
if u'-' == v: if u'-' == val:
if minus: # 2 "-" after another if minus: # 2 "-" after another
self._log.error( self._log.error( # TODO:
u'CSSValueList: Unknown syntax: %r.' u'CSSValueList: Unknown syntax: %r.'
% u''.join(self.seq)) % u''.join(self.seq))
else: else:
minus = v minus = val
elif isinstance(v, basestring) and not v.strip() == u'' and\ elif isinstance(val, basestring) and not type_ == 'separator' and\
not u'/' == v: not u'/' == val:
if minus: if minus:
v = minus + v val = minus + val
minus = None minus = None
# TODO: complete # TODO: complete
# if shorthand get new propname # if shorthand get new propname
@ -1007,55 +1117,70 @@ class CSSValueList(CSSValue):
if propname in self._SHORTHANDPROPERTIES: if propname in self._SHORTHANDPROPERTIES:
propname = None propname = None
if i+1 < max and self.seq[i+1] == u',': if i+1 < max and self.seq[i+1].value == u',':
# a comma separated list of values as ONE value # a comma separated list of values as ONE value
# e.g. font-family: a,b # e.g. font-family: a,b
fullvalue = [v] # CSSValue already has removed extra S tokens!
fullvalue = [val]
expected = 'comma' # or 'value' expected = 'comma' # or 'value'
for j in range(i+1, max): for j in range(i+1, max):
testv = self.seq[j] item2 = self.seq[j]
if u' ' == testv: # a single value follows typ2, val2, line2, col2 = (item2.type, item2.value,
item2.line, item2.col)
if u' ' == val2:
# end or a single value follows
break break
elif testv in ('-', '+') and expected == 'value': elif 'value' == expected and val2 in u'-+':
# unary modifier # unary modifier
fullvalue.append(testv) fullvalue.append(val2)
expected = 'value' expected = 'value'
elif u',' == testv and expected == 'comma': elif 'comma' == expected and u',' == val2:
fullvalue.append(testv) fullvalue.append(val2)
expected = 'value' expected = 'value'
elif u',' != testv and expected == 'value': elif 'value' == expected and u',' != val2:
fullvalue.append(testv) if 'STRING' == typ2:
val2 = cssutils.ser._string(val2)
fullvalue.append(val2)
expected = 'comma' expected = 'comma'
else: else:
self._log.error( self._log.error(
u'CSSValueList: Unknown syntax: %r.' u'CSSValueList: Unknown syntax: %r.'
% testv) % val2)
return return
if expected == 'value': if expected == 'value':
self._log.error( self._log.error( # TODO:
u'CSSValueList: Unknown syntax: %r.' u'CSSValueList: Unknown syntax: %r.'
% u''.join(self.seq)) % u''.join(self.seq))
return return
# setting _propertyName this way does not work # setting _propertyName this way does not work
# for compound props like font! # for compound props like font!
i += len(fullvalue) - 1 i += len(fullvalue) - 1
o = CSSValue(cssText=u''.join(fullvalue), obj = CSSValue(cssText=u''.join(fullvalue),
_propertyName=propname) _propertyName=propname)
else: else:
# a single value, u' ' or nothing should be following # a single value, u' ' or nothing should be following
o = CSSValue(cssText=v, _propertyName=propname) if 'STRING' == type_:
val = cssutils.ser._string(val)
elif 'URI' == type_:
val = cssutils.ser._uri(val)
self._items.append(o) obj = CSSValue(cssText=val, _propertyName=propname)
newseq.append(o)
self._items.append(obj)
newseq.append(obj, CSSValue)
elif CSSColor == type_:
self._items.append(val)
newseq.append(val, CSSColor)
else: else:
# S (or TODO: comment?) # S (or TODO: comment?)
newseq.append(v) newseq.append(val, type_)
i += 1 i += 1
self.seq = newseq self._setSeq(newseq)
length = property(lambda self: len(self._items), length = property(lambda self: len(self._items),
doc="(DOM attribute) The number of CSSValues in the list.") doc="(DOM attribute) The number of CSSValues in the list.")
@ -1081,6 +1206,174 @@ class CSSValueList(CSSValue):
for i in range (0, self.length): for i in range (0, self.length):
yield self.item(i) yield self.item(i)
def __str_(self): def __str__(self):
return "<cssutils.css.%s object length=%s at 0x%x>" % ( return "<cssutils.css.%s object cssValueType=%r cssText=%r length=%r propname=%r valid=%r at 0x%x>" % (
self.__class__.__name__, self.length, id(self)) self.__class__.__name__, self.cssValueTypeString,
self.cssText, self.length, self._propertyName,
self.valid, id(self))
class CSSFunction(CSSPrimitiveValue):
"""A CSS function value like rect() etc."""
def __init__(self, cssText=None, readonly=False):
"""
Init a new CSSFunction
cssText
the parsable cssText of the value
readonly
defaults to False
"""
super(CSSColor, self).__init__()
self.valid = False
self.wellformed = False
if cssText is not None:
self.cssText = cssText
self._funcType = None
self._readonly = readonly
def _setCssText(self, cssText):
self._checkReadonly()
if False:
pass
else:
types = self._prods # rename!
valueProd = Prod(name='value',
match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE),
toSeq=CSSPrimitiveValue,
toStore='parts'
)
# COLOR PRODUCTION
funcProds = Sequence([
Prod(name='FUNC',
match=lambda t, v: t == types.FUNCTION,
toStore='funcType' ),
Prod(**PreDef.sign),
valueProd,
# more values starting with Comma
# should use store where colorType is saved to
# define min and may, closure?
Sequence([Prod(**PreDef.comma),
Prod(**PreDef.sign),
valueProd],
minmax=lambda: (2, 2)),
Prod(**PreDef.funcEnd)
])
# store: colorType, parts
wellformed, seq, store, unusedtokens = ProdsParser().parse(cssText,
u'CSSFunction',
funcProds,
{'parts': []})
if wellformed:
self.wellformed = True
self._setSeq(seq)
self._funcType = self._normalize(store['colorType'].value[:-1])
cssText = property(lambda self: cssutils.ser.do_css_CSSColor(self),
_setCssText)
funcType = property(lambda self: self._funcType)
def __repr__(self):
return "cssutils.css.%s(%r)" % (self.__class__.__name__, self.cssText)
def __str__(self):
return "<cssutils.css.%s object colorType=%r cssText=%r at 0x%x>" % (
self.__class__.__name__, self.colorType, self.cssText,
id(self))
class CSSColor(CSSPrimitiveValue):
"""A CSS color like RGB, RGBA or a simple value like `#000` or `red`."""
def __init__(self, cssText=None, readonly=False):
"""
Init a new CSSColor
cssText
the parsable cssText of the value
readonly
defaults to False
"""
super(CSSColor, self).__init__()
self._colorType = None
self.valid = False
self.wellformed = False
if cssText is not None:
self.cssText = cssText
self._readonly = readonly
def _setCssText(self, cssText):
self._checkReadonly()
if False:
pass
else:
types = self._prods # rename!
valueProd = Prod(name='value',
match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE),
toSeq=CSSPrimitiveValue,
toStore='parts'
)
# COLOR PRODUCTION
funccolor = Sequence([Prod(name='FUNC',
match=lambda t, v: self._normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla(') and t == types.FUNCTION,
toSeq=lambda v: self._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,
Prod(name='HEX color',
match=lambda t, v: t == types.HASH and
len(v) == 4 or len(v) == 7,
toStore='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'CSSColor',
colorprods,
{'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 = self._normalize(store['colorType'].value)[:-1]
self._setSeq(seq)
cssText = property(lambda self: cssutils.ser.do_css_CSSColor(self),
_setCssText)
colorType = property(lambda self: self._colorType)
def __repr__(self):
return "cssutils.css.%s(%r)" % (self.__class__.__name__, self.cssText)
def __str__(self):
return "<cssutils.css.%s object colorType=%r cssText=%r at 0x%x>" % (
self.__class__.__name__, self.colorType, self.cssText,
id(self))

View File

@ -4,11 +4,12 @@ Internal use only, may be removed in the future!
""" """
__all__ = ['Property'] __all__ = ['Property']
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__version__ = '$Id: property.py 1305 2008-06-22 18:42:51Z cthedot $' __version__ = '$Id: property.py 1444 2008-08-31 18:45:35Z cthedot $'
import xml.dom import xml.dom
import cssutils import cssutils
import cssproperties #import cssproperties
from cssutils.profiles import profiles
from cssvalue import CSSValue from cssvalue import CSSValue
from cssutils.helper import Deprecated from cssutils.helper import Deprecated
@ -236,10 +237,10 @@ class Property(cssutils.util.Base):
self.seqs[0] = newseq self.seqs[0] = newseq
# validate # validate
if self._name not in cssproperties.cssvalues: if self._name not in profiles.propertiesByProfile():
self.valid = False self.valid = False
tokenizer=self._tokenize2(name) tokenizer=self._tokenize2(name)
self._log.info(u'Property: No CSS2 Property: %r.' % self._log.warn(u'Property: Unknown Property: %r.' %
new['literalname'], token=token, neverraise=True) new['literalname'], token=token, neverraise=True)
else: else:
self.valid = True self.valid = True

402
src/cssutils/prodparser.py Normal file
View File

@ -0,0 +1,402 @@
# -*- 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 $'
import cssutils
class ParseError(Exception):
"""Base Exception class for ProdParser (used internally)."""
pass
class Exhausted(ParseError):
"""Raised if Sequence or Choice is done."""
pass
class NoMatch(ParseError):
"""Raised if Sequence or Choice do not match."""
pass
class MissingToken(ParseError):
"""Raised if Sequence or Choice are not exhausted."""
pass
class Choice(object):
"""A Choice of productions (Sequence or single Prod)."""
def __init__(self, prods):
"""
prods
Prod or Sequence objects
"""
self._prods = prods
self._exhausted = False
def nextProd(self, token):
"""
Return:
- next matching Prod or Sequence
- raises ParseError if nothing matches
- raises Exhausted if choice already done
``token`` may be None but this occurs when no tokens left."""
if not self._exhausted:
for x in self._prods:
if isinstance(x, Prod):
test = x
else:
# nested Sequence matches if 1st prod matches
test = x.first()
try:
if test.matches(token):
self._exhausted = True
return x
except ParseError, e:
# do not raise if other my match
continue
else:
# None matched
raise ParseError(u'No match in choice')
else:
raise Exhausted(u'Extra token')
class Sequence(object):
"""A Sequence of productions (Choice or single Prod)."""
def __init__(self, prods, minmax=None):
"""
prods
Prod or Sequence objects
minmax = lambda: (1, 1)
callback returning number of times this sequence may run
"""
self._prods = prods
if not minmax:
minmax = lambda: (1, 1)
self._min, self._max = minmax()
self._number = len(self._prods)
self._round = 1 # 1 based!
self._pos = 0
def first(self):
"""Return 1st element of Sequence, used by Choice"""
# TODO: current impl first only if 1st if an prod!
for prod in self._prods:
if not prod.optional:
return prod
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._pos:]:
if not prod.optional:
return prod.name
else:
return 'Unknown'
name = property(_currentName, doc='Used for Error reporting')
def nextProd(self, token):
"""Return
- next matching Prod or Choice
- raises ParseError if nothing matches
- raises Exhausted if sequence already done
"""
while self._pos < self._number:
x = self._prods[self._pos]
thisround = self._round
self._pos += 1
if self._pos == self._number:
if self._round < self._max:
# new round?
self._pos = 0
self._round += 1
if isinstance(x, Prod):
if not token and (x.optional or thisround > self._min):
# token is None if nothing expected
raise Exhausted()
elif not token and not x.optional:
raise MissingToken(u'Missing token for production %s'
% x.name)
elif x.matches(token):
return x
elif x.optional:
# try next
continue
# elif thisround > self._min:
# # minimum done
# self._round = self._max
# self._pos = self._number
# return None
else:
# should have matched
raise NoMatch(u'No matching production for token')
else:
# nested Sequence or Choice
return x
# Sequence is exhausted
if self._round >= self._max:
raise Exhausted(u'Extra token')
class Prod(object):
"""Single Prod in Sequence or Choice."""
def __init__(self, name, match, toSeq=None, toStore=None,
optional=False):
"""
name
name used for error reporting
match callback
function called with parameters tokentype and tokenvalue
returning True, False or raising ParseError
toSeq callback (optional)
if given calling toSeq(token) will be appended to seq
else simply seq
toStore (optional)
key to save util.Item to store or callback(store, util.Item)
optional = False
wether Prod is optional or not
"""
self.name = name
self.match = match
self.optional=optional
def makeToStore(key):
"Return a function used by toStore."
def toStore(store, item):
"Set or append store item."
if key in store:
store[key].append(item)
else:
store[key] = item
return toStore
if toSeq:
# called: seq.append(toSeq(value))
self.toSeq = toSeq
else:
self.toSeq = lambda val: val
if callable(toStore):
self.toStore = toStore
elif toStore:
self.toStore = makeToStore(toStore)
else:
# always set!
self.toStore = None
def matches(self, token):
"""Return if token matches."""
type_, val, line, col = token
return self.match(type_, val)
def __repr__(self):
return "<cssutils.prodsparser.%s object name=%r at 0x%x>" % (
self.__class__.__name__, self.name, id(self))
class ProdParser(object):
"""Productions parser."""
def __init__(self):
self.types = cssutils.cssproductions.CSSProductions
self._log = cssutils.log
self._tokenizer = cssutils.tokenize2.Tokenizer()
def parse(self, text, name, productions, 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
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
"""
if isinstance(text, basestring):
# to tokenize
tokens = self._tokenizer.tokenize(text)
elif isinstance(text, tuple):
# (token, tokens) or a single token
if len(text) == 2:
# (token, tokens)
def gen(token, tokens):
"new generator appending token and tokens"
yield token
for t in tokens:
yield t
tokens = (t for t in gen(*text))
else:
# single token
tokens = [text]
else:
# already tokenized, assume generator
tokens = text
# a new seq to append all Items to
seq = cssutils.util.Seq(readonly=False)
# store for specific values
if not store:
store = {}
# store['_raw'] = []
# stack of productions
prods = [productions]
wellformed = True
for token in tokens:
type_, val, line, col = token
# store['_raw'].append(val)
# default productions
if type_ == self.types.S:
# always append S?
seq.append(val, type_, line, col)
elif type_ == self.types.COMMENT:
# always append COMMENT
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.EOF:
# do nothing
pass
# next = 'EOF'
else:
# check prods
try:
while True:
# find next matching production
try:
prod = prods[-1].nextProd(token)
except (NoMatch, Exhausted), e:
# try next
prod = None
if isinstance(prod, Prod):
break
elif not prod:
if len(prods) > 1:
# nested exhausted, next in parent
prods.pop()
else:
raise Exhausted('Extra token')
else:
# nested Sequence, Choice
prods.append(prod)
except ParseError, e:
wellformed = False
self._log.error(u'%s: %s: %r' % (name, e, token))
else:
# process prod
if prod.toSeq:
seq.append(prod.toSeq(val), type_, line, col)
else:
seq.append(val, type_, line, col)
if prod.toStore:
prod.toStore(store, seq[-1])
# if 'STOP' == next: # EOF?
# # stop here and ignore following tokens
# break
while True:
# all productions exhausted?
try:
prod = prods[-1].nextProd(token=None)
except Exhausted, e:
prod = None # ok
except (MissingToken, NoMatch), e:
wellformed = False
self._log.error(u'%s: %s'
% (name, e))
else:
try:
if prod.optional:
# ignore optional ones
continue
except AttributeError:
pass
if prod:
wellformed = False
self._log.error(u'%s: Missing token for production %r'
% (name, prod.name))
elif len(prods) > 1:
# nested exhausted, next in parent
prods.pop()
else:
break
# bool, Seq, None or generator
return wellformed, seq, store, tokens
class PreDef(object):
"""Predefined Prod definition for use in productions definition
for ProdParser instances.
"""
@staticmethod
def comma():
","
return Prod(name=u'comma', match=lambda t, v: v == u',')
@staticmethod
def funcEnd():
")"
return Prod(name=u'end FUNC ")"', match=lambda t, v: v == u')')
@staticmethod
def unary():
"+ or -"
return Prod(name=u'unary +-', match=lambda t, v: v in u'+-',
optional=True)

367
src/cssutils/profiles.py Normal file
View File

@ -0,0 +1,367 @@
"""CSS profiles. Predefined are:
- 'CSS level 2'
"""
__all__ = ['profiles']
__docformat__ = 'restructuredtext'
__version__ = '$Id: cssproperties.py 1116 2008-03-05 13:52:23Z cthedot $'
import cssutils
import re
"""
Define some regular expression fragments that will be used as
macros within the CSS property value regular expressions.
"""
css2macros = {
'border-style': 'none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset',
'border-color': '{color}',
'border-width': '{length}|thin|medium|thick',
'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-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}{identifier}{w}(?:,{w}{list-style-type}{w})?\)',
'identifier': r'{ident}',
'family-name': r'{string}|{identifier}',
'generic-family': r'serif|sans-serif|cursive|fantasy|monospace',
'absolute-size': r'(x?x-)?(small|large)|medium',
'relative-size': r'smaller|larger',
'font-family': r'(({family-name}|{generic-family}){w},{w})*({family-name}|{generic-family})|inherit',
'font-size': r'{absolute-size}|{relative-size}|{length}|{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',
'outline-color': r'{color}|invert|inherit',
'outline-style': r'{border-style}|inherit',
'outline-width': r'{border-width}|inherit',
'padding-width': r'{length}|{percentage}',
'specific-voice': r'{identifier}',
'generic-voice': r'male|female|child',
'content': r'{string}|{uri}|{counter}|attr\({w}{identifier}{w}\)|open-quote|close-quote|no-open-quote|no-close-quote',
'border-attrs': r'{border-width}|{border-style}|{border-color}',
'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}',
'outline-attrs': r'{outline-color}|{outline-style}|{outline-width}',
'text-attrs': r'underline|overline|line-through|blink',
}
"""
Define the regular expressions for validation all CSS values
"""
css2 = {
'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-color': r'({border-color}|transparent)(\s+({border-color}|transparent)){0,3}|inherit',
'border-spacing': r'{length}(\s+{length})?|inherit',
'border-style': r'{border-style}(\s+{border-style}){0,3}|inherit',
'border-top': r'{border-attrs}(\s+{border-attrs})*|inherit',
'border-right': r'{border-attrs}(\s+{border-attrs})*|inherit',
'border-bottom': r'{border-attrs}(\s+{border-attrs})*|inherit',
'border-left': r'{border-attrs}(\s+{border-attrs})*|inherit',
'border-top-color': r'{border-color}|transparent|inherit',
'border-right-color': r'{border-color}|transparent|inherit',
'border-bottom-color': r'{border-color}|transparent|inherit',
'border-left-color': r'{border-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}(\s+{border-attrs})*|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'normal|{content}(\s+{content})*|inherit',
'counter-increment': r'({identifier}(\s+{integer})?)(\s+({identifier}(\s+{integer})))*|none|inherit',
'counter-reset': r'({identifier}(\s+{integer})?)(\s+({identifier}(\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',
'font-family': r'{font-family}',
'font-size': r'{font-size}',
'font-style': r'{font-style}',
'font-variant': r'{font-variant}',
'font-weight': r'{font-weight}',
'font': r'({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{font-family}|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',
'outline-color': r'{outline-color}',
'outline-style': r'{outline-style}',
'outline-width': r'{outline-width}',
'outline': r'{outline-attrs}(\s+{outline-attrs})*|inherit',
'overflow': r'visible|hidden|scroll|auto|inherit',
'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',
}
# CSS Color Module Level 3
css3colormacros = {
# orange and transparent in CSS 2.1
'namedcolor': r'(currentcolor|transparent|orange|black|green|silver|lime|gray|olive|white|yellow|maroon|navy|red|blue|purple|teal|fuchsia|aqua)',
# orange?
'rgbacolor': r'rgba\({w}{int}{w},{w}{int}{w},{w}{int}{w},{w}{int}{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}\)',
}
css3color = {
'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{rgbacolor}|{hslcolor}|inherit',
'opacity': r'{num}|inherit'
}
class NoSuchProfileException(Exception):
"""Raised if no profile with given name is found"""
pass
class Profiles(object):
"""
A dictionary of::
profilename: {
propname: propvalue_regex*
}
Predefined profiles are:
- 'CSS level 2': Properties defined by CSS2
"""
basicmacros = {
'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+',
'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*',
}
generalmacros = {
'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)',
'angle': r'0|{num}(deg|grad|rad)',
'time': r'0|{num}m?s',
'frequency': r'0|{num}k?Hz',
'percentage': r'{num}%',
}
CSS_LEVEL_2 = 'CSS Level 2.1'
CSS_COLOR_LEVEL_3 = 'CSS Color Module Level 3'
def __init__(self):
self._log = cssutils.log
self._profilenames = [] # to keep order, REFACTOR!
self._profiles = {}
self.addProfile(self.CSS_LEVEL_2, css2, css2macros)
self.addProfile(self.CSS_COLOR_LEVEL_3, css3color, css3colormacros)
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 callable(value):
while re.search(r'{[a-z][a-z0-9-]*}', value):
value = re.sub(r'{(?P<macro>[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():
if not callable(value):
value = re.compile('^(?:%s)$' % value, re.I).match
dictionary[key] = value
return dictionary
profiles = property(lambda self: sorted(self._profiles.keys()),
doc=u'Names of all profiles.')
def addProfile(self, profile, properties, macros=None):
"""Add a new profile with name ``profile`` (e.g. 'CSS level 2')
and the given ``properties``. ``macros`` are
``profile``
The new profile's name
``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.
"""
if not macros:
macros = {}
m = self.basicmacros
m.update(self.generalmacros)
m.update(macros)
properties = self._expand_macros(properties, m)
self._profilenames.append(profile)
self._profiles[profile] = self._compile_regexes(properties)
def propertiesByProfile(self, profiles=None):
"""Generator: Yield property names, if no profile(s) is given all
profile's properties are used."""
if not profiles:
profiles = self.profiles
elif isinstance(profiles, basestring):
profiles = (profiles, )
try:
for profile in sorted(profiles):
for name in sorted(self._profiles[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."""
for profile in self.profiles:
if name in self._profiles[profile]:
try:
# custom validation errors are caught
r = bool(self._profiles[profile][name](value))
except Exception, e:
self._log.error(e, error=Exception)
return False
if r:
return r
return False
def validateWithProfile(self, name, value):
"""Check if value is valid for given property name returning
(valid, valid_in_profile).
You may want to check if valid_in_profile is what you expected.
Example: You might expect a valid Profiles.CSS_LEVEL_2 value but
e.g. ``validateWithProfile('color', 'rgba(1,1,1,1)')`` returns
(True, Profiles.CSS_COLOR_LEVEL_3)
"""
for profilename in self._profilenames:
if name in self._profiles[profilename]:
try:
# custom validation errors are caught
r = (bool(self._profiles[profilename][name](value)),
profilename)
except Exception, e:
self._log.error(e, error=Exception)
r = False, None
if r[0]:
return r
return False, None
profiles = Profiles()

View File

@ -5,9 +5,10 @@
""" """
__all__ = ['CSSSerializer', 'Preferences'] __all__ = ['CSSSerializer', 'Preferences']
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__version__ = '$Id: serialize.py 1419 2008-08-09 19:28:06Z cthedot $' __version__ = '$Id: serialize.py 1472 2008-09-15 21:14:54Z cthedot $'
import codecs import codecs
import re import re
import xml.dom
import cssutils import cssutils
def _escapecss(e): def _escapecss(e):
@ -193,17 +194,24 @@ class Out(object):
- some other vals - some other vals
add ``*spacer`` except ``space=False`` add ``*spacer`` except ``space=False``
""" """
if val or 'STRING' == typ: if val or typ in ('STRING', 'URI'):
# PRE # PRE
if 'COMMENT' == typ: if 'COMMENT' == typ:
if self.ser.prefs.keepComments: if self.ser.prefs.keepComments:
val = val.cssText val = val.cssText
else: else:
return return
elif typ in ('Property', cssutils.css.CSSRule.UNKNOWN_RULE): elif hasattr(val, 'cssText'):
val = val.cssText val = val.cssText
# elif typ in ('Property', cssutils.css.CSSRule.UNKNOWN_RULE):
# val = val.cssText
elif 'S' == typ and not keepS: elif 'S' == typ and not keepS:
return return
elif typ in ('NUMBER', 'DIMENSION', 'PERCENTAGE') and val == '0':
# remove sign + or - if value is zero
# TODO: only for lenghts!
if self.out and self.out[-1] in u'+-':
del self.out[-1]
elif 'STRING' == typ: elif 'STRING' == typ:
# may be empty but MUST not be None # may be empty but MUST not be None
if val is None: if val is None:
@ -211,7 +219,7 @@ class Out(object):
val = self.ser._string(val) val = self.ser._string(val)
elif 'URI' == typ: elif 'URI' == typ:
val = self.ser._uri(val) val = self.ser._uri(val)
elif val in u'+>~,:{;)]': elif val in u'+>~,:{;)]/':
self._remove_last_if_S() self._remove_last_if_S()
# APPEND # APPEND
@ -235,12 +243,13 @@ class Out(object):
self.out.append(self.ser.prefs.lineSeparator) self.out.append(self.ser.prefs.lineSeparator)
elif u';' == val: # end or prop or block elif u';' == val: # end or prop or block
self.out.append(self.ser.prefs.lineSeparator) self.out.append(self.ser.prefs.lineSeparator)
elif val not in u'}[]()' and space: elif val not in u'}[]()/' and typ not in ('FUNCTION',) and space:
self.out.append(self.ser.prefs.spacer) self.out.append(self.ser.prefs.spacer)
def value(self, delim=u'', end=None): def value(self, delim=u'', end=None, keepS=False):
"returns all items joined by delim" "returns all items joined by delim"
self._remove_last_if_S() if not keepS:
self._remove_last_if_S()
if end: if end:
self.out.append(end) self.out.append(end)
return delim.join(self.out) return delim.join(self.out)
@ -253,8 +262,9 @@ class CSSSerializer(object):
To use your own serializing method the easiest is to subclass CSS To use your own serializing method the easiest is to subclass CSS
Serializer and overwrite the methods you like to customize. Serializer and overwrite the methods you like to customize.
""" """
# chars not in URI without quotes around # chars not in URI without quotes around as problematic with other stuff
__forbidden_in_uri_matcher = re.compile(ur'''.*?[\)\s\;]''', re.U).match # really ","?
__forbidden_in_uri_matcher = re.compile(ur'''.*?[\(\)\s\;,]''', re.U).match
def __init__(self, prefs=None): def __init__(self, prefs=None):
""" """
@ -825,30 +835,100 @@ class CSSSerializer(object):
return u''.join(out).strip() return u''.join(out).strip()
def do_css_CSSValue(self, cssvalue): def do_css_CSSValue(self, cssvalue):
""" """Serializes a CSSValue"""
serializes a CSSValue
"""
# TODO: use Out()
# TODO: use self._valid(cssvalue)? # TODO: use self._valid(cssvalue)?
if not cssvalue: if not cssvalue:
return u'' return u''
else: else:
sep = u',%s' % self.prefs.listItemSpacer out = Out(self)
out = [] for item in cssvalue.seq:
for part in cssvalue.seq: type_, val = item.type, item.value
if hasattr(part, 'cssText'): if type_ in (cssutils.css.CSSColor,
# comments or CSSValue if a CSSValueList cssutils.css.CSSValue):
out.append(part.cssText) # CSSColor or CSSValue if a CSSValueList
elif isinstance(part, basestring) and part == u',': out.append(val.cssText, type_, space=False, keepS=True)
out.append(sep)
else: else:
# TODO: escape func parameter if STRING! if val and val[0] == val[-1] and val[0] in '\'"':
if part and part[0] == part[-1] and part[0] in '\'"': val = self._string(val[1:-1])
# string has " " around it in CSSValue! # S must be kept! in between values but no extra space
part = self._string(part[1:-1]) out.append(val, type_, space=False, keepS=True)
out.append(part) return out.value()
return (u''.join(out)).strip()
def do_css_CSSPrimitiveValue(self, cssvalue):
"""Serialize a CSSPrimitiveValue"""
# TODO: use self._valid(cssvalue)?
if not cssvalue:
return u''
else:
out = Out(self)
unary = None
for item in cssvalue.seq:
type_, val = item.type, item.value
if 'CHAR' == type_ and val in u'+-':
# save for next round
unary = val
continue
if cssutils.css.CSSColor == type_:
# Comment or CSSColor
val = val.cssText
elif type_ in ('DIMENSION', 'NUMBER', 'PERCENTAGE'):
# handle saved unary and add to number
try:
# NUMBER or DIMENSION and is it 0?
if 0 == cssvalue.getFloatValue():
val = u'0'
else:
# add unary to val if not 0
# TODO: only for lengths!
if u'-' == unary:
val = unary + val
except xml.dom.InvalidAccessErr, e:
pass
unary = None
elif unary:
# or simple add
out.append(unary, 'CHAR', space=False, keepS=True)
unary = None
out.append(val, type_)
# if hasattr(val, 'cssText'):
# # comments or CSSValue if a CSSValueList
# out.append(val.cssText, type_)
# else:
# out.append(val, type_) #?
return out.value()
def do_css_CSSColor(self, cssvalue):
"""Serialize a CSSColor value"""
if not cssvalue:
return u''
else:
out = Out(self)
unary = None
for item in cssvalue.seq:
type_, val = item.type, item.value
# prepare
if 'HASH' == type_:
# TODO: add pref for this!
if len(val) == 7 and val[1] == val[2] and \
val[3] == val[4] and val[5] == val[6]:
val = u'#%s%s%s' % (val[1], val[3], val[5])
elif 'CHAR' == type_ and val in u'+-':
# save - for next round
if u'-' == val:
# omit +
unary = val
continue
elif unary:
val = unary + val.cssText
unary = None
out.append(val, type_)
return out.value()
def do_stylesheets_medialist(self, medialist): def do_stylesheets_medialist(self, medialist):
""" """

View File

@ -2,7 +2,7 @@
""" """
__all__ = [] __all__ = []
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
__version__ = '$Id: util.py 1429 2008-08-11 19:01:52Z cthedot $' __version__ = '$Id: util.py 1453 2008-09-08 20:57:19Z cthedot $'
import codecs import codecs
from itertools import ifilter from itertools import ifilter
@ -13,6 +13,7 @@ import xml.dom
from helper import normalize from helper import normalize
import tokenize2 import tokenize2
import cssutils import cssutils
import encutils
class Base(object): class Base(object):
""" """
@ -153,7 +154,7 @@ class Base(object):
""" """
if token: if token:
value = token[1][4:-1].strip() value = token[1][4:-1].strip()
if (value[0] in '\'"') and (value[0] == value[-1]): if value and (value[0] in '\'"') and (value[0] == value[-1]):
# a string "..." or '...' # a string "..." or '...'
value = value.replace('\\'+value[0], value[0])[1:-1] value = value.replace('\\'+value[0], value[0])[1:-1]
return value return value
@ -326,7 +327,7 @@ class Base(object):
return p return p
def _parse(self, expected, seq, tokenizer, productions, default=None, def _parse(self, expected, seq, tokenizer, productions, default=None,
new=None): new=None, initialtoken=None):
""" """
puts parsed tokens in seq by calling a production with puts parsed tokens in seq by calling a production with
(seq, tokenizer, token) (seq, tokenizer, token)
@ -343,13 +344,28 @@ class Base(object):
default callback if tokentype not in productions default callback if tokentype not in productions
new new
used to init default productions used to init default productions
initialtoken
will be used together with tokenizer running 1st this token
and then all tokens in tokenizer
returns (wellformed, expected) which the last prod might have set returns (wellformed, expected) which the last prod might have set
""" """
wellformed = True wellformed = True
if tokenizer:
if initialtoken:
# add initialtoken to tokenizer
def tokens():
"Build new tokenizer including initialtoken"
yield initialtoken
for item in tokenizer:
yield item
fulltokenizer = (t for t in tokens())
else:
fulltokenizer = tokenizer
if fulltokenizer:
prods = self._adddefaultproductions(productions, new) prods = self._adddefaultproductions(productions, new)
for token in tokenizer: for token in fulltokenizer:
p = prods.get(token[0], default) p = prods.get(token[0], default)
if p: if p:
expected = p(expected, seq, token, tokenizer) expected = p(expected, seq, token, tokenizer)
@ -487,16 +503,36 @@ class Seq(object):
else: else:
self._seq[index] = Item(val, typ, line, col) self._seq[index] = Item(val, typ, line, col)
def appendToVal(self, val=None, index=-1):
"""
if not readonly append to Item's value at index
"""
if self._readonly:
raise AttributeError('Seq is readonly.')
else:
old = self._seq[index]
self._seq[index] = Item(old.value + val, old.type,
old.line, old.col)
def __repr__(self): def __repr__(self):
"returns a repr same as a list of tuples of (value, type)" "returns a repr same as a list of tuples of (value, type)"
return u'cssutils.%s.%s([\n %s])' % (self.__module__, return u'cssutils.%s.%s([\n %s])' % (self.__module__,
self.__class__.__name__, self.__class__.__name__,
u',\n '.join([u'(%r, %r)' % (item.type, item.value) u',\n '.join([u'%r' % item for item in self._seq]
for item in self._seq]
)) ))
def __str__(self): def __str__(self):
return "<cssutils.%s.%s object length=%r at 0x%x>" % ( vals = []
self.__module__, self.__class__.__name__, len(self), id(self)) for v in self:
if isinstance(v.value, basestring):
vals.append(v.value)
elif type(v) == tuple:
vals.append(v.value[1])
else:
vals.append(str(v))
return "<cssutils.%s.%s object length=%r valuestring=%r at 0x%x>" % (
self.__module__, self.__class__.__name__, len(self),
u''.join(vals), id(self))
class Item(object): class Item(object):
""" """
@ -740,7 +776,7 @@ def _defaultFetcher(url):
if res: if res:
mimeType, encoding = encutils.getHTTPInfo(res) mimeType, encoding = encutils.getHTTPInfo(res)
if mimeType != u'text/css': if mimeType != u'text/css':
cssutils.log.error(u'Expected "text/css" mime type for url=%s but found: %r' % cssutils.log.error(u'Expected "text/css" mime type for url=%r but found: %r' %
(url, mimeType), error=ValueError) (url, mimeType), error=ValueError)
return encoding, res.read() return encoding, res.read()