Compare commits
73 Commits
build/2.0.
...
build/2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2852407ea | ||
|
|
88e738c6cd | ||
|
|
eaae8bdb0b | ||
|
|
95d146fea2 | ||
|
|
dc20b68a37 | ||
|
|
b3ba4db00b | ||
|
|
a4c1480a1a | ||
|
|
91e0452320 | ||
|
|
ad80ea7885 | ||
|
|
1c20cda389 | ||
|
|
631759d833 | ||
|
|
ca02c66f26 | ||
|
|
3ac095d359 | ||
|
|
e1bc223de0 | ||
|
|
e065ead9b3 | ||
|
|
f9471f9b9b | ||
|
|
2612b50d06 | ||
|
|
d9ce2906a0 | ||
|
|
b76397f98e | ||
|
|
fcad9e0be5 | ||
|
|
2934347865 | ||
|
|
315f1b0207 | ||
|
|
965bd79a86 | ||
|
|
c18563e34b | ||
|
|
161e0de8d5 | ||
|
|
40aeca0740 | ||
|
|
63dd7fa7c0 | ||
|
|
509b49caf1 | ||
|
|
38c51cf79c | ||
|
|
0b693bba4e | ||
|
|
1258f34c78 | ||
|
|
510c0d5f56 | ||
|
|
cdb630e580 | ||
|
|
65fbd38105 | ||
|
|
1570132a55 | ||
|
|
7b5b748d23 | ||
|
|
041601c4a5 | ||
|
|
f692fd0202 | ||
|
|
e7b4de56f2 | ||
|
|
4a616a0c04 | ||
|
|
0814675d2a | ||
|
|
13df35462b | ||
|
|
899868f51e | ||
|
|
ee466aebce | ||
|
|
687ef2662e | ||
|
|
5aa29acbd3 | ||
|
|
1c2b3d063b | ||
|
|
551a000893 | ||
|
|
0d82d425cc | ||
|
|
0e1cea1034 | ||
|
|
2b75153148 | ||
|
|
c170615fb3 | ||
|
|
f6e84b6a35 | ||
|
|
6144f09a1f | ||
|
|
de142e8050 | ||
|
|
d0c1a119fd | ||
|
|
8fd80d3185 | ||
|
|
ae28c82858 | ||
|
|
1766764c7d | ||
|
|
129f8d72bd | ||
|
|
7314b5ecae | ||
|
|
7b0806355f | ||
|
|
49cf72e058 | ||
|
|
a11cad619d | ||
|
|
c1d35e8a57 | ||
|
|
fede348fbd | ||
|
|
f3c60e8fa6 | ||
|
|
00e53439ed | ||
|
|
368fced0c4 | ||
|
|
666771fb0f | ||
|
|
9e3f978677 | ||
|
|
f467d1c4f7 | ||
|
|
d8fc9d937e |
@@ -4,9 +4,11 @@ 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 re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -109,7 +111,8 @@ class ClientScript(Plugin):
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
data = cssprefixer.process(f, debug = False, minify = True)
|
||||
data = self.prefix(f)
|
||||
data = cssmin(f)
|
||||
data = data.replace('../images/', '../static/images/')
|
||||
data = data.replace('../fonts/', '../static/fonts/')
|
||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||
@@ -122,7 +125,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, ss(data.strip()))
|
||||
self.createFile(out, data.strip())
|
||||
|
||||
if not self.minified.get(file_type):
|
||||
self.minified[file_type] = {}
|
||||
@@ -170,3 +173,28 @@ class ClientScript(Plugin):
|
||||
if not self.paths[type].get(location):
|
||||
self.paths[type][location] = []
|
||||
self.paths[type][location].append(file_path)
|
||||
|
||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||
prefix_tags = ['ms', 'moz', 'webkit']
|
||||
def prefix(self, data):
|
||||
|
||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||
|
||||
new_data = ''
|
||||
colon_split = trimmed_data.split(';')
|
||||
for splt in colon_split:
|
||||
curl_split = splt.strip().split('{')
|
||||
for curly in curl_split:
|
||||
curly = curly.strip()
|
||||
for prop in self.prefix_properties:
|
||||
if curly[:len(prop) + 1] == prop + ':':
|
||||
for tag in self.prefix_tags:
|
||||
new_data += ' -%s-%s; ' % (tag, curly)
|
||||
|
||||
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
|
||||
|
||||
new_data += '; '
|
||||
|
||||
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
|
||||
|
||||
return new_data
|
||||
|
||||
@@ -33,7 +33,7 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
addEvent('app.load', self.autoUpdate)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
@@ -62,7 +62,7 @@ class Updater(Plugin):
|
||||
self.autoUpdate() # Check after enabling
|
||||
|
||||
def autoUpdate(self):
|
||||
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.updater.doUpdate():
|
||||
|
||||
# Notify before restarting
|
||||
@@ -80,8 +80,8 @@ class Updater(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def check(self):
|
||||
if self.isDisabled():
|
||||
def check(self, force = False):
|
||||
if not force and self.isDisabled():
|
||||
return
|
||||
|
||||
if self.updater.check():
|
||||
@@ -100,7 +100,7 @@ class Updater(Plugin):
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
'update_available': self.check(),
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import httplib
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
@@ -37,12 +36,7 @@ class Transmission(Downloader):
|
||||
|
||||
if len(self.conf('directory', default = '')) > 0:
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
|
||||
params['download-dir'] = folder_path
|
||||
params['download-dir'] = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('ratio'):
|
||||
@@ -94,11 +88,13 @@ class Transmission(Downloader):
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
|
||||
}
|
||||
queue = trpc.get_alltorrents(return_params)
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed getting queue: %s', err)
|
||||
return False
|
||||
|
||||
if not queue:
|
||||
return []
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get torrents status
|
||||
|
||||
@@ -3,10 +3,9 @@ from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from multipartpost import MultipartPostHandler
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import cookielib
|
||||
import httplib
|
||||
import json
|
||||
@@ -106,35 +105,6 @@ class uTorrent(Downloader):
|
||||
return False
|
||||
|
||||
statuses = StatusList(self)
|
||||
download_folder = ''
|
||||
settings_dict = {}
|
||||
|
||||
try:
|
||||
data = self.utorrent_api.get_settings()
|
||||
utorrent_settings = json.loads(data)
|
||||
|
||||
# Create settings dict
|
||||
for item in utorrent_settings['settings']:
|
||||
if item[1] == 0: # int
|
||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||
elif item[1] == 1: # bool
|
||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||
elif item[1] == 2: # string
|
||||
settings_dict[item[0]] = item[2]
|
||||
log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
# Get the download path from the uTorrent settings
|
||||
if settings_dict['dir_completed_download_flag']:
|
||||
download_folder = settings_dict['dir_completed_download']
|
||||
elif settings_dict['dir_active_download_flag']:
|
||||
download_folder = settings_dict['dir_active_download']
|
||||
else:
|
||||
log.info('No download folder set in uTorrent. Please set a download folder')
|
||||
return False
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
return False
|
||||
|
||||
# Get torrents
|
||||
for item in queue.get('torrents', []):
|
||||
@@ -144,18 +114,13 @@ class uTorrent(Downloader):
|
||||
if item[21] == 'Finished' or item[21] == 'Seeding':
|
||||
status = 'completed'
|
||||
|
||||
if settings_dict['dir_add_label']:
|
||||
release_folder = os.path.join(download_folder, item[11], item[2])
|
||||
else:
|
||||
release_folder = os.path.join(download_folder, item[2])
|
||||
|
||||
statuses.append({
|
||||
'id': item[0],
|
||||
'name': item[2],
|
||||
'status': status,
|
||||
'original_status': item[1],
|
||||
'timeleft': str(timedelta(seconds = item[10])),
|
||||
'folder': release_folder,
|
||||
'folder': item[26],
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -235,4 +200,22 @@ class uTorrentAPI(object):
|
||||
|
||||
def get_settings(self):
|
||||
action = "action=getsettings"
|
||||
return self._request(action)
|
||||
settings_dict = {}
|
||||
try:
|
||||
utorrent_settings = json.loads(self._request(action))
|
||||
|
||||
# Create settings dict
|
||||
for item in utorrent_settings['settings']:
|
||||
if item[1] == 0: # int
|
||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||
elif item[1] == 1: # bool
|
||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||
elif item[1] == 2: # string
|
||||
settings_dict[item[0]] = item[2]
|
||||
|
||||
#log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
|
||||
return settings_dict
|
||||
|
||||
@@ -139,8 +139,13 @@ def fireEvent(name, *args, **kwargs):
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
kwargs['async'] = True
|
||||
fireEvent(*args, **kwargs)
|
||||
try:
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
def errorHandler(error):
|
||||
etype, value, tb = error
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
@@ -50,8 +49,8 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except:
|
||||
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
if not Env.get('dev'):
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ var NotificationBase = new Class({
|
||||
});
|
||||
|
||||
window.addEvent('load', function(){
|
||||
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self)
|
||||
});
|
||||
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ config = [{
|
||||
'description': 'Default should be on localhost',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@ class Pushover(Notification):
|
||||
}
|
||||
|
||||
if data and data.get('library'):
|
||||
api_data.extend({
|
||||
api_data.update({
|
||||
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
|
||||
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
|
||||
})
|
||||
|
||||
@@ -31,6 +31,13 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -13,6 +13,7 @@ class XBMC(Notification):
|
||||
|
||||
listen_to = ['renamer.after']
|
||||
use_json_notifications = {}
|
||||
http_time_between_calls = 0
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class Plugin(object):
|
||||
|
||||
_needs_shutdown = False
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
@@ -104,12 +105,13 @@ class Plugin(object):
|
||||
if not params: params = {}
|
||||
|
||||
# Fill in some headers
|
||||
headers['Referer'] = headers.get('Referer', urlparse(url).hostname)
|
||||
headers['Host'] = headers.get('Host', urlparse(url).hostname)
|
||||
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
parsed_url = urlparse(url)
|
||||
host = parsed_url.hostname
|
||||
|
||||
host = urlparse(url).hostname
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
@@ -138,7 +140,7 @@ class Plugin(object):
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Dashboard(Plugin):
|
||||
profile_pre[profile.get('id')] = contains
|
||||
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
active_status, snatched_status, downloaded_status, available_status = fireEvent('status.get', ['active', 'snatched', 'downloaded', 'available'], single = True)
|
||||
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
|
||||
|
||||
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
|
||||
@@ -108,6 +108,14 @@ class Dashboard(Plugin):
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
coming_soon = True
|
||||
|
||||
# Skip if movie is snatched/downloaded/available
|
||||
skip = False
|
||||
for release in movie.releases:
|
||||
if release.status_id in [snatched_status.get('id'), downloaded_status.get('id'), available_status.get('id')]:
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if coming_soon:
|
||||
temp = movie.to_dict({
|
||||
@@ -125,6 +133,7 @@ class Dashboard(Plugin):
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
|
||||
@@ -73,7 +73,7 @@ class FileManager(Plugin):
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
if root == python_cache or 'minified' in filename or 'version' in filename: continue
|
||||
if root == python_cache or 'minified' in filename or 'version' in filename or 'temp_updater' in root: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
initialize: function(type, file){
|
||||
var self = this;
|
||||
|
||||
if(!file){
|
||||
self.empty = true;
|
||||
self.el = new Element('div');
|
||||
self.el = new Element('div.empty_file.'+type);
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ var File = new Class({
|
||||
var file_name = self.data.path.replace(/^.*[\\\/]/, '');
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'type_image ' + self.type.identifier
|
||||
'class': 'type_image ' + self.type.identifier,
|
||||
'styles': {
|
||||
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('img', {
|
||||
'src': Api.createUrl('file.cache') + file_name
|
||||
@@ -45,7 +48,7 @@ var FileSelect = new Class({
|
||||
});
|
||||
|
||||
if(single)
|
||||
return new File(results.pop());
|
||||
return new File(type, results.pop());
|
||||
|
||||
return results;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class LibraryPlugin(Plugin):
|
||||
addEvent('library.update', self.update)
|
||||
addEvent('library.update_release_date', self.updateReleaseDate)
|
||||
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
|
||||
db = get_session()
|
||||
@@ -53,6 +52,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
@@ -132,6 +132,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
@@ -150,6 +151,7 @@ class LibraryPlugin(Plugin):
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
db.expire_all()
|
||||
return dates
|
||||
|
||||
|
||||
|
||||
@@ -6,19 +6,6 @@ Page.Log = new Class({
|
||||
title: 'Show recent logs.',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options)
|
||||
|
||||
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}))
|
||||
|
||||
},
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
@@ -118,12 +118,13 @@ class MoviePlugin(Plugin):
|
||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
#
|
||||
for movie in movies:
|
||||
for rel in movie.releases:
|
||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def getView(self):
|
||||
|
||||
movie_id = getParam('id')
|
||||
@@ -149,6 +150,7 @@ class MoviePlugin(Plugin):
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
@@ -174,8 +176,6 @@ class MoviePlugin(Plugin):
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
total_count = q.count()
|
||||
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
@@ -193,6 +193,8 @@ class MoviePlugin(Plugin):
|
||||
if filter_or:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
@@ -216,15 +218,14 @@ class MoviePlugin(Plugin):
|
||||
results = q2.all()
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = movie.to_dict({
|
||||
movies.append(movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movies.append(temp)
|
||||
}))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return (total_count, movies)
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
@@ -259,7 +260,7 @@ class MoviePlugin(Plugin):
|
||||
if char not in chars:
|
||||
chars += str(char)
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
|
||||
def listView(self):
|
||||
@@ -318,8 +319,7 @@ class MoviePlugin(Plugin):
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -428,7 +428,7 @@ class MoviePlugin(Plugin):
|
||||
if added:
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ class MoviePlugin(Plugin):
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -540,7 +540,7 @@ class MoviePlugin(Plugin):
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
@@ -568,7 +568,6 @@ class MoviePlugin(Plugin):
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -578,6 +577,7 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||
db.expire_all()
|
||||
|
||||
return onComplete
|
||||
|
||||
@@ -588,5 +588,6 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
|
||||
@@ -14,6 +14,7 @@ var MovieList = new Class({
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
@@ -23,7 +24,7 @@ var MovieList = new Class({
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'startswith': null,
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ var MovieList = new Class({
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
@@ -62,7 +63,8 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id]
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -76,6 +78,7 @@ var MovieList = new Class({
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
@@ -115,7 +118,7 @@ var MovieList = new Class({
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
self.total_movies = total;
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
@@ -125,8 +128,41 @@ var MovieList = new Class({
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
self.counter_count = count;
|
||||
self.navigation_counter.set('text', (count || 0) + ' movies');
|
||||
|
||||
if (self.empty_message) {
|
||||
self.empty_message.destroy();
|
||||
self.empty_message = null;
|
||||
}
|
||||
|
||||
if(self.total_movies && count == 0 && !self.empty_message){
|
||||
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
self.empty_message = new Element('.message', {
|
||||
'html': 'No movies found ' + message + '.<br/>'
|
||||
}).grab(
|
||||
new Element('a', {
|
||||
'text': 'Reset filter',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.filter = {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
self.navigation_search_input.set('value', '');
|
||||
self.reset();
|
||||
self.activateLetter();
|
||||
self.getMovies(true);
|
||||
self.last_search_value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.movie_list);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createMovie: function(movie, inject_at){
|
||||
@@ -151,66 +187,69 @@ var MovieList = new Class({
|
||||
|
||||
self.el.addClass('with_navigation')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').grab(
|
||||
new Element('div').adopt(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
self.movie_list.empty()
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies()
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide()
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
|
||||
self.navigation_search_input = new Element('input.search.inlay', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self),
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
self.navigation_menu = new Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
@@ -223,20 +262,39 @@ var MovieList = new Class({
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
self.navigation_search_input.focus();
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(this.get('data-view'));
|
||||
this.addClass(a);
|
||||
}
|
||||
}
|
||||
}).adopt(new Element('span'))
|
||||
)
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
@@ -260,11 +318,11 @@ var MovieList = new Class({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -322,14 +380,14 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy()
|
||||
erase_movies.include(movie)
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -448,8 +506,7 @@ var MovieList = new Class({
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
@@ -461,11 +518,10 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(){
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
@@ -492,10 +548,13 @@ var MovieList = new Class({
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit + ',' + self.offset
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
if(reset)
|
||||
self.movie_list.empty();
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
class_name: 'action icon2',
|
||||
|
||||
initialize: function(movie){
|
||||
var self = this;
|
||||
@@ -82,7 +82,7 @@ MA.Release = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
@@ -100,10 +100,8 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
self.options_container = new Element('div.options').grab(
|
||||
self.release_container = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
@@ -149,11 +147,11 @@ MA.Release = new Class({
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
release.info['detail_url'] ? new Element('a.info.icon', {
|
||||
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -162,7 +160,7 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -190,7 +188,9 @@ MA.Release = new Class({
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
@@ -237,18 +237,19 @@ MA.Release = new Class({
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||
|
||||
self.trynext_container.adopt(
|
||||
self.next_release ? [new Element('a.icon.readd', {
|
||||
self.next_release ? [new Element('a.icon2.readd', {
|
||||
'text': self.last_release ? 'Download another release' : 'Download the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.icon.download', {
|
||||
new Element('a.icon2.download', {
|
||||
'text': 'pick one yourself',
|
||||
'events': {
|
||||
'click': function(){
|
||||
@@ -256,7 +257,7 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
})] : null,
|
||||
new Element('a.icon.completed', {
|
||||
new Element('a.icon2.completed', {
|
||||
'text': 'mark this movie done',
|
||||
'events': {
|
||||
'click': function(){
|
||||
@@ -292,7 +293,7 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
self.movie.busy(true);
|
||||
|
||||
|
||||
@@ -9,9 +9,20 @@
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.movies > div .message {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.movies > div .message a {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies.thumbs_list > div:not(.description) {
|
||||
margin-right: -4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .loading {
|
||||
@@ -79,24 +90,36 @@
|
||||
|
||||
.movies .movie {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
|
||||
transition-property: width, height;
|
||||
background: rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.movies.mass_edit_list .movie {
|
||||
padding-left: 22px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies.details_list .movie {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
height: 32px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.movies.list_list .movie:last-child,
|
||||
.movies.mass_edit_list .movie:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie {
|
||||
width: 16.66667%;
|
||||
height: auto;
|
||||
@@ -104,9 +127,6 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@@ -125,16 +145,7 @@
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
margin: 1px 0;
|
||||
border-radius: 0;
|
||||
background: no-repeat;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.movies.list_list .movie:hover:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
background: rgba(255,255,255,0.03);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.movies .data {
|
||||
@@ -142,18 +153,18 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
right: 0;
|
||||
}
|
||||
.movies.list_list .movie:not(.details_view) .data,
|
||||
.movies.mass_edit_list .movie .data {
|
||||
height: 30px;
|
||||
padding: 3px 0 3px 10px;
|
||||
box-shadow: none;
|
||||
padding: 0 0 0 10px;
|
||||
border: 0;
|
||||
background: #4e5969;
|
||||
}
|
||||
.movies.mass_edit_list .movie .data {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .data {
|
||||
position: absolute;
|
||||
@@ -165,14 +176,6 @@
|
||||
background: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
|
||||
}
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
|
||||
|
||||
.movies.thumbs_list .movie:hover .data {
|
||||
background: rgba(0,0,0,0.9);
|
||||
@@ -201,18 +204,16 @@
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 4px 0 0 4px;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
.movies.thumbs_list .poster {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
}
|
||||
.movies.list_list .movie:not(.details_view) .poster,
|
||||
.movies.mass_edit_list .poster {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
border-radius: 1px 0 0 1px;
|
||||
}
|
||||
.movies.mass_edit_list .poster {
|
||||
display: none;
|
||||
@@ -222,7 +223,13 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: none;
|
||||
background: no-repeat center;
|
||||
background-size: cover;
|
||||
}
|
||||
.movies.thumbs_list .no_thumbnail .empty_file {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .poster img,
|
||||
.options .poster img {
|
||||
@@ -234,7 +241,7 @@
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.movies .info {
|
||||
@@ -248,22 +255,32 @@
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 2px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-right: 80px;
|
||||
transition: all 0.2s linear;
|
||||
height: 35px;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
.movies.list_list .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.touch_enabled .movies.list_list .info .title {
|
||||
display: inline-block;
|
||||
padding-right: 55px;
|
||||
}
|
||||
|
||||
.movies .info .title span {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .info .title span {
|
||||
@@ -300,13 +317,14 @@
|
||||
font-size: 21px;
|
||||
word-wrap: break-word;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .info .year {
|
||||
position: absolute;
|
||||
color: #bbb;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
top: 6px;
|
||||
text-align: right;
|
||||
transition: all 0.2s linear;
|
||||
font-weight: normal;
|
||||
@@ -326,6 +344,10 @@
|
||||
right: auto;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.touch_enabled .movies.list_list .movie .info .year {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.movies .info .description {
|
||||
top: 30px;
|
||||
@@ -350,6 +372,17 @@
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.movies.list_list .movie:hover .data .quality {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.touch_enabled .movies.list_list .movie .data .quality {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .quality {
|
||||
display: none;
|
||||
@@ -382,12 +415,12 @@
|
||||
right: 0;
|
||||
margin-right: 60px;
|
||||
z-index: 1;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.movies .data .quality .available,
|
||||
.movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -407,13 +440,14 @@
|
||||
|
||||
.movies .data .actions {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
bottom: 17px;
|
||||
right: 20px;
|
||||
line-height: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .actions {
|
||||
@@ -421,10 +455,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.movies .movie:hover .data .actions {
|
||||
.movies .movie:hover .data .actions,
|
||||
.touch_enabled .movies .movie .data .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.movies.details_list .data .actions {
|
||||
@@ -437,7 +471,7 @@
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.thumbs_list .data .actions {
|
||||
bottom: 2px;
|
||||
bottom: 12px;
|
||||
right: 10px;
|
||||
top: auto;
|
||||
}
|
||||
@@ -446,20 +480,31 @@
|
||||
.movies .movie:hover .action:hover { opacity: 1; }
|
||||
|
||||
.movies .data .action {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
height: 22px;
|
||||
min-width: 33px;
|
||||
padding: 0 5px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.movies .data .action.trailer { color: #FFF; }
|
||||
.movies .data .action.download { color: #b9dec0; }
|
||||
.movies .data .action.edit { color: #c6b589; }
|
||||
.movies .data .action.refresh { color: #cbeecc; }
|
||||
.movies .data .action.delete { color: #e9b0b0; }
|
||||
.movies .data .action.directory { color: #ffed92; }
|
||||
.movies .data .action.readd { color: #c2fac5; }
|
||||
|
||||
.movies.mass_edit_list .movie .data .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.list_list .movie:not(.details_view):hover .actions,
|
||||
.movies.mass_edit_list .movie:hover .actions {
|
||||
.movies.mass_edit_list .movie:hover .actions,
|
||||
.touch_enabled .movies.list_list .movie:not(.details_view) .actions {
|
||||
margin: 0;
|
||||
background: #4e5969;
|
||||
top: 2px;
|
||||
@@ -473,12 +518,10 @@
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
padding: 70px 0 0;
|
||||
padding: 80px 0 0;
|
||||
left: 120px;
|
||||
right: 0;
|
||||
}
|
||||
.movies .delete_container .cancel {
|
||||
}
|
||||
.movies .delete_container .or {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -498,9 +541,9 @@
|
||||
}
|
||||
|
||||
.movies .options .form {
|
||||
margin: 70px 20px 0;
|
||||
float: left;
|
||||
margin: 80px 0 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .options .form select {
|
||||
@@ -510,6 +553,7 @@
|
||||
.movies .options .table {
|
||||
height: 180px;
|
||||
overflow: auto;
|
||||
line-height: 2em;
|
||||
}
|
||||
.movies .options .table .item {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
@@ -518,8 +562,10 @@
|
||||
text-decoration: line-through;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.movies .options .table .item.ignored .delete {
|
||||
background-image: url('../images/icon.undo.png');
|
||||
.movies .options .table .item.ignored .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.movies .options .table .item:last-child { border: 0; }
|
||||
@@ -565,10 +611,12 @@
|
||||
width: 30px !important;
|
||||
height: 20px;
|
||||
opacity: 0.8;
|
||||
line-height: 25px;
|
||||
}
|
||||
.movies .options .table a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .options .table a:hover { opacity: 1; }
|
||||
.movies .options .table a.download { color: #a7fbaf; }
|
||||
.movies .options .table a.delete { color: #fda3a3; }
|
||||
.movies .options .table .ignored a.delete { color: #b5fda3; }
|
||||
|
||||
.movies .options .table .head > * {
|
||||
font-weight: bold;
|
||||
@@ -601,7 +649,6 @@
|
||||
text-align: center;
|
||||
padding: 3px 10px;
|
||||
background: #4e5969;
|
||||
border-radius: 0 0 2px 2px;
|
||||
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||
z-index: 11;
|
||||
}
|
||||
@@ -637,16 +684,17 @@
|
||||
.movies .movie .trynext {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
right: 135px;
|
||||
right: 180px;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
background: #4e5969;
|
||||
min-width: 300px;
|
||||
text-align: right;
|
||||
height: 100%;
|
||||
padding: 3px 0;
|
||||
top: 0;
|
||||
}
|
||||
.touch_enabled .movies .movie .trynext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .movie .trynext {
|
||||
@@ -655,9 +703,10 @@
|
||||
}
|
||||
.movies.mass_edit_list .trynext { display: none; }
|
||||
.wanted .movies .movie .trynext {
|
||||
padding-right: 50px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
.movies .movie:hover .trynext {
|
||||
.movies .movie:hover .trynext,
|
||||
.touch_enabled .movies.details_list .movie .trynext {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -665,8 +714,7 @@
|
||||
background: #47515f;
|
||||
padding: 0;
|
||||
right: 0;
|
||||
bottom: 35px;
|
||||
height: auto;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.movies .movie .trynext a {
|
||||
@@ -674,12 +722,23 @@
|
||||
padding: 0 5px 0 25px;
|
||||
margin-right: 10px;
|
||||
color: #FFF;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
line-height: 27px;
|
||||
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
||||
}
|
||||
.movies .movie .trynext a:last-child {
|
||||
margin: 0;
|
||||
.movies .movie .trynext a:before {
|
||||
margin: 2px 0 0 -20px;
|
||||
position: absolute;
|
||||
font-family: 'Elusive-Icons';
|
||||
}
|
||||
.movies .movie .trynext a:hover {
|
||||
.movies.details_list .movie .trynext a {
|
||||
line-height: 23px;
|
||||
}
|
||||
.movies .movie .trynext a:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
.movies .movie .trynext a:hover,
|
||||
.touch_enabled .movies .movie .trynext a {
|
||||
background-color: #369545;
|
||||
}
|
||||
|
||||
@@ -694,14 +753,7 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav {
|
||||
transition: box-shadow .4s linear;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
top: 0px;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
@@ -709,13 +761,10 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.movies .alph_nav > div {
|
||||
position: relative;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
min-height: 24px;
|
||||
|
||||
.movies .alph_nav .menus {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers,
|
||||
@@ -724,118 +773,152 @@
|
||||
list-style: none;
|
||||
padding: 0 0 1px;
|
||||
margin: 0;
|
||||
float: left;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.movies .alph_nav .counter {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
right: 270px;
|
||||
background: #4e5969;
|
||||
padding: 4px 10px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
line-height: 43px;
|
||||
border-right: 1px solid rgba(255,255,255,.07);
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
height: 24px;
|
||||
line-height: 23px;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: rgba(255,255,255,0.2);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@media all and (max-width: 900px) {
|
||||
.movies .alph_nav .numbers {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li {
|
||||
width: auto;
|
||||
padding: 0 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.movies .alph_nav .numbers li.letter_all {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.movies .alph_nav li.available {
|
||||
color: #FFF;
|
||||
font-weight: bolder;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.available:hover {
|
||||
background: rgba(255,255,255,.1);
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.movies .alph_nav .search {
|
||||
.movies .alph_nav .search input {
|
||||
padding: 6px 5px;
|
||||
margin: 0 0 0 20px;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
width: 154px;
|
||||
height: 25px;
|
||||
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #444;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
padding: 0 10px 0 30px;
|
||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||
}
|
||||
.movies .alph_nav .search input:focus {
|
||||
background: rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.movies .alph_nav .search input::-webkit-input-placeholder {
|
||||
color: #444;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.movies .alph_nav .search:before {
|
||||
font-family: 'Elusive-Icons';
|
||||
content: "\e03e";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
line-height: 45px;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0 10px;
|
||||
opacity: .6;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions {
|
||||
margin: 0 6px 0 0;
|
||||
-moz-user-select: none;
|
||||
position: absolute;
|
||||
right: 183px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255,255,255,.07);
|
||||
border-width: 0 1px;
|
||||
}
|
||||
.movies .alph_nav .actions:hover {
|
||||
box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55);
|
||||
}
|
||||
.movies .alph_nav .actions li {
|
||||
border-radius: 1px;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.movies .alph_nav .actions li.active {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.movies .alph_nav .actions li span {
|
||||
display: block;
|
||||
background: url('../images/sprite.png') no-repeat;
|
||||
width: 25px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.mass_edit span {
|
||||
background-position: 3px 3px;
|
||||
.movies .alph_nav .actions:hover li:not(.active) {
|
||||
display: block;
|
||||
background: #FFF;
|
||||
color: #444;
|
||||
}
|
||||
.movies .alph_nav .actions li:hover:not(.active) {
|
||||
background: #ccc;
|
||||
}
|
||||
.movies .alph_nav .actions li.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.list span {
|
||||
background-position: 3px -95px;
|
||||
.movies .alph_nav .actions li.mass_edit:before {
|
||||
content: "\e070";
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.details span {
|
||||
background-position: 3px -74px;
|
||||
.movies .alph_nav .actions li.list:before {
|
||||
content: "\e0d8";
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.movies .alph_nav .actions li:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.movies .alph_nav .actions li.details:before {
|
||||
content: "\e022";
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select {
|
||||
float: left;
|
||||
margin: 5px 0 0 5px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select span {
|
||||
.movies.mass_edit_list .mass_edit_form .select .check {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: -4px 0 0 5px;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select span {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select .count {
|
||||
@@ -844,8 +927,7 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .quality {
|
||||
float: left;
|
||||
padding: 8px 0 0;
|
||||
display: inline-block;
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
.movies .alph_nav .mass_edit_form .quality select {
|
||||
@@ -858,8 +940,8 @@
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .delete {
|
||||
float: left;
|
||||
padding: 8px 0 0 8px;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh span,
|
||||
@@ -867,28 +949,48 @@
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu > a {
|
||||
background-color: #4e5969;
|
||||
background-position: center -158px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.extra > a:before {
|
||||
content: '...';
|
||||
font-size: 1.7em;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter {
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter > a:before {
|
||||
content: "\e0e8";
|
||||
font-family: 'Elusive-Icons';
|
||||
line-height: 33px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter .wrapper {
|
||||
right: 88px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.movies .empty_wanted {
|
||||
background-image: url('../images/emptylist.png');
|
||||
background-position: 80% 0;
|
||||
height: 750px;
|
||||
width: 800px;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
padding-top: 260px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.movies .empty_manage {
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
line-height: 150%;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.movies .empty_manage .after_manage {
|
||||
@@ -897,7 +999,6 @@
|
||||
}
|
||||
|
||||
.movies .progress {
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
text-align: left;
|
||||
@@ -912,7 +1013,6 @@
|
||||
width: 49%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin: 2px 0.5%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.movies .progress > div .folder {
|
||||
|
||||
@@ -11,7 +11,7 @@ var Movie = new Class({
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie.inlay');
|
||||
self.el = new Element('div.movie');
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.parent(self, options);
|
||||
@@ -23,7 +23,8 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.busy(false)
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
|
||||
@@ -107,6 +108,7 @@ var Movie = new Class({
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.create();
|
||||
@@ -139,9 +141,6 @@ var Movie = new Class({
|
||||
'text': self.data.library.year || 'n/a'
|
||||
})
|
||||
),
|
||||
self.rating = new Element('div.rating.icon', {
|
||||
'text': self.data.library.rating
|
||||
}),
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
@@ -149,8 +148,8 @@ var Movie = new Class({
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -199,9 +198,6 @@ var Movie = new Class({
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
if(!self.data.library.rating)
|
||||
self.rating.hide();
|
||||
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
@@ -244,10 +240,10 @@ var Movie = new Class({
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('details')
|
||||
self.changeView('details');
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView()
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
|
||||
@@ -44,9 +44,8 @@
|
||||
.search_form .input input {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: rgba(255,255,255,.08);
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font-size: 25px;
|
||||
height: 100%;
|
||||
@@ -106,7 +105,7 @@
|
||||
background: #5c697b;
|
||||
margin: 4px 0 0;
|
||||
width: 470px;
|
||||
min-height: 140px;
|
||||
min-height: 50px;
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ Block.Search = new Class({
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q){
|
||||
if(self.api_request && self.api_request.isRunning())
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
@@ -116,12 +119,9 @@ Block.Search = new Class({
|
||||
},
|
||||
|
||||
list: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.api_request && self.api_request.running) return
|
||||
|
||||
var q = self.q();
|
||||
var cache = self.cache[q];
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
@@ -164,9 +164,6 @@ Block.Search = new Class({
|
||||
|
||||
});
|
||||
|
||||
if(q != self.q())
|
||||
self.list()
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
|
||||
@@ -62,6 +62,7 @@ class ProfilePlugin(Plugin):
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
@@ -109,6 +110,7 @@ class ProfilePlugin(Plugin):
|
||||
default = db.query(Profile).first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
|
||||
db.expire_all()
|
||||
return default_dict
|
||||
|
||||
def saveOrder(self):
|
||||
@@ -151,6 +153,7 @@ class ProfilePlugin(Plugin):
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': success,
|
||||
'message': message
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
.profile > .delete {
|
||||
position: absolute;
|
||||
padding: 25px 20px;
|
||||
background-position: center;
|
||||
padding: 16px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: #fd5353;
|
||||
}
|
||||
.profile > .delete:hover {
|
||||
opacity: 1;
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
.profile .qualities {
|
||||
min-height: 80px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.profile .formHint {
|
||||
@@ -97,11 +98,13 @@
|
||||
}
|
||||
|
||||
.profile .types .type .delete {
|
||||
background-position: left center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #fd5353;
|
||||
}
|
||||
|
||||
.profile .types .type:hover:not(.is_empty) .delete {
|
||||
|
||||
@@ -24,7 +24,7 @@ var Profile = new Class({
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.profile').adopt(
|
||||
self.delete_button = new Element('span.delete.icon', {
|
||||
self.delete_button = new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
@@ -256,7 +256,7 @@ Profile.Type = new Class({
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('span.delete.icon', {
|
||||
new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
|
||||
@@ -25,7 +24,7 @@ class QualityPlugin(Plugin):
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
@@ -164,7 +163,6 @@ class QualityPlugin(Plugin):
|
||||
if cached and extra is {}: return cached
|
||||
|
||||
for cur_file in files:
|
||||
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
|
||||
words = re.split('\W+', cur_file.lower())
|
||||
|
||||
for quality in self.all():
|
||||
@@ -188,29 +186,30 @@ class QualityPlugin(Plugin):
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
# Try again with loose testing
|
||||
quality = self.guessLoose(hash, extra = extra)
|
||||
quality = self.guessLoose(hash, files = files, extra = extra)
|
||||
if quality:
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
log.debug('Could not identify quality for: %s', files)
|
||||
return None
|
||||
|
||||
def guessLoose(self, hash, extra):
|
||||
def guessLoose(self, hash, files = None, extra = None):
|
||||
|
||||
for quality in self.all():
|
||||
if extra:
|
||||
for quality in self.all():
|
||||
|
||||
# Check width resolution, range 20
|
||||
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
# Check width resolution, range 20
|
||||
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
# Check height resolution, range 20
|
||||
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
# Check height resolution, range 20
|
||||
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(hash, self.single('dvdrip'))
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(hash, self.single('dvdrip'))
|
||||
|
||||
return None
|
||||
|
||||
@@ -180,7 +180,7 @@ class Release(Plugin):
|
||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||
|
||||
if item['type'] != 'torrent_magnet':
|
||||
item['download'] = provider.download
|
||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||
|
||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
|
||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb, link, symlink
|
||||
getImdb, link, symlink, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||
@@ -25,10 +25,10 @@ class Renamer(Plugin):
|
||||
checking_snatched = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('renamer.scan', self.scanView, docs = {
|
||||
'desc': 'For the renamer to check for new files to rename in a folder',
|
||||
'params': {
|
||||
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
||||
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
|
||||
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
|
||||
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
|
||||
@@ -62,11 +62,14 @@ class Renamer(Plugin):
|
||||
def scanView(self):
|
||||
|
||||
params = getParams()
|
||||
async = tryInt(params.get('async', None))
|
||||
movie_folder = params.get('movie_folder', None)
|
||||
downloader = params.get('downloader', None)
|
||||
download_id = params.get('download_id', None)
|
||||
|
||||
fireEventAsync('renamer.scan',
|
||||
fire_handle = fireEvent if not async else fireEventAsync
|
||||
|
||||
fire_handle('renamer.scan',
|
||||
movie_folder = movie_folder,
|
||||
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
|
||||
)
|
||||
@@ -204,6 +207,7 @@ class Renamer(Plugin):
|
||||
cd = 1 if multiple else 0
|
||||
|
||||
for current_file in sorted(list(group['files'][file_type])):
|
||||
current_file = toUnicode(current_file)
|
||||
|
||||
# Original filename
|
||||
replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
|
||||
|
||||
@@ -336,7 +336,7 @@ class Scanner(Plugin):
|
||||
break
|
||||
|
||||
if return_ignored is False and identifier in ignored_identifiers:
|
||||
log.debug('Ignore file found, ignoring release: %s' % identifier)
|
||||
log.debug('Ignore file found, ignoring release: %s', identifier)
|
||||
continue
|
||||
|
||||
# Group extra (and easy) files first
|
||||
@@ -385,6 +385,8 @@ class Scanner(Plugin):
|
||||
for file_type in group['files']:
|
||||
if not file_type is 'leftover':
|
||||
group['files']['leftover'] -= set(group['files'][file_type])
|
||||
group['files'][file_type] = list(group['files'][file_type])
|
||||
group['files']['leftover'] = list(group['files']['leftover'])
|
||||
|
||||
# Delete the unsorted list
|
||||
del group['unsorted_files']
|
||||
@@ -438,11 +440,14 @@ class Scanner(Plugin):
|
||||
if data.get('audio'): break
|
||||
|
||||
# Use the quality guess first, if that failes use the quality we wanted to download
|
||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||
data['quality'] = None
|
||||
if download_info and download_info.get('quality'):
|
||||
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
|
||||
|
||||
if not data['quality']:
|
||||
if download_info and download_info.get('quality'):
|
||||
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
|
||||
else:
|
||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||
|
||||
if not data['quality']:
|
||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
||||
|
||||
@@ -392,7 +392,7 @@ class Searcher(Plugin):
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if self.conf('required_words') and req_match == 0:
|
||||
log.info2("Wrong: Required word missing: %s" % nzb['name'])
|
||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
||||
return False
|
||||
|
||||
# Ignore releases
|
||||
@@ -403,7 +403,7 @@ class Searcher(Plugin):
|
||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
||||
|
||||
if self.conf('ignored_words') and ignored_match:
|
||||
log.info2("Wrong: '%s' contains 'ignored words'" % (nzb['name']))
|
||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
||||
return False
|
||||
|
||||
# Ignore porn stuff
|
||||
@@ -462,7 +462,7 @@ class Searcher(Plugin):
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}):
|
||||
|
||||
@@ -59,7 +59,7 @@ class Subtitle(Plugin):
|
||||
|
||||
for d_sub in downloaded:
|
||||
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
|
||||
group['files']['subtitle'].add(d_sub.path)
|
||||
group['files']['subtitle'].append(d_sub.path)
|
||||
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
||||
|
||||
return True
|
||||
|
||||
@@ -11,6 +11,7 @@ log = CPLog(__name__)
|
||||
class Automation(Provider):
|
||||
|
||||
enabled_option = 'automation_enabled'
|
||||
http_time_between_calls = 2
|
||||
|
||||
interval = 86400
|
||||
last_checked = 0
|
||||
@@ -73,13 +74,13 @@ class Automation(Provider):
|
||||
req_match += len(list(set(movie_genres) & set(req))) == len(req)
|
||||
|
||||
if self.getMinimal('required_genres') and req_match == 0:
|
||||
log.info2("Required genre(s) missing for %s" % movie['original_title'])
|
||||
log.info2('Required genre(s) missing for %s', movie['original_title'])
|
||||
return False
|
||||
|
||||
for ign_set in ignored_genres:
|
||||
ign = splitString(ign_set, '&')
|
||||
if len(list(set(movie_genres) & set(ign))) == len(ign):
|
||||
log.info2("%s has blacklisted genre(s): %s" % (movie['original_title'], ign))
|
||||
log.info2('%s has blacklisted genre(s): %s', (movie['original_title'], ign))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Goodfilms(Automation):
|
||||
|
||||
url = 'http://goodfil.ms/%s/queue'
|
||||
url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
@@ -25,12 +25,25 @@ class Goodfilms(Automation):
|
||||
|
||||
def getWatchlist(self):
|
||||
|
||||
url = self.url % self.conf('automation_username')
|
||||
soup = BeautifulSoup(self.getHTMLData(url))
|
||||
|
||||
movies = []
|
||||
page = 1
|
||||
|
||||
for movie in soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True }):
|
||||
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
|
||||
while True:
|
||||
url = self.url % (self.conf('automation_username'), page)
|
||||
data = self.getHTMLData(url)
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
this_watch_list = soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True })
|
||||
|
||||
if not this_watch_list: # No Movies
|
||||
break
|
||||
|
||||
for movie in this_watch_list:
|
||||
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
|
||||
|
||||
if not 'next page' in data.lower():
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
return movies
|
||||
|
||||
@@ -35,10 +35,10 @@ class Rottentomatoes(Automation, RSS):
|
||||
name = result.group(0)
|
||||
|
||||
if rating < tryInt(self.conf('tomatometer_percent')):
|
||||
log.info2('%s seems to be rotten...' % name)
|
||||
log.info2('%s seems to be rotten...', name)
|
||||
else:
|
||||
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s' % (rating, name))
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
imdb = self.search(name, year)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Provider(Plugin):
|
||||
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
||||
data = self.getCache(cache_key, url, **kwargs)
|
||||
|
||||
if data:
|
||||
if data and len(data) > 0:
|
||||
try:
|
||||
data = XMLTree.fromstring(data)
|
||||
return self.getElements(data, item_path)
|
||||
@@ -104,7 +104,7 @@ class YarrProvider(Provider):
|
||||
try:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
opener.addheaders = []
|
||||
opener.addheaders = [('User-Agent', self.user_agent)]
|
||||
urllib2.install_opener(opener)
|
||||
log.info2('Logging into %s', self.urls['login'])
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
@@ -114,9 +114,12 @@ class YarrProvider(Provider):
|
||||
if self.loginSuccess(output):
|
||||
self.login_opener = opener
|
||||
return True
|
||||
except:
|
||||
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
error = 'unknown'
|
||||
except:
|
||||
error = traceback.format_exc()
|
||||
|
||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
||||
return False
|
||||
|
||||
def loginSuccess(self, output):
|
||||
@@ -179,7 +182,7 @@ class YarrProvider(Provider):
|
||||
if hostname in download_url:
|
||||
return self
|
||||
except:
|
||||
log.debug('Url % s doesn\'t belong to %s', (url, self.getName()))
|
||||
log.debug('Url %s doesn\'t belong to %s', (url, self.getName()))
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.helpers.variable import mergeDicts, randomString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -11,6 +12,25 @@ log = CPLog(__name__)
|
||||
|
||||
class MovieResultModifier(Plugin):
|
||||
|
||||
default_info = {
|
||||
'tmdb_id': 0,
|
||||
'titles': [],
|
||||
'original_title': '',
|
||||
'year': 0,
|
||||
'images': {
|
||||
'poster': [],
|
||||
'backdrop': [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': []
|
||||
},
|
||||
'runtime': 0,
|
||||
'plot': '',
|
||||
'tagline': '',
|
||||
'imdb': '',
|
||||
'genres': [],
|
||||
'release_date': {}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
||||
addEvent('result.modify.movie.info', self.checkLibrary)
|
||||
@@ -67,6 +87,9 @@ class MovieResultModifier(Plugin):
|
||||
return temp
|
||||
|
||||
def checkLibrary(self, result):
|
||||
|
||||
result = mergeDicts(copy.deepcopy(self.default_info), copy.deepcopy(result))
|
||||
|
||||
if result and result.get('imdb'):
|
||||
return mergeDicts(result, self.getLibraryTags(result['imdb']))
|
||||
return result
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from couchpotato.core.settings.model import Movie
|
||||
@@ -15,13 +14,13 @@ log = CPLog(__name__)
|
||||
class CouchPotatoApi(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'https://couchpota.to/api/search/%s/',
|
||||
'info': 'https://couchpota.to/api/info/%s/',
|
||||
'is_movie': 'https://couchpota.to/api/ismovie/%s/',
|
||||
'eta': 'https://couchpota.to/api/eta/%s/',
|
||||
'suggest': 'https://couchpota.to/api/suggest/',
|
||||
'updater': 'https://couchpota.to/api/updater/?%s',
|
||||
'messages': 'https://couchpota.to/api/messages/?%s',
|
||||
'search': 'https://api.couchpota.to/search/%s/',
|
||||
'info': 'https://api.couchpota.to/info/%s/',
|
||||
'is_movie': 'https://api.couchpota.to/ismovie/%s/',
|
||||
'eta': 'https://api.couchpota.to/eta/%s/',
|
||||
'suggest': 'https://api.couchpota.to/suggest/',
|
||||
'updater': 'https://api.couchpota.to/updater/?%s',
|
||||
'messages': 'https://api.couchpota.to/messages/?%s',
|
||||
}
|
||||
http_time_between_calls = 0
|
||||
api_version = 1
|
||||
@@ -71,7 +70,8 @@ class CouchPotatoApi(MovieProvider):
|
||||
return
|
||||
|
||||
result = self.getJsonData(self.urls['info'] % identifier, headers = self.getRequestHeaders())
|
||||
if result: return result
|
||||
if result:
|
||||
return dict((k, v) for k, v in result.iteritems() if v)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class OMDBAPI(MovieProvider):
|
||||
movie_data = {
|
||||
'via_imdb': True,
|
||||
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
||||
'original_title': movie.get('Title', ''),
|
||||
'original_title': movie.get('Title'),
|
||||
'images': {
|
||||
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
|
||||
},
|
||||
@@ -96,14 +96,15 @@ class OMDBAPI(MovieProvider):
|
||||
},
|
||||
'imdb': str(movie.get('imdbID', '')),
|
||||
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
|
||||
'released': movie.get('Released', ''),
|
||||
'released': movie.get('Released'),
|
||||
'year': year if isinstance(year, (int)) else None,
|
||||
'plot': movie.get('Plot', ''),
|
||||
'plot': movie.get('Plot'),
|
||||
'genres': splitString(movie.get('Genre', '')),
|
||||
'directors': splitString(movie.get('Director', '')),
|
||||
'writers': splitString(movie.get('Writer', '')),
|
||||
'actors': splitString(movie.get('Actors', '')),
|
||||
}
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
except:
|
||||
log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class TheMovieDb(MovieProvider):
|
||||
if raw:
|
||||
try:
|
||||
results = self.parseMovie(raw)
|
||||
log.info('Found: %s', results['titles'][0] + ' (' + str(results['year']) + ')')
|
||||
log.info('Found: %s', results['titles'][0] + ' (' + str(results.get('year', 0)) + ')')
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
@@ -81,7 +81,7 @@ class TheMovieDb(MovieProvider):
|
||||
if nr == limit:
|
||||
break
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
@@ -170,11 +170,12 @@ class TheMovieDb(MovieProvider):
|
||||
'runtime': movie.get('runtime'),
|
||||
'released': movie.get('released'),
|
||||
'year': year,
|
||||
'plot': movie.get('overview', ''),
|
||||
'tagline': '',
|
||||
'plot': movie.get('overview'),
|
||||
'genres': genres,
|
||||
}
|
||||
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
|
||||
# Add alternative names
|
||||
for alt in ['original_name', 'alternative_name']:
|
||||
alt_name = toUnicode(movie.get(alt))
|
||||
|
||||
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import json
|
||||
import traceback
|
||||
|
||||
|
||||
45
couchpotato/core/providers/torrent/hdbits/__init__.py
Normal file
45
couchpotato/core/providers/torrent/hdbits/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from .main import HDBits
|
||||
|
||||
def start():
|
||||
return HDBits()
|
||||
|
||||
config = [{
|
||||
'name': 'hdbits',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'HDBits',
|
||||
'description': 'See <a href="http://hdbits.org">HDBits</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
55
couchpotato/core/providers/torrent/hdbits/main.py
Normal file
55
couchpotato/core/providers/torrent/hdbits/main.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class HDBits(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'https://hdbits.org/',
|
||||
'login' : 'https://hdbits.org/login/doLogin/',
|
||||
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
|
||||
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
||||
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
data = self.getJsonData(self.urls['search'] % movie['library']['identifier'], opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
try:
|
||||
for result in data:
|
||||
results.append({
|
||||
'id': result['id'],
|
||||
'name': result['title'],
|
||||
'url': self.urls['download'] % (result['title'], result['id'], self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % result['id'],
|
||||
'size': self.parseSize(result['size']),
|
||||
'seeders': tryInt(result['seeder']),
|
||||
'leechers': tryInt(result['leecher'])
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
data = self.getHTMLData('https://hdbits.org/login')
|
||||
bs = BeautifulSoup(data)
|
||||
secret = bs.find('input', attrs = {'name': 'lol'})['value']
|
||||
|
||||
return tryUrlencode({
|
||||
'uname': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'lol': secret
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/logout.php' in output.lower()
|
||||
@@ -38,6 +38,30 @@ config = [{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'prefer_golden',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'label': 'Prefer golden',
|
||||
'default': 1,
|
||||
'description': 'Favors Golden Popcorn-releases over all other releases.'
|
||||
},
|
||||
{
|
||||
'name': 'prefer_scene',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'label': 'Prefer scene',
|
||||
'default': 0,
|
||||
'description': 'Favors scene-releases over non-scene releases.'
|
||||
},
|
||||
{
|
||||
'name': 'require_approval',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'label': 'Require approval',
|
||||
'default': 0,
|
||||
'description': 'Require staff-approval for releases to be accepted.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
|
||||
@@ -58,7 +58,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
|
||||
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||
def http_error_302(self, req, fp, code, msg, headers):
|
||||
log.debug("302 detected; redirected to %s" % headers['Location'])
|
||||
log.debug("302 detected; redirected to %s", headers['Location'])
|
||||
if (headers['Location'] != 'login.php'):
|
||||
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
|
||||
else:
|
||||
@@ -84,7 +84,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
txt = self.urlopen(url, opener = self.login_opener)
|
||||
res = json.loads(txt)
|
||||
except:
|
||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
|
||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)', params)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -96,18 +96,23 @@ class PassThePopcorn(TorrentProvider):
|
||||
|
||||
for ptpmovie in res['Movies']:
|
||||
if not 'Torrents' in ptpmovie:
|
||||
log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year']))
|
||||
log.debug('Movie %s (%s) has NO torrents', (ptpmovie['Title'], ptpmovie['Year']))
|
||||
continue
|
||||
|
||||
log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
|
||||
log.debug('Movie %s (%s) has %d torrents', (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
|
||||
for torrent in ptpmovie['Torrents']:
|
||||
torrent_id = tryInt(torrent['Id'])
|
||||
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec'])
|
||||
torrentscore = 0
|
||||
|
||||
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
|
||||
torrentdesc += ' HQ'
|
||||
if self.conf('prefer_golden'):
|
||||
torrentscore += 200
|
||||
if 'Scene' in torrent and torrent['Scene']:
|
||||
torrentdesc += ' Scene'
|
||||
if self.conf('prefer_scene'):
|
||||
torrentscore += 50
|
||||
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
|
||||
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
|
||||
|
||||
@@ -115,18 +120,21 @@ class PassThePopcorn(TorrentProvider):
|
||||
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc))
|
||||
|
||||
def extra_check(item):
|
||||
return self.torrentMeetsQualitySpec(item, type)
|
||||
return self.torrentMeetsQualitySpec(item, quality_id)
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'Source': torrent['Source'],
|
||||
'Checked': 'true' if torrent['Checked'] else 'false',
|
||||
'Resolution': torrent['Resolution'],
|
||||
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
|
||||
'size': tryInt(torrent['Size']) / (1024 * 1024),
|
||||
'seeders': tryInt(torrent['Seeders']),
|
||||
'leechers': tryInt(torrent['Leechers']),
|
||||
'score': 50 if torrent['GoldenPopcorn'] else 0,
|
||||
'score': torrentscore,
|
||||
'extra_check': extra_check,
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
@@ -151,7 +159,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
try:
|
||||
response = opener.open(self.urls['login'], self.getLoginParams())
|
||||
except urllib2.URLError as e:
|
||||
log.error('Login to PassThePopcorn failed: %s' % e)
|
||||
log.error('Login to PassThePopcorn failed: %s', e)
|
||||
return False
|
||||
|
||||
if response.getcode() == 200:
|
||||
@@ -159,7 +167,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
self.login_opener = opener
|
||||
return True
|
||||
else:
|
||||
log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode())
|
||||
log.error('Login to PassThePopcorn failed: returned code %d', response.getcode())
|
||||
return False
|
||||
|
||||
def torrentMeetsQualitySpec(self, torrent, quality):
|
||||
@@ -167,12 +175,18 @@ class PassThePopcorn(TorrentProvider):
|
||||
if not quality in self.post_search_filters:
|
||||
return True
|
||||
|
||||
for field, specs in self.post_search_filters[quality].items():
|
||||
reqs = self.post_search_filters[quality].copy()
|
||||
|
||||
if self.conf('require_approval'):
|
||||
log.debug('Config: Require staff-approval activated')
|
||||
reqs['Checked'] = ['true']
|
||||
|
||||
for field, specs in reqs.items():
|
||||
matches_one = False
|
||||
seen_one = False
|
||||
|
||||
if not field in torrent:
|
||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality))
|
||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['Id'], field, quality))
|
||||
continue
|
||||
|
||||
for spec in specs:
|
||||
@@ -182,11 +196,14 @@ class PassThePopcorn(TorrentProvider):
|
||||
return False
|
||||
else:
|
||||
# a positive rule; if any of the possible positive values match the field, return True
|
||||
log.debug('Checking if torrents field %s equals %s' % (field, spec))
|
||||
seen_one = True
|
||||
if torrent[field] == spec:
|
||||
log.debug('Torrent satisfied %s == %s' % (field, spec))
|
||||
matches_one = True
|
||||
|
||||
if seen_one and not matches_one:
|
||||
log.debug('Torrent did not satisfy requirements, ignoring')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -35,8 +35,11 @@ class ThePirateBay(TorrentMagnetProvider):
|
||||
'https://piratereverse.info',
|
||||
'https://tpb.pirateparty.org.uk',
|
||||
'https://argumentomteemigreren.nl',
|
||||
'https://livepirate.com/',
|
||||
'https://www.getpirate.com/',
|
||||
'https://livepirate.com',
|
||||
'https://www.getpirate.com',
|
||||
'https://tpb.partipirate.org',
|
||||
'https://tpb.piraten.lu',
|
||||
'https://kuiken.co',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
@@ -50,7 +53,7 @@ class ThePirateBay(TorrentMagnetProvider):
|
||||
|
||||
while page < total_pages:
|
||||
|
||||
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s %s"' % (title, movie['library']['year'])), page, self.getCatId(quality['identifier'])[0])
|
||||
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['library']['year'])), page, self.getCatId(quality['identifier'])[0])
|
||||
page += 1
|
||||
|
||||
data = self.getHTMLData(search_url)
|
||||
|
||||
47
couchpotato/core/providers/torrent/torrentshack/__init__.py
Normal file
47
couchpotato/core/providers/torrent/torrentshack/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from .main import TorrentShack
|
||||
|
||||
def start():
|
||||
return TorrentShack()
|
||||
|
||||
config = [{
|
||||
'name': 'torrentshack',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentShack',
|
||||
'description': 'See <a href="http://www.torrentshack.net/">TorrentShack</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'scene_only',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Only allow scene releases.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
81
couchpotato/core/providers/torrent/torrentshack/main.py
Normal file
81
couchpotato/core/providers/torrent/torrentshack/main.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class TorrentShack(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'http://www.torrentshack.net/',
|
||||
'login' : 'http://www.torrentshack.net/login.php',
|
||||
'detail' : 'http://www.torrentshack.net/torrent/%s',
|
||||
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
||||
'download' : 'http://www.torrentshack.net/%s',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([970], ['bd50']),
|
||||
([300], ['720p', '1080p']),
|
||||
([350], ['dvdr']),
|
||||
([400], ['brrip', 'dvdrip']),
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
url = self.urls['search'] % (tryUrlencode('"%s" %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id' : 'torrent_table'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr', attrs = {'class' : 'torrent'})
|
||||
|
||||
for result in entries:
|
||||
|
||||
link = result.find('span', attrs = {'class' : 'torrent_name_link'}).parent
|
||||
url = result.find('td', attrs = {'class' : 'torrent_td'}).find('a')
|
||||
|
||||
extra_info = ''
|
||||
if result.find('span', attrs = {'class' : 'torrent_extra_info'}):
|
||||
extra_info = result.find('span', attrs = {'class' : 'torrent_extra_info'}).text
|
||||
|
||||
if not self.conf('scene_only') or extra_info != '[NotScene]':
|
||||
results.append({
|
||||
'id': link['href'].replace('torrents.php?torrentid=', ''),
|
||||
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find_all('td')[6].string),
|
||||
'leechers': tryInt(result.find_all('td')[7].string),
|
||||
})
|
||||
else:
|
||||
log.info('Not adding release %s [NotScene]' % unicode(link.span.string).translate({ord(u'\xad'): None}))
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'keeplogged': '1',
|
||||
'login': 'Login',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
@@ -4,7 +4,9 @@ import re
|
||||
|
||||
class Filmweb(UserscriptBase):
|
||||
|
||||
includes = ['http://www.filmweb.pl/*']
|
||||
version = 2
|
||||
|
||||
includes = ['http://www.filmweb.pl/film/*']
|
||||
|
||||
def getMovie(self, url):
|
||||
|
||||
|
||||
@@ -15,19 +15,21 @@ Block.Menu = new Class({
|
||||
self.wrapper = new Element('div.wrapper').adopt(
|
||||
self.more_option_ul = new Element('ul')
|
||||
),
|
||||
new Element('a.button.onlay' + (self.options.button_class ? '.' + self.options.button_class : ''), {
|
||||
self.button = new Element('a.button' + (self.options.button_class ? '.' + self.options.button_class : ''), {
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.el.toggleClass('show')
|
||||
self.fireEvent(self.el.hasClass('show') ? 'open' : 'close')
|
||||
|
||||
if(self.el.hasClass('show'))
|
||||
this.addEvent('outerClick', function(){
|
||||
self.el.removeClass('show')
|
||||
this.removeEvents('outerClick');
|
||||
if(self.el.hasClass('show')){
|
||||
self.el.addEvent('outerClick', self.removeOuterClick.bind(self))
|
||||
this.addEvent('outerClick', function(e){
|
||||
if(e.target.get('tag') != 'input')
|
||||
self.removeOuterClick()
|
||||
})
|
||||
}
|
||||
else
|
||||
this.removeEvents('outerClick');
|
||||
self.removeOuterClick()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -36,6 +38,15 @@ Block.Menu = new Class({
|
||||
|
||||
},
|
||||
|
||||
removeOuterClick: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.removeClass('show')
|
||||
self.el.removeEvents('outerClick');
|
||||
|
||||
self.button.removeEvents('outerClick');
|
||||
},
|
||||
|
||||
addLink: function(tab, position){
|
||||
var self = this;
|
||||
var el = new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom');
|
||||
|
||||
@@ -67,12 +67,12 @@ Block.Navigation = new Class({
|
||||
if(!self.added){
|
||||
|
||||
new Element('li.separator').inject(self.nav);
|
||||
body.getElements('.header .more_menu.menu li a').each(function(el, nr){
|
||||
if([0, 1, 2, 5].indexOf(nr) > -1){
|
||||
self.nav.grab(
|
||||
new Element('li').grab(el.clone().cloneEvents(el))
|
||||
);
|
||||
}
|
||||
body.getElements('.header .more_menu.menu li a, .header .more_menu.menu li span.separator').each(function(el, nr){
|
||||
if(nr <= 2) return;
|
||||
if(el.get('tag') == 'a')
|
||||
self.nav.grab(new Element('li').grab(el.clone().cloneEvents(el)));
|
||||
else
|
||||
self.nav.grab(new Element('li.separator'));
|
||||
});
|
||||
|
||||
self.added = true;
|
||||
|
||||
@@ -30,6 +30,12 @@ var CouchPotato = new Class({
|
||||
History.addEvent('change', self.openPage.bind(self));
|
||||
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
|
||||
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
|
||||
|
||||
// Check if device is touchenabled
|
||||
self.touch_device = 'ontouchstart' in document.documentElement;
|
||||
if(self.touch_device)
|
||||
self.c.addClass('touch_enabled');
|
||||
|
||||
},
|
||||
|
||||
getOption: function(name){
|
||||
@@ -68,28 +74,42 @@ var CouchPotato = new Class({
|
||||
self.block.footer = new Block.Footer(self, {})
|
||||
);
|
||||
|
||||
[new Element('a.orange', {
|
||||
'text': 'Restart',
|
||||
'events': {
|
||||
'click': self.restartQA.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.red', {
|
||||
'text': 'Shutdown',
|
||||
'events': {
|
||||
'click': self.shutdownQA.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a', {
|
||||
'text': 'Update to latest',
|
||||
'events': {
|
||||
'click': self.checkForUpdate.bind(self, null)
|
||||
}
|
||||
}),
|
||||
new Element('a', {
|
||||
'text': 'Run install wizard',
|
||||
'href': App.createUrl('wizard')
|
||||
})].each(function(a){
|
||||
var setting_links = [
|
||||
new Element('a', {
|
||||
'text': 'About CouchPotato',
|
||||
'href': App.createUrl('settings/about')
|
||||
}),
|
||||
new Element('a', {
|
||||
'text': 'Check for Updates',
|
||||
'events': {
|
||||
'click': self.checkForUpdate.bind(self, null)
|
||||
}
|
||||
}),
|
||||
new Element('span.separator'),
|
||||
new Element('a', {
|
||||
'text': 'Settings',
|
||||
'href': App.createUrl('settings/general')
|
||||
}),
|
||||
new Element('a', {
|
||||
'text': 'Logs',
|
||||
'href': App.createUrl('log')
|
||||
}),
|
||||
new Element('span.separator'),
|
||||
new Element('a', {
|
||||
'text': 'Restart',
|
||||
'events': {
|
||||
'click': self.restartQA.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a', {
|
||||
'text': 'Shutdown',
|
||||
'events': {
|
||||
'click': self.shutdownQA.bind(self)
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
setting_links.each(function(a){
|
||||
self.block.more.addLink(a)
|
||||
})
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ var AboutSettingTab = new Class({
|
||||
var self = this;
|
||||
var date = new Date(json.version.date * 1000);
|
||||
self.version_text.set('text', json.version.hash + (json.version.date ? ' ('+date.toLocaleString()+')' : ''));
|
||||
self.updater_type.set('text', json.version.type + ', ' + json.branch);
|
||||
self.updater_type.set('text', (json.version.type != json.branch) ? (json.version.type + ', ' + json.branch) : json.branch);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -39,7 +39,8 @@ Page.Home = new Class({
|
||||
),
|
||||
'filter': {
|
||||
'release_status': 'snatched,available'
|
||||
}
|
||||
},
|
||||
'limit': null
|
||||
});
|
||||
|
||||
// Coming Soon
|
||||
@@ -65,31 +66,37 @@ Page.Home = new Class({
|
||||
|
||||
// Make all thumbnails the same size
|
||||
self.soon_list.addEvent('loaded', function(){
|
||||
var images = $(self.soon_list).getElements('img'),
|
||||
var images = $(self.soon_list).getElements('.poster'),
|
||||
timer,
|
||||
lowest = null;
|
||||
highest = 100;
|
||||
|
||||
images.addEvent('load', function(){
|
||||
var height = this.getSize().y;
|
||||
if(!lowest || lowest > height){
|
||||
lowest = height;
|
||||
if(timer) clearTimeout(timer);
|
||||
timer = (function(){
|
||||
images.getParent().setStyle('height', lowest);
|
||||
}).delay(300)
|
||||
}
|
||||
images.each(function(img_container){
|
||||
img_container.getElements('img').addEvent('load', function(){
|
||||
var img = this,
|
||||
height = img.getSize().y;
|
||||
if(!highest || highest < height){
|
||||
highest = height;
|
||||
if(timer) clearTimeout(timer);
|
||||
timer = (function(){
|
||||
images.setStyle('height', highest);
|
||||
}).delay(50);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(window).addEvent('resize', function(){
|
||||
if(timer) clearTimeout(timer);
|
||||
timer = (function(){
|
||||
var lowest;
|
||||
images.each(function(img){
|
||||
var highest = 100;
|
||||
images.each(function(img_container){
|
||||
var img = img_container.getElement('img');
|
||||
if(!img) return
|
||||
|
||||
var height = img.getSize().y;
|
||||
if(!lowest || lowest > height)
|
||||
lowest = height;
|
||||
if(!highest || highest < height)
|
||||
highest = height;
|
||||
});
|
||||
images.getParent().setStyle('height', lowest);
|
||||
images.setStyle('height', highest);
|
||||
}).delay(300);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,20 +11,6 @@ Page.Settings = new Class({
|
||||
current: 'about',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options);
|
||||
|
||||
// Add to more menu
|
||||
if(self.name == 'settings')
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}), 'top')
|
||||
|
||||
},
|
||||
|
||||
open: function(action, params){
|
||||
var self = this;
|
||||
self.action = action == 'index' ? self.default_action : action;
|
||||
@@ -876,7 +862,7 @@ Option.Directories = new Class({
|
||||
$(dir).addClass('is_empty');
|
||||
|
||||
// Add remove button
|
||||
new Element('a.icon.delete', {
|
||||
new Element('a.icon2.delete', {
|
||||
'events': {
|
||||
'click': self.delItem.bind(self, dir)
|
||||
}
|
||||
@@ -1385,7 +1371,7 @@ Option.Combined = new Class({
|
||||
|
||||
item[value_empty == value_count ? 'addClass' : 'removeClass']('is_empty');
|
||||
|
||||
new Element('a.icon.delete', {
|
||||
new Element('a.icon2.delete', {
|
||||
'events': {
|
||||
'click': self.deleteCombinedItem.bind(self)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #4e5969;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
body { overflow-y: scroll; }
|
||||
body.noscroll { overflow: hidden; }
|
||||
@@ -34,11 +35,21 @@ input:focus, textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:-moz-placeholder, textarea:-moz-placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
::-moz-placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
::-webkit-input-placeholder, ::-webkit-textarea-placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
input:-moz-placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
::-webkit-input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a img {
|
||||
@@ -108,7 +119,7 @@ body > .spinner, .mask{
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x;
|
||||
background: #5082bc;
|
||||
padding: 5px 10px 6px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
@@ -154,6 +165,7 @@ body > .spinner, .mask{
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 15px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.icon2.cog:before { content: "\e109"; }
|
||||
@@ -161,6 +173,30 @@ body > .spinner, .mask{
|
||||
.icon2.search:before { content: "\e03e"; }
|
||||
.icon2.return-key:before { content: "\e111"; }
|
||||
.icon2.close:before { content: "\e04e"; }
|
||||
.icon2.trailer:before { content: "\e0e9"; }
|
||||
.icon2.download:before { content: "\e0c3"; }
|
||||
.icon2.edit:before { content: "\e068"; }
|
||||
.icon2.refresh:before { content: "\e04f"; font-weight: bold; }
|
||||
.icon2.delete:before { content: "\e04e"; }
|
||||
.icon2.directory:before { content: "\e097"; }
|
||||
.icon2.completed:before { content: "\e070"; }
|
||||
.icon2.info:before { content: "\e089"; }
|
||||
.icon2.attention:before { content: "\e009"; }
|
||||
.icon2.readd:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
.icon2.imdb:before {
|
||||
content: "IMDb";
|
||||
color: #444;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: #fbec98;
|
||||
font: bold 8px Arial;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
.icon2.menu:before {
|
||||
content: "\e076 \e076 \e076";
|
||||
line-height: 6px;
|
||||
@@ -178,13 +214,10 @@ body > .spinner, .mask{
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
background: #4e5969;
|
||||
background: #5c697b;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
transition: all .4s ease-in-out;
|
||||
}
|
||||
.header.with_shadow {
|
||||
background-color: #46505e;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.header {
|
||||
@@ -315,7 +348,7 @@ body > .spinner, .mask{
|
||||
}
|
||||
|
||||
.menu_shown body {
|
||||
left: 160px;
|
||||
left: 240px;
|
||||
}
|
||||
|
||||
.header .navigation {
|
||||
@@ -327,13 +360,13 @@ body > .spinner, .mask{
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 160px;
|
||||
left: 240px;
|
||||
}
|
||||
|
||||
.header .navigation ul {
|
||||
width: 160px;
|
||||
width: 240px;
|
||||
position: fixed;
|
||||
left: -160px;
|
||||
left: -240px;
|
||||
background: rgba(0,0,0,.5);
|
||||
transition: all .5s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
@@ -374,26 +407,15 @@ body > .spinner, .mask{
|
||||
|
||||
.header .more_menu .button {
|
||||
height: 100%;
|
||||
width: 44px;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
line-height: 66px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-width: 0 0 4px;
|
||||
}
|
||||
.header .more_menu .button:hover {
|
||||
background: none;
|
||||
border-color: #047792;
|
||||
}
|
||||
|
||||
.header .more_menu .wrapper {
|
||||
width: 150px;
|
||||
width: 200px;
|
||||
margin-left: -106px;
|
||||
margin-top: 66px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
@@ -402,7 +424,7 @@ body > .spinner, .mask{
|
||||
}
|
||||
|
||||
.header .more_menu .wrapper {
|
||||
margin-top: 44px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,13 +461,20 @@ body > .spinner, .mask{
|
||||
}
|
||||
|
||||
.header .notification_menu ul {
|
||||
min-height: 60px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header .notification_menu > a {
|
||||
background-position: center -209px;
|
||||
}
|
||||
.header .notification_menu ul:empty:after {
|
||||
content: 'No notifications (yet)';
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding: 18px 0;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.header .notification_menu li > span {
|
||||
padding: 5px;
|
||||
@@ -491,6 +520,7 @@ body > .spinner, .mask{
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
background: url('../images/sprite.png') no-repeat -200px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.check.highlighted { background-color: #424c59; }
|
||||
.check.checked { background-position: -2px 0; }
|
||||
@@ -508,14 +538,7 @@ body > .spinner, .mask{
|
||||
.select .selection {
|
||||
display: inline-block;
|
||||
padding: 0 30px 0 20px;
|
||||
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(
|
||||
180deg,
|
||||
#5b9bd1 0%,
|
||||
#406db8 100%
|
||||
);
|
||||
background: #5b9bd1 url('../images/sprite.png') no-repeat 94% -53px;
|
||||
}
|
||||
|
||||
.select .selection .selectionDisplay {
|
||||
@@ -535,11 +558,9 @@ body > .spinner, .mask{
|
||||
.select .list {
|
||||
display: none;
|
||||
background: #282d34;
|
||||
border: 1px solid #1f242b;
|
||||
position: absolute;
|
||||
margin: 28px 0 0 0;
|
||||
margin: 25px 0 0 0;
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.4);
|
||||
border-radius:3px;
|
||||
z-index: 3;
|
||||
}
|
||||
.select.active .list {
|
||||
@@ -566,15 +587,14 @@ body > .spinner, .mask{
|
||||
.inlay {
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius:3px;
|
||||
background-color: #282d34;
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25);
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.inlay.light {
|
||||
background-color: #47515f;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.inlay:focus {
|
||||
@@ -585,7 +605,7 @@ body > .spinner, .mask{
|
||||
.onlay, .inlay .selected, .inlay:not(.reversed) > li:hover, .inlay > li.active, .inlay.reversed > li {
|
||||
border-radius:3px;
|
||||
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);
|
||||
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20);
|
||||
background: rgb(55,62,74);
|
||||
background-image: linear-gradient(
|
||||
0,
|
||||
@@ -597,7 +617,7 @@ body > .spinner, .mask{
|
||||
color: #fff;
|
||||
border: 1px solid transparent;
|
||||
background-color: #282d34;
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25);
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.question {
|
||||
@@ -648,38 +668,37 @@ body > .spinner, .mask{
|
||||
|
||||
.more_menu > a {
|
||||
display: block;
|
||||
background: url('../images/sprite.png') no-repeat center -137px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
border: 1px solid rgba(0,0,0,0.3);
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
.more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) {
|
||||
background-color: #406db8;
|
||||
|
||||
.more_menu .button:hover {
|
||||
border-color: #047792;
|
||||
}
|
||||
|
||||
.more_menu.show .button {
|
||||
border-color: #04bce6;
|
||||
}
|
||||
|
||||
.more_menu .wrapper {
|
||||
display: none;
|
||||
background: rgba(255,255,255,0.98);
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
margin: 26px 0 0 0;
|
||||
margin: 11px 0 0 0;
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
width: 185px;
|
||||
box-shadow: 0 20px 20px -5px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
color: #000;
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
rgb(200,200,200) 0%,
|
||||
rgb(255,255,255) 100%
|
||||
);
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||
color: #444;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.more_menu.show .wrapper {
|
||||
display: block;
|
||||
top: 44px;
|
||||
}
|
||||
|
||||
.more_menu ul {
|
||||
@@ -698,11 +717,19 @@ body > .spinner, .mask{
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
box-shadow: none;
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2em;
|
||||
letter-spacing: 1px;
|
||||
padding: 3px 0;
|
||||
color: #000;
|
||||
padding: 2px 10px;
|
||||
color: #444;
|
||||
}
|
||||
.more_menu .wrapper li:first-child a { padding-top: 5px; }
|
||||
.more_menu .wrapper li:last-child a { padding-bottom: 5px; }
|
||||
|
||||
.more_menu .wrapper li .separator {
|
||||
border-bottom: 1px solid rgba(0,0,0,.1);
|
||||
display: block;
|
||||
height: 1;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.more_menu .wrapper li:last-child a {
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
display: block;
|
||||
text-align: right;
|
||||
height: 20px;
|
||||
margin: 0 0 -37px;
|
||||
margin: 0 0 -38px;
|
||||
}
|
||||
.page .advanced_toggle .check {
|
||||
margin: 0;
|
||||
@@ -262,18 +262,24 @@
|
||||
color: #edc07f;
|
||||
}
|
||||
|
||||
.page .directory {
|
||||
.page form .directory {
|
||||
display: inline-block;
|
||||
padding: 0 4% 0 4px;
|
||||
font-size: 13px;
|
||||
width: 30%;
|
||||
background-image: url('../images/icon.folder.gif');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 97% center;
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
}
|
||||
.page .directory > span {
|
||||
.page form .directory:after {
|
||||
content: "\e097";
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 2px;
|
||||
font-family: 'Elusive-Icons';
|
||||
color: #f5e39c;
|
||||
}
|
||||
.page form .directory > span {
|
||||
height: 25px;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
@@ -281,15 +287,19 @@
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page form .directory span:empty:before {
|
||||
content: 'No folder selected';
|
||||
font-style: italic;
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.page .directory_list {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 450px;
|
||||
margin: 28px 0 20px 18%;
|
||||
margin: 28px 0 20px 18.4%;
|
||||
background: #5c697b;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||
box-shadow: 0 20px 40px -20px rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
.page .directory_list .pointer {
|
||||
@@ -299,7 +309,7 @@
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
margin: -6px 0 0 22%;
|
||||
margin: -6px 0 0 45%;
|
||||
}
|
||||
|
||||
.page .directory_list ul {
|
||||
@@ -311,11 +321,14 @@
|
||||
}
|
||||
|
||||
.page .directory_list li {
|
||||
padding: 4px 10px;
|
||||
padding: 4px 30px 4px 10px;
|
||||
cursor: pointer;
|
||||
margin: 0 !important;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
background: url('../images/right.arrow.png') no-repeat 98% center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.page .directory_list li:last-child {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
@@ -341,7 +354,10 @@
|
||||
.page .directory_list .actions {
|
||||
clear: both;
|
||||
padding: 4% 4% 2%;
|
||||
min-height: 25px;
|
||||
min-height: 45px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page .directory_list .actions label {
|
||||
@@ -350,7 +366,7 @@
|
||||
padding: 0;
|
||||
}
|
||||
.page .directory_list .actions .inlay {
|
||||
margin: -2px 0 0 7px;
|
||||
margin: 0 0 0 7px;
|
||||
}
|
||||
|
||||
.page .directory_list .actions .back {
|
||||
@@ -360,6 +376,9 @@
|
||||
padding: 0;
|
||||
line-height: 120%;
|
||||
vertical-align: top;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
left: 4%;
|
||||
}
|
||||
|
||||
.page .directory_list .actions:last-child {
|
||||
@@ -373,10 +392,10 @@
|
||||
}
|
||||
|
||||
.page .directory_list .actions:last-child > .clear {
|
||||
left: -90%;
|
||||
position: relative;
|
||||
background-color: #af3128;
|
||||
}
|
||||
left: 4%;
|
||||
position: absolute;
|
||||
background-color: #af3128;
|
||||
}
|
||||
|
||||
.page .directory_list .actions:last-child > .cancel {
|
||||
font-weight: bold;
|
||||
@@ -563,8 +582,10 @@
|
||||
display: none;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background-position: left center;
|
||||
color: #fe3d3d;
|
||||
}
|
||||
.page .combined_table .ctrlHolder:hover .delete {
|
||||
display: inline-block;
|
||||
@@ -576,7 +597,8 @@
|
||||
|
||||
.page .tab_about .usenet {
|
||||
padding: 20px 30px 0;
|
||||
font-size: 17px;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.page .tab_about .usenet a {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "CouchPotato"
|
||||
#define MyAppVer "2.0.8"
|
||||
#define MyAppVer "2.0.8.1"
|
||||
|
||||
[Setup]
|
||||
AppName={#MyAppName}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# CSSPrefixer
|
||||
# Copyright 2010-2012 Greg V. <floatboth@me.com>
|
||||
|
||||
# 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')
|
||||
@@ -1,117 +0,0 @@
|
||||
# CSSPrefixer
|
||||
# Copyright 2010-2012 Greg V. <floatboth@me.com>
|
||||
|
||||
# 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']
|
||||
@@ -1,271 +0,0 @@
|
||||
# CSSPrefixer
|
||||
# Copyright 2010-2012 Greg V. <floatboth@me.com>
|
||||
|
||||
# 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,
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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 <http://www.w3.org/TR/CSS2/>`__
|
||||
General CSS rules and properties are defined here
|
||||
`CSS 2.1 Errata <http://www.w3.org/Style/css2-updates/CR-CSS21-20070719-errata.html>`__
|
||||
A few errata, mainly the definition of CHARSET_SYM tokens
|
||||
`CSS3 Module: Syntax <http://www.w3.org/TR/css3-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 <http://www.w3.org/TR/css3-mediaqueries/>`__
|
||||
MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in
|
||||
@import and @media rules.
|
||||
`Namespaces <http://dev.w3.org/csswg/css3-namespace/>`__
|
||||
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 <http://www.w3.org/TR/css3-page/>`__
|
||||
Most properties of this spec are implemented including MarginRules
|
||||
`Selectors <http://www.w3.org/TR/css3-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 <http://www.w3.org/TR/DOM-Level-2-Style/css.html>`__
|
||||
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 <http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html>`__
|
||||
DOM for package stylesheets
|
||||
`CSSOM <http://dev.w3.org/csswg/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) <http://www.w3.org/TR/css3-syntax/>`__ 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__
|
||||
@@ -1,584 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,608 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,44 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,68 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,80 +0,0 @@
|
||||
"""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 *
|
||||
@@ -1,184 +0,0 @@
|
||||
# -*- 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),
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
"""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"<cssutils.css.%s object encoding=%r at 0x%x>" % (
|
||||
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))
|
||||
@@ -1,87 +0,0 @@
|
||||
"""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"<cssutils.css.%s object cssText=%r at 0x%x>" % (
|
||||
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)
|
||||
@@ -1,184 +0,0 @@
|
||||
"""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"<cssutils.css.%s object style=%r valid=%r at 0x%x>" % (
|
||||
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)
|
||||
@@ -1,396 +0,0 @@
|
||||
"""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"<cssutils.css.%s object href=%r mediaText=%r name=%r at 0x%x>"\
|
||||
% (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)
|
||||
@@ -1,302 +0,0 @@
|
||||
"""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"<cssutils.css.%s object mediaText=%r at 0x%x>" % (
|
||||
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)
|
||||
@@ -1,295 +0,0 @@
|
||||
"""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"<cssutils.css.%s object namespaceURI=%r prefix=%r at 0x%x>" % (
|
||||
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)
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
"""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"<cssutils.css.%s object selectorText=%r specificity=%r "+
|
||||
u"style=%r cssRules=%r at 0x%x>") % (
|
||||
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)
|
||||
@@ -1,122 +0,0 @@
|
||||
"""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)))
|
||||
@@ -1,304 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,53 +0,0 @@
|
||||
"""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
|
||||
@@ -1,697 +0,0 @@
|
||||
"""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
|
||||
<BLANKLINE>
|
||||
|
||||
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"<cssutils.css.%s object length=%r (all: %r) at 0x%x>" % (
|
||||
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
|
||||
<BLANKLINE>
|
||||
|
||||
"""
|
||||
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!")
|
||||
@@ -1,234 +0,0 @@
|
||||
"""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"<cssutils.css.%s object selectorText=%r style=%r _namespaces=%r "\
|
||||
u"at 0x%x>" % (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)
|
||||
@@ -1,804 +0,0 @@
|
||||
"""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 "<cssutils.css.%s object encoding=%r href=%r "\
|
||||
"media=%r title=%r namespaces=%r at 0x%x>" % (
|
||||
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)
|
||||
@@ -1,209 +0,0 @@
|
||||
"""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"<cssutils.css.%s object cssText=%r at 0x%x>" % (
|
||||
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))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,330 +0,0 @@
|
||||
"""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"<cssutils.css.%s object length=%r at 0x%x>" % (
|
||||
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.")
|
||||
@@ -1,198 +0,0 @@
|
||||
"""CSSVariables implements (and only partly) experimental
|
||||
`CSS Variables <http://disruptive-innovations.com/zoo/cssvariables/>`_
|
||||
"""
|
||||
__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"<cssutils.css.%s object mediaText=%r variables=%r valid=%r " \
|
||||
u"at 0x%x>" % (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)
|
||||
@@ -1,215 +0,0 @@
|
||||
"""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"<cssutils.css.%s object margin=%r style=%r "\
|
||||
u"at 0x%x>" % (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))
|
||||
|
||||
@@ -1,510 +0,0 @@
|
||||
"""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.")
|
||||
@@ -1,813 +0,0 @@
|
||||
"""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"<cssutils.css.%s object selectorText=%r specificity=%r" \
|
||||
u" _namespaces=%r at 0x%x>" % (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")
|
||||
@@ -1,234 +0,0 @@
|
||||
"""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"<cssutils.css.%s object selectorText=%r _namespaces=%r at " \
|
||||
u"0x%x>" % (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)))
|
||||
|
||||
@@ -1,871 +0,0 @@
|
||||
"""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"<cssutils.css.%s object length=%r cssText=%r at "\
|
||||
u"0x%x>" % (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"<cssutils.css.%s object type=%s value=%r cssText=%r at 0x%x>"\
|
||||
% (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"<cssutils.css.%s object type=%s value=%r colorType=%r "\
|
||||
u"red=%s blue=%s green=%s alpha=%s at 0x%x>"\
|
||||
% (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"<cssutils.css.%s object type=%s value=%r dimension=%r cssText=%r at 0x%x>"\
|
||||
% (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"<cssutils.css.%s object type=%s value=%r uri=%r cssText=%r at 0x%x>"\
|
||||
% (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"<cssutils.css.%s object name=%r value=%r at 0x%x>" % (
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,131 +0,0 @@
|
||||
"""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 CDO;}
|
||||
('CDC', 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])
|
||||
@@ -1,124 +0,0 @@
|
||||
"""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\..+\('
|
||||
)
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/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__
|
||||
@@ -1,137 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,232 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,733 +0,0 @@
|
||||
# -*- 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 "<cssutils.prodsparser.%s object name=%r at 0x%x>" % (
|
||||
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
|
||||
)
|
||||
@@ -1,791 +0,0 @@
|
||||
"""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<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():
|
||||
# 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',
|
||||
|
||||
#[[ <family-name> | <generic-family> ] [, <family-name>| <generic-family>]* ] | 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})*',
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
#!/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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user