diff --git a/couchpotato/api.py b/couchpotato/api.py
index 718527c9..58fd3102 100644
--- a/couchpotato/api.py
+++ b/couchpotato/api.py
@@ -11,16 +11,12 @@ api_nonblock = {}
class NonBlockHandler(RequestHandler):
- def __init__(self, application, request, **kwargs):
- cls = NonBlockHandler
- cls.stoppers = []
- super(NonBlockHandler, self).__init__(application, request, **kwargs)
+ stoppers = []
@asynchronous
def get(self, route):
- cls = NonBlockHandler
start, stop = api_nonblock[route]
- cls.stoppers.append(stop)
+ self.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
@@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler):
self.finish(response)
def on_connection_close(self):
- cls = NonBlockHandler
- for stop in cls.stoppers:
+ for stop in self.stoppers:
stop(self.onNewMessage)
- cls.stoppers = []
+ self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):
diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py
index c8c3fda6..4d1a6840 100644
--- a/couchpotato/core/_base/_core/__init__.py
+++ b/couchpotato/core/_base/_core/__init__.py
@@ -70,7 +70,7 @@ config = [{
'name': 'development',
'default': 0,
'type': 'bool',
- 'description': 'Disables some checks/downloads for faster reloading.',
+ 'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
},
{
'name': 'data_dir',
diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
index c91140fa..d2b6e2d3 100644
--- a/couchpotato/core/_base/_core/main.py
+++ b/couchpotato/core/_base/_core/main.py
@@ -79,7 +79,7 @@ class Core(Plugin):
def shutdown():
self.initShutdown()
- IOLoop.instance().add_callback(shutdown)
+ IOLoop.current().add_callback(shutdown)
return 'shutdown'
@@ -89,7 +89,7 @@ class Core(Plugin):
def restart():
self.initShutdown(restart = True)
- IOLoop.instance().add_callback(restart)
+ IOLoop.current().add_callback(restart)
return 'restarting'
@@ -128,7 +128,7 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
- IOLoop.instance().stop()
+ IOLoop.current().stop()
except RuntimeError:
pass
except:
diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py
index f2a30f6f..9536463e 100644
--- a/couchpotato/core/_base/clientscript/main.py
+++ b/couchpotato/core/_base/clientscript/main.py
@@ -1,10 +1,11 @@
from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-from minify.cssmin import cssmin
from minify.jsmin import jsmin
+import cssprefixer
import os
import traceback
@@ -23,7 +24,6 @@ class ClientScript(Plugin):
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
- 'scripts/library/prefix_free.js',
'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
@@ -69,7 +69,8 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
- addEvent('app.load', self.minify)
+ if not Env.get('dev'):
+ addEvent('app.load', self.minify)
self.addCore()
@@ -108,8 +109,10 @@ class ClientScript(Plugin):
if file_type == 'script':
data = jsmin(f)
else:
- data = cssmin(f)
+ data = cssprefixer.process(f, debug = False, minify = True)
data = data.replace('../images/', '../static/images/')
+ data = data.replace('../fonts/', '../static/fonts/')
+ data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
@@ -119,7 +122,7 @@ class ClientScript(Plugin):
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n'
- self.createFile(out, data.strip())
+ self.createFile(out, ss(data.strip()))
if not self.minified.get(file_type):
self.minified[file_type] = {}
diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py
index 4102552e..2c97e1b4 100644
--- a/couchpotato/core/_base/scheduler/main.py
+++ b/couchpotato/core/_base/scheduler/main.py
@@ -16,51 +16,19 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval)
- addEvent('schedule.start', self.start)
- addEvent('schedule.restart', self.start)
-
- addEvent('app.load', self.start)
+ addEvent('schedule.remove', self.remove)
self.sched = Sched(misfire_grace_time = 60)
-
- def remove(self, identifier):
- for type in ['interval', 'cron']:
- try:
- self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
- log.debug('%s unscheduled %s', (type.capitalize(), identifier))
- except:
- pass
-
- def start(self):
-
- # Stop all running
- self.stop()
-
- # Crons
- for identifier in self.crons:
- try:
- self.remove(identifier)
- cron = self.crons[identifier]
- job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
- cron['job'] = job
- except ValueError, e:
- log.error('Failed adding cronjob: %s', e)
-
- # Intervals
- for identifier in self.intervals:
- try:
- self.remove(identifier)
- interval = self.intervals[identifier]
- job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
- interval['job'] = job
- except ValueError, e:
- log.error('Failed adding interval cronjob: %s', e)
-
- # Start it
- log.debug('Starting scheduler')
self.sched.start()
self.started = True
- log.debug('Scheduler started')
+
+ def remove(self, identifier):
+ for cron_type in ['intervals', 'crons']:
+ try:
+ self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
+ log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
+ except:
+ pass
def doShutdown(self):
super(Scheduler, self).doShutdown()
@@ -82,6 +50,7 @@ class Scheduler(Plugin):
'day': day,
'hour': hour,
'minute': minute,
+ 'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
}
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
@@ -93,4 +62,5 @@ class Scheduler(Plugin):
'hours': hours,
'minutes': minutes,
'seconds': seconds,
+ 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
}
diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
index 18d2c303..44ceebdd 100644
--- a/couchpotato/core/_base/updater/main.py
+++ b/couchpotato/core/_base/updater/main.py
@@ -15,6 +15,7 @@ import tarfile
import time
import traceback
import version
+import zipfile
log = CPLog(__name__)
@@ -32,7 +33,6 @@ class Updater(Plugin):
else:
self.updater = SourceUpdater()
- fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.autoUpdate)
addEvent('updater.info', self.info)
@@ -52,6 +52,15 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'}
})
+ addEvent('setting.save.updater.enabled.after', self.setCrons)
+
+ def setCrons(self):
+
+ fireEvent('schedule.remove', 'updater.check', single = True)
+ if self.isEnabled():
+ fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
+ self.autoUpdate() # Check after enabling
+
def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
@@ -255,11 +264,11 @@ class SourceUpdater(BaseUpdater):
def doUpdate(self):
try:
- url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
- destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
- extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
+ download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
+ destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
- destination = fireEvent('file.download', url = url, dest = destination, single = True)
+ extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
+ destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
# Cleanup leftover from last time
if os.path.isdir(extracted_path):
@@ -267,9 +276,14 @@ class SourceUpdater(BaseUpdater):
self.makeDir(extracted_path)
# Extract
- tar = tarfile.open(destination)
- tar.extractall(path = extracted_path)
- tar.close()
+ if download_data.get('type') == 'zip':
+ zip = zipfile.ZipFile(destination)
+ zip.extractall(extracted_path)
+ else:
+ tar = tarfile.open(destination)
+ tar.extractall(path = extracted_path)
+ tar.close()
+
os.remove(destination)
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
index cc17be55..0577c783 100644
--- a/couchpotato/core/_base/updater/static/updater.js
+++ b/couchpotato/core/_base/updater/static/updater.js
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
initialize: function(){
var self = this;
- App.addEvent('load', self.info.bind(self, 1000))
+ App.addEvent('load', self.info.bind(self, 2000))
App.addEvent('unload', function(){
if(self.timer)
clearTimeout(self.timer);
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self)
}
})
- ).inject($(document.body).getElement('.header'))
+ ).inject(document.body)
},
doUpdate: function(){
diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py
index 70500dc0..8ccc1369 100644
--- a/couchpotato/core/downloaders/base.py
+++ b/couchpotato/core/downloaders/base.py
@@ -1,5 +1,6 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
import random
@@ -103,6 +104,12 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
+ def downloadReturnId(self, download_id):
+ return {
+ 'downloader': self.getName(),
+ 'id': download_id
+ }
+
def isDisabled(self, manual, data):
return not self.isEnabled(manual, data)
@@ -116,3 +123,35 @@ class Downloader(Provider):
return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type')))
+
+
+class StatusList(list):
+
+ provider = None
+
+ def __init__(self, provider, **kwargs):
+
+ self.provider = provider
+ self.kwargs = kwargs
+
+ super(StatusList, self).__init__()
+
+ def extend(self, results):
+ for r in results:
+ self.append(r)
+
+ def append(self, result):
+ new_result = self.fillResult(result)
+ super(StatusList, self).append(new_result)
+
+ def fillResult(self, result):
+
+ defaults = {
+ 'id': 0,
+ 'status': 'busy',
+ 'downloader': self.provider.getName(),
+ 'folder': '',
+ }
+
+ return mergeDicts(defaults, result)
+
diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py
index 403a7e7d..a17b2dc5 100644
--- a/couchpotato/core/downloaders/nzbget/__init__.py
+++ b/couchpotato/core/downloaders/nzbget/__init__.py
@@ -24,6 +24,12 @@ config = [{
'default': 'localhost:6789',
'description': 'Hostname with port. Usually localhost:6789',
},
+ {
+ 'name': 'username',
+ 'default': 'nzbget',
+ 'advanced': True,
+ 'description': 'Set a different username to connect. Default: nzbget',
+ },
{
'name': 'password',
'type': 'password',
@@ -48,6 +54,12 @@ config = [{
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
+ {
+ 'name': 'delete_failed',
+ 'default': True,
+ 'type': 'bool',
+ 'description': 'Delete a release after the download has failed.',
+ },
],
}
],
diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py
index 82d8a363..fc54f024 100644
--- a/couchpotato/core/downloaders/nzbget/main.py
+++ b/couchpotato/core/downloaders/nzbget/main.py
@@ -1,9 +1,11 @@
from base64 import standard_b64encode
-from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss
-from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog
+from datetime import timedelta
import re
+import shutil
import socket
import traceback
import xmlrpclib
@@ -14,7 +16,7 @@ class NZBGet(Downloader):
type = ['nzb']
- url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
+ url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, filedata = None):
@@ -24,7 +26,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name'))
- url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
+ url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url)
@@ -50,7 +52,124 @@ class NZBGet(Downloader):
if xml_response:
log.info('NZB sent successfully to NZBGet')
- return True
+ nzb_id = md5(data['url']) # about as unique as they come ;)
+ couchpotato_id = "couchpotato=" + nzb_id
+ groups = rpc.listgroups()
+ file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
+ confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
+ if confirmed:
+ log.debug('couchpotato parameter set in nzbget download')
+ return self.downloadReturnId(nzb_id)
else:
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
+
+ def getAllDownloadStatus(self):
+
+ log.debug('Checking NZBGet download status.')
+
+ url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
+
+ rpc = xmlrpclib.ServerProxy(url)
+ try:
+ if rpc.writelog('INFO', 'CouchPotato connected to check status'):
+ log.info('Successfully connected to NZBGet')
+ else:
+ log.info('Successfully connected to NZBGet, but unable to send a message')
+ except socket.error:
+ log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
+ return False
+ except xmlrpclib.ProtocolError, e:
+ if e.errcode == 401:
+ log.error('Password is incorrect.')
+ else:
+ log.error('Protocol Error: %s', e)
+ return False
+
+ # Get NZBGet data
+ try:
+ status = rpc.status()
+ groups = rpc.listgroups()
+ queue = rpc.postqueue(0)
+ history = rpc.history()
+ except:
+ log.error('Failed getting data: %s', traceback.format_exc(1))
+ return False
+
+ statuses = StatusList(self)
+
+ for item in groups:
+ log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
+ try:
+ nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
+ except:
+ nzb_id = item['NZBID']
+ statuses.append({
+ 'id': nzb_id,
+ 'name': item['NZBFilename'],
+ 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
+ # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
+ 'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
+ })
+
+ for item in queue: # 'Parameters' is not passed in rpc.postqueue
+ log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
+ statuses.append({
+ 'id': item['NZBID'],
+ 'name': item['NZBFilename'],
+ 'original_status': item['Stage'],
+ 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
+ })
+
+ for item in history:
+ log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
+ try:
+ nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
+ except:
+ nzb_id = item['NZBID']
+ statuses.append({
+ 'id': nzb_id,
+ 'name': item['NZBFilename'],
+ 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
+ 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
+ 'timeleft': str(timedelta(seconds = 0)),
+ 'folder': item['DestDir']
+ })
+
+ return statuses
+
+ def removeFailed(self, item):
+
+ log.info('%s failed downloading, deleting...', item['name'])
+
+ url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
+
+ rpc = xmlrpclib.ServerProxy(url)
+ try:
+ if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
+ log.info('Successfully connected to NZBGet')
+ else:
+ log.info('Successfully connected to NZBGet, but unable to send a message')
+ except socket.error:
+ log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
+ return False
+ except xmlrpclib.ProtocolError, e:
+ if e.errcode == 401:
+ log.error('Password is incorrect.')
+ else:
+ log.error('Protocol Error: %s', e)
+ return False
+
+ try:
+ history = rpc.history()
+ for hist in history:
+ if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']:
+ nzb_id = hist['ID']
+ path = hist['DestDir']
+ if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
+ shutil.rmtree(path, True)
+ except:
+ log.error('Failed deleting: %s', traceback.format_exc(0))
+ return False
+
+ return True
diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py
index 1462c678..f1f8acc6 100644
--- a/couchpotato/core/downloaders/nzbvortex/main.py
+++ b/couchpotato/core/downloaders/nzbvortex/main.py
@@ -1,5 +1,5 @@
from base64 import b64encode
-from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
@@ -29,7 +29,9 @@ class NZBVortex(Downloader):
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
- return True
+ raw_statuses = self.call('nzb')
+ nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
+ return self.downloadReturnId(nzb_id)
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
@@ -38,7 +40,7 @@ class NZBVortex(Downloader):
raw_statuses = self.call('nzb')
- statuses = []
+ statuses = StatusList(self)
for item in raw_statuses.get('nzbs', []):
# Check status
@@ -53,7 +55,8 @@ class NZBVortex(Downloader):
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
- 'timeleft':-1,
+ 'timeleft': -1,
+ 'folder': item['destinationPath'],
})
return statuses
diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py
index a287f119..f2f217a1 100644
--- a/couchpotato/core/downloaders/sabnzbd/main.py
+++ b/couchpotato/core/downloaders/sabnzbd/main.py
@@ -1,8 +1,9 @@
-from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
+from datetime import timedelta
from urllib2 import URLError
import json
import traceback
@@ -17,8 +18,7 @@ class Sabnzbd(Downloader):
log.info('Sending "%s" to SABnzbd.', data.get('name'))
- params = {
- 'apikey': self.conf('api_key'),
+ req_params = {
'cat': self.conf('category'),
'mode': 'addurl',
'nzbname': self.createNzbName(data, movie),
@@ -31,17 +31,15 @@ class Sabnzbd(Downloader):
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, filedata, movie)
- params['mode'] = 'addfile'
+ req_params['mode'] = 'addfile'
else:
- params['name'] = data.get('url')
-
- url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
+ req_params['name'] = data.get('url')
try:
- if params.get('mode') is 'addfile':
- sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
+ if req_params.get('mode') is 'addfile':
+ sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else:
- sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
+ sab_data = self.call(req_params)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
@@ -49,17 +47,15 @@ class Sabnzbd(Downloader):
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False
- result = sab.strip()
- if not result:
- log.error('SABnzbd didn\'t return anything.')
- return False
-
- log.debug('Result text from SAB: %s', result[:40])
- if result[:2] == 'ok':
+ log.debug('Result from SAB: %s', sab_data)
+ if sab_data.get('status') and not sab_data.get('error'):
log.info('NZB sent to SAB successfully.')
- return True
+ if filedata:
+ return self.downloadReturnId(sab_data.get('nzo_ids')[0])
+ else:
+ return True
else:
- log.error(result[:40])
+ log.error('Error getting data from SABNZBd: %s', sab_data)
return False
def getAllDownloadStatus(self):
@@ -85,14 +81,13 @@ class Sabnzbd(Downloader):
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
- statuses = []
+ statuses = StatusList(self)
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
- 'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
@@ -111,7 +106,8 @@ class Sabnzbd(Downloader):
'name': item['name'],
'status': status,
'original_status': item['status'],
- 'timeleft': 0,
+ 'timeleft': str(timedelta(seconds = 0)),
+ 'folder': item['storage'],
})
return statuses
@@ -133,21 +129,21 @@ class Sabnzbd(Downloader):
return True
- def call(self, params, use_json = True):
+ def call(self, request_params, use_json = True, **kwargs):
- url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
+ url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
- data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
+ data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
if use_json:
d = json.loads(data)
if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
- return d[params['mode']]
+ return d.get(request_params['mode']) or d
else:
return data
diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py
index 210a0d9e..6dfbd3f9 100644
--- a/couchpotato/core/downloaders/transmission/__init__.py
+++ b/couchpotato/core/downloaders/transmission/__init__.py
@@ -41,15 +41,22 @@ config = [{
{
'name': 'directory',
'type': 'directory',
- 'description': 'Where should Transmission saved the downloaded files?',
+ 'description': 'Download to this directory. Keep empty for default Transmission download directory.',
},
{
'name': 'ratio',
'default': 10,
- 'type': 'int',
+ 'type': 'float',
'advanced': True,
'description': 'Stop transfer when reaching ratio',
},
+ {
+ 'name': 'ratiomode',
+ 'default': 0,
+ 'type': 'int',
+ 'advanced': True,
+ 'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
+ },
{
'name': 'manual',
'default': 0,
diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py
index 5c13af6e..8cf47380 100644
--- a/couchpotato/core/downloaders/transmission/main.py
+++ b/couchpotato/core/downloaders/transmission/main.py
@@ -1,11 +1,15 @@
from base64 import b64encode
-from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
+from couchpotato.environment import Env
+from datetime import timedelta
import httplib
import json
import os.path
import re
+import shutil
+import traceback
import urllib2
log = CPLog(__name__)
@@ -18,7 +22,7 @@ class Transmission(Downloader):
def download(self, data, movie, filedata = None):
- log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
+ log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port.
host = self.conf('host').split(':')
@@ -27,22 +31,24 @@ class Transmission(Downloader):
return False
# Set parameters for Transmission
- 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 = {
'paused': self.conf('paused', default = 0),
- 'download-dir': folder_path
}
+ 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
+
torrent_params = {}
if self.conf('ratio'):
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
- 'seedRatioMode': self.conf('ratio')
+ 'seedRatioMode': self.conf('ratiomode')
}
if not filedata and data.get('type') == 'torrent':
@@ -58,15 +64,97 @@ class Transmission(Downloader):
else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
+ if not remote_torrent:
+ return False
+
# Change settings of added torrents
- if torrent_params:
+ elif torrent_params:
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
- return True
- except Exception, err:
- log.error('Failed to change settings for transfer: %s', err)
+ log.info('Torrent sent to Transmission successfully.')
+ return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
+ except:
+ log.error('Failed to change settings for transfer: %s', traceback.format_exc())
return False
+ def getAllDownloadStatus(self):
+
+ log.debug('Checking Transmission download status.')
+
+ # Load host from config and split out port.
+ host = self.conf('host').split(':')
+ if not isInt(host[1]):
+ log.error('Config properties are not filled in correctly, port is missing.')
+ return False
+
+ # Go through Queue
+ try:
+ trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
+ return_params = {
+ '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
+
+ statuses = StatusList(self)
+
+ # Get torrents status
+ # CouchPotato Status
+ #status = 'busy'
+ #status = 'failed'
+ #status = 'completed'
+ # Transmission Status
+ #status = 0 => "Torrent is stopped"
+ #status = 1 => "Queued to check files"
+ #status = 2 => "Checking files"
+ #status = 3 => "Queued to download"
+ #status = 4 => "Downloading"
+ #status = 4 => "Queued to seed"
+ #status = 6 => "Seeding"
+ #To do :
+ # add checking file
+ # manage no peer in a range time => fail
+
+ for item in queue['torrents']:
+ log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
+
+ if not os.path.isdir(Env.setting('from', 'renamer')):
+ log.error('Renamer "from" folder doesn\'t to exist.')
+ return
+
+ if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
+ try:
+ trpc.stop_torrent(item['hashString'], {})
+ statuses.append({
+ 'id': item['hashString'],
+ 'name': item['name'],
+ 'status': 'completed',
+ 'original_status': item['status'],
+ 'timeleft': str(timedelta(seconds = 0)),
+ 'folder': os.path.join(item['downloadDir'], item['name']),
+ })
+ except Exception, err:
+ log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
+ statuses.append({
+ 'id': item['hashString'],
+ 'name': item['name'],
+ 'status': 'failed',
+ 'original_status': item['status'],
+ 'timeleft': str(timedelta(seconds = 0)),
+ })
+ else:
+ statuses.append({
+ 'id': item['hashString'],
+ 'name': item['name'],
+ 'status': 'busy',
+ 'original_status': item['status'],
+ 'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
+ })
+
+ return statuses
class TransmissionRPC(object):
@@ -97,6 +185,7 @@ class TransmissionRPC(object):
try:
open_request = urllib2.urlopen(request)
response = json.loads(open_request.read())
+ log.debug('request: %s', json.dumps(ojson))
log.debug('response: %s', json.dumps(response))
if response['result'] == 'success':
log.debug('Transmission action successfull')
@@ -146,3 +235,18 @@ class TransmissionRPC(object):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
return self._request(post_data)
+
+ def get_alltorrents(self, arguments):
+ post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
+ return self._request(post_data)
+
+ def stop_torrent(self, torrent_id, arguments):
+ arguments['ids'] = torrent_id
+ post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
+ return self._request(post_data)
+
+ def remove_torrent(self, torrent_id, remove_local_data, arguments):
+ arguments['ids'] = torrent_id
+ arguments['delete-local-data'] = remove_local_data
+ post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
+ return self._request(post_data)
diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py
index 5953b117..bca57e71 100644
--- a/couchpotato/core/downloaders/utorrent/main.py
+++ b/couchpotato/core/downloaders/utorrent/main.py
@@ -1,10 +1,12 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
-from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog
from hashlib import sha1
from multipartpost import MultipartPostHandler
+from datetime import timedelta
+import os
import cookielib
import httplib
import json
@@ -66,7 +68,7 @@ class uTorrent(Downloader):
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
- return True
+ return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
@@ -103,7 +105,36 @@ class uTorrent(Downloader):
log.debug('Nothing in queue')
return False
- statuses = []
+ 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', []):
@@ -113,12 +144,18 @@ 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': item[10],
+ 'timeleft': str(timedelta(seconds = item[10])),
+ 'folder': release_folder,
})
return statuses
@@ -195,3 +232,7 @@ class uTorrentAPI(object):
def get_status(self):
action = "list=1"
return self._request(action)
+
+ def get_settings(self):
+ action = "action=getsettings"
+ return self._request(action)
diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
index bd704f5c..2f872a6a 100644
--- a/couchpotato/core/event.py
+++ b/couchpotato/core/event.py
@@ -16,10 +16,8 @@ def runHandler(name, handler, *args, **kwargs):
def addEvent(name, handler, priority = 100):
- if events.get(name):
- e = events[name]
- else:
- e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
+ if not events.get(name):
+ events[name] = []
def createHandle(*args, **kwargs):
@@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100):
return h
- e.handle(createHandle, priority = priority)
+ events[name].append({
+ 'handler': createHandle,
+ 'priority': priority,
+ })
def removeEvent(name, handler):
e = events[name]
@@ -43,6 +44,12 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs):
if not events.get(name): return
+
+ e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
+
+ for event in events[name]:
+ e.handle(event['handler'], priority = event['priority'])
+
#log.debug('Firing event %s', name)
try:
@@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs):
'single': False, # Return single handler
'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish
+ 'async': False
}
# Do options
@@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs):
options[x] = val
except: pass
- e = events[name]
-
- # Lock this event
- e.lock.acquire()
-
- e.asynchronous = False
-
# Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
@@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs):
# Fire
result = e(*args, **kwargs)
- # Release lock for this event
- e.lock.release()
-
if options['single'] and not options['merge']:
results = None
@@ -104,13 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Merge
if options['merge'] and len(results) > 0:
- results.reverse() # Priority 1 is higher then 100
# Dict
if isinstance(results[0], dict):
+ results.reverse()
+
merged = {}
for result in results:
- merged = mergeDicts(merged, result)
+ merged = mergeDicts(merged, result, prepend_list = True)
results = merged
# Lists
@@ -140,13 +139,8 @@ def fireEvent(name, *args, **kwargs):
log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs):
- try:
- my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
- my_thread.setDaemon(True)
- my_thread.start()
- return True
- except Exception, e:
- log.error('%s: %s', (args[0], e))
+ kwargs['async'] = True
+ fireEvent(*args, **kwargs)
def errorHandler(error):
etype, value, tb = error
diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
index 82bf88f7..25def9ae 100644
--- a/couchpotato/core/helpers/variable.py
+++ b/couchpotato/core/helpers/variable.py
@@ -10,6 +10,20 @@ import sys
log = CPLog(__name__)
+def link(src, dst):
+ if os.name == 'nt':
+ import ctypes
+ if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
+ else:
+ os.link(src, dst)
+
+def symlink(src, dst):
+ if os.name == 'nt':
+ import ctypes
+ if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
+ else:
+ os.symlink(src, dst)
+
def getUserDir():
try:
import pwd
@@ -53,7 +67,7 @@ def getDataDir():
def isDict(object):
return isinstance(object, dict)
-def mergeDicts(a, b):
+def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b)
dst = a.copy()
@@ -67,7 +81,7 @@ def mergeDicts(a, b):
if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
- current_dst[key].extend(current_src[key])
+ current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
current_dst[key] = removeListDuplicates(current_dst[key])
else:
current_dst[key] = current_src[key]
diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py
index 7e98fa51..8d1608a3 100644
--- a/couchpotato/core/notifications/base.py
+++ b/couchpotato/core/notifications/base.py
@@ -2,13 +2,15 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
+from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
log = CPLog(__name__)
-class Notification(Plugin):
+class Notification(Provider):
+
+ type = 'notification'
default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!'
@@ -16,11 +18,12 @@ class Notification(Plugin):
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
+ 'core.message',
]
dont_listen_to = []
def __init__(self):
- addEvent('notify.%s' % self.getName().lower(), self.notify)
+ addEvent('notify.%s' % self.getName().lower(), self._notify)
addApiView(self.testNotifyName(), self.test)
@@ -33,10 +36,17 @@ class Notification(Plugin):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return
- return self.notify(message = message, data = data if data else group, listener = listener)
+ return self._notify(message = message, data = data if data else group, listener = listener)
return notify
+ def getNotificationImage(self, size = 'small'):
+ return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
+
+ def _notify(self, *args, **kwargs):
+ if self.isEnabled():
+ return self.notify(*args, **kwargs)
+
def notify(self, message = '', data = {}, listener = None):
pass
@@ -46,7 +56,7 @@ class Notification(Plugin):
log.info('Sending test to %s', test_type)
- success = self.notify(
+ success = self._notify(
message = self.test_message,
data = {},
listener = 'test'
diff --git a/couchpotato/core/notifications/boxcar/main.py b/couchpotato/core/notifications/boxcar/main.py
index 3135614a..b30d487a 100644
--- a/couchpotato/core/notifications/boxcar/main.py
+++ b/couchpotato/core/notifications/boxcar/main.py
@@ -11,7 +11,6 @@ class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
try:
message = message.strip()
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index bc8bd860..6aacd234 100644
--- a/couchpotato/core/notifications/core/main.py
+++ b/couchpotato/core/notifications/core/main.py
@@ -1,12 +1,13 @@
from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView
-from couchpotato.core.event import addEvent
+from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
+from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_
import threading
import time
@@ -21,11 +22,6 @@ class CoreNotifier(Notification):
messages = []
listeners = []
- listen_to = [
- 'renamer.after', 'movie.snatched',
- 'updater.available', 'updater.updated',
- ]
-
def __init__(self):
super(CoreNotifier, self).__init__()
@@ -54,7 +50,10 @@ class CoreNotifier(Notification):
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener)
+ fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
+
addEvent('app.load', self.clean)
+ addEvent('app.load', self.checkMessages)
def clean(self):
@@ -112,6 +111,22 @@ class CoreNotifier(Notification):
'notifications': notifications
})
+ def checkMessages(self):
+
+ prop_name = 'messages.last_check'
+ last_check = tryInt(Env.prop(prop_name, default = 0))
+
+ messages = fireEvent('cp.messages', last_check = last_check, single = True)
+
+ for message in messages:
+ if message.get('time') > last_check:
+ fireEvent('core.message', message = message.get('message'), data = message)
+
+ if last_check < message.get('time'):
+ last_check = message.get('time')
+
+ Env.prop(prop_name, value = last_check)
+
def notify(self, message = '', data = {}, listener = None):
db = get_session()
diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
index 52062e93..2b74364a 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -21,20 +21,17 @@ var NotificationBase = new Class({
App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, {
+ 'button_class': 'icon2.eye-open',
'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self)
})
$(App.block.notification).inject(App.getBlock('search'), 'after');
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
- /* App.getBlock('notification').addLink(new Element('a.more', {
- 'href': App.createUrl('notifications'),
- 'text': 'Show older notifications'
- })); */
});
window.addEvent('load', function(){
- self.startInterval.delay(Browser.safari ? 100 : 0, self)
+ self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self)
});
},
@@ -47,14 +44,19 @@ var NotificationBase = new Class({
result.el = App.getBlock('notification').addLink(
new Element('span.'+(result.read ? 'read' : '' )).adopt(
- new Element('span.message', {'text': result.message}),
+ new Element('span.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
)
, 'top');
self.notifications.include(result);
- if(!result.read)
+ if(result.data.important !== undefined && !result.read){
+ var sticky = true
+ App.fireEvent('message', [result.message, sticky, result])
+ }
+ else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
+ }
},
@@ -64,20 +66,26 @@ var NotificationBase = new Class({
self.badge[value ? 'show' : 'hide']()
},
- markAsRead: function(){
- var self = this;
+ markAsRead: function(force_ids){
+ var self = this,
+ ids = force_ids;
- var rn = self.notifications.filter(function(n){
- return !n.read
- })
+ if(!force_ids) {
+ var rn = self.notifications.filter(function(n){
+ return !n.read && n.data.important === undefined
+ })
- var ids = []
- rn.each(function(n){
- ids.include(n.id)
- })
+ var ids = []
+ rn.each(function(n){
+ ids.include(n.id)
+ })
+ }
if(ids.length > 0)
Api.request('notification.markread', {
+ 'data': {
+ 'ids': ids.join(',')
+ },
'onSuccess': function(){
self.setBadge('')
}
@@ -93,11 +101,20 @@ var NotificationBase = new Class({
return;
}
- Api.request('notification.listener', {
+ self.request = Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
}).send()
+ setInterval(function(){
+
+ if(self.request && self.request.isRunning()){
+ self.request.cancel();
+ self.startPoll()
+ }
+
+ }, 120000);
+
},
startPoll: function(){
@@ -143,26 +160,41 @@ var NotificationBase = new Class({
self.startPoll()
},
- showMessage: function(message){
+ showMessage: function(message, sticky, data){
var self = this;
if(!self.message_container)
self.message_container = new Element('div.messages').inject(document.body);
- var new_message = new Element('div.message', {
- 'text': message
- }).inject(self.message_container);
+ var new_message = new Element('div', {
+ 'class': 'message' + (sticky ? ' sticky' : ''),
+ 'html': message
+ }).inject(self.message_container, 'top');
setTimeout(function(){
new_message.addClass('show')
}, 10);
- setTimeout(function(){
+ var hide_message = function(){
new_message.addClass('hide')
setTimeout(function(){
new_message.destroy();
}, 1000);
- }, 4000);
+ }
+
+ if(sticky)
+ new_message.grab(
+ new Element('a.close.icon2', {
+ 'events': {
+ 'click': function(){
+ self.markAsRead([data.id]);
+ hide_message();
+ }
+ }
+ })
+ );
+ else
+ setTimeout(hide_message, 4000);
},
diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py
index 014b5856..21fcf157 100644
--- a/couchpotato/core/notifications/email/main.py
+++ b/couchpotato/core/notifications/email/main.py
@@ -12,7 +12,6 @@ log = CPLog(__name__)
class Email(Notification):
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
# Extract all the settings from settings
from_address = self.conf('from')
@@ -50,6 +49,5 @@ class Email(Notification):
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
- return False
return False
diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py
index 7f1398d9..caad661b 100644
--- a/couchpotato/core/notifications/growl/main.py
+++ b/couchpotato/core/notifications/growl/main.py
@@ -44,7 +44,6 @@ class Growl(Notification):
log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
self.register()
diff --git a/couchpotato/core/notifications/notifo/main.py b/couchpotato/core/notifications/notifo/main.py
index 64eee5ec..312055a8 100644
--- a/couchpotato/core/notifications/notifo/main.py
+++ b/couchpotato/core/notifications/notifo/main.py
@@ -13,7 +13,6 @@ class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
try:
params = {
diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py
index cc6ef662..2c4ac90c 100644
--- a/couchpotato/core/notifications/notifymyandroid/main.py
+++ b/couchpotato/core/notifications/notifymyandroid/main.py
@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyAndroid(Notification):
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
nma = pynma.PyNMA()
keys = splitString(self.conf('api_key'))
diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py
index 3258e85f..167b6eeb 100644
--- a/couchpotato/core/notifications/notifymywp/main.py
+++ b/couchpotato/core/notifications/notifymywp/main.py
@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key'))
diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py
index 9790ccfe..95139eed 100644
--- a/couchpotato/core/notifications/plex/main.py
+++ b/couchpotato/core/notifications/plex/main.py
@@ -46,7 +46,6 @@ class Plex(Notification):
return True
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
successful = 0
diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py
index 0990d0d1..e5c4678b 100644
--- a/couchpotato/core/notifications/prowl/main.py
+++ b/couchpotato/core/notifications/prowl/main.py
@@ -13,7 +13,6 @@ class Prowl(Notification):
}
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
data = {
'apikey': self.conf('api_key'),
diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py
index 3b113311..4c5e76c3 100644
--- a/couchpotato/core/notifications/pushalot/main.py
+++ b/couchpotato/core/notifications/pushalot/main.py
@@ -12,16 +12,15 @@ class Pushalot(Notification):
}
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
- 'LinkTitle': toUnicode("CouchPotato"),
- 'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
+ 'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
+ 'Source': toUnicode(self.default_title)
}
headers = {
diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py
index bcd3245c..ba7f426e 100644
--- a/couchpotato/core/notifications/pushover/main.py
+++ b/couchpotato/core/notifications/pushover/main.py
@@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
+from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
@@ -11,21 +12,26 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443")
- data = {
+ api_data = {
'user': self.conf('user_key'),
'token': self.app_token,
'message': toUnicode(message),
- 'priority': self.conf('priority')
+ 'priority': self.conf('priority'),
}
+ if data and data.get('library'):
+ api_data.extend({
+ 'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
+ 'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
+ })
+
http_handler.request('POST',
"/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'},
- body = tryUrlencode(data)
+ body = tryUrlencode(api_data)
)
response = http_handler.getresponse()
diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty/main.py
index 638c75dd..79b021e2 100644
--- a/couchpotato/core/notifications/toasty/main.py
+++ b/couchpotato/core/notifications/toasty/main.py
@@ -12,7 +12,6 @@ class Toasty(Notification):
}
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
data = {
'title': self.default_title,
diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py
new file mode 100644
index 00000000..b119736c
--- /dev/null
+++ b/couchpotato/core/notifications/trakt/__init__.py
@@ -0,0 +1,30 @@
+from .main import Trakt
+
+def start():
+ return Trakt()
+
+config = [{
+ 'name': 'trakt',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'trakt',
+ 'label': 'Trakt',
+ 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings',
+ 'options': [
+ {
+ 'name': 'notification_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'remove_watchlist_enabled',
+ 'label': 'Remove from watchlist',
+ 'default': False,
+ 'type': 'bool',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py
new file mode 100644
index 00000000..86d47086
--- /dev/null
+++ b/couchpotato/core/notifications/trakt/main.py
@@ -0,0 +1,46 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+
+log = CPLog(__name__)
+
+class Trakt(Notification):
+
+ urls = {
+ 'base': 'http://api.trakt.tv/%s',
+ 'library': 'movie/library/%s',
+ 'unwatchlist': 'movie/unwatchlist/%s',
+ }
+
+ listen_to = ['movie.downloaded']
+
+ def notify(self, message = '', data = {}, listener = None):
+
+ post_data = {
+ 'username': self.conf('automation_username'),
+ 'password' : self.conf('automation_password'),
+ 'movies': [{
+ 'imdb_id': data['library']['identifier'],
+ 'title': data['library']['titles'][0]['title'],
+ 'year': data['library']['year']
+ }] if data else []
+ }
+
+ result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
+ if self.conf('remove_watchlist_enabled'):
+ result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
+
+ return result
+
+ def call(self, method_url, post_data):
+
+ try:
+ response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
+ if response:
+ if response.get('status') == "success":
+ log.info('Successfully called Trakt')
+ return True
+ except:
+ pass
+
+ log.error('Failed to call trakt, check your login.')
+ return False
diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py
index bbb9262b..2db19743 100644
--- a/couchpotato/core/notifications/twitter/main.py
+++ b/couchpotato/core/notifications/twitter/main.py
@@ -32,7 +32,6 @@ class Twitter(Notification):
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py
index a1987bfa..84b9f7e4 100755
--- a/couchpotato/core/notifications/xbmc/main.py
+++ b/couchpotato/core/notifications/xbmc/main.py
@@ -13,10 +13,8 @@ class XBMC(Notification):
listen_to = ['renamer.after']
use_json_notifications = {}
- couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
hosts = splitString(self.conf('host'))
@@ -28,7 +26,7 @@ class XBMC(Notification):
if self.use_json_notifications.get(host):
response = self.request(host, [
- ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
+ ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}),
])
else:
@@ -90,7 +88,7 @@ class XBMC(Notification):
self.use_json_notifications[host] = True
# send the text message
- resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
+ resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
@@ -113,7 +111,7 @@ class XBMC(Notification):
server = 'http://%s/xbmcCmds/' % host
# Notification(title, message [, timeout , image])
- cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
+ cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :)
diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py
index b7b1ab28..440232b2 100644
--- a/couchpotato/core/plugins/automation/__init__.py
+++ b/couchpotato/core/plugins/automation/__init__.py
@@ -36,6 +36,20 @@ config = [{
'unit': 'hours',
'description': 'hours',
},
+ {
+ 'name': 'required_genres',
+ 'label': 'Required Genres',
+ 'default': '',
+ 'placeholder': 'Example: Action, Crime & Drama',
+ 'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
+ },
+ {
+ 'name': 'ignored_genres',
+ 'label': 'Ignored Genres',
+ 'default': '',
+ 'placeholder': 'Example: Horror, Comedy & Drama & Romance',
+ 'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
+ },
],
},
],
diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py
index f4ede40d..67bae1d1 100644
--- a/couchpotato/core/plugins/automation/main.py
+++ b/couchpotato/core/plugins/automation/main.py
@@ -10,11 +10,16 @@ class Automation(Plugin):
def __init__(self):
- fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
+ addEvent('app.load', self.setCrons)
if not Env.get('dev'):
addEvent('app.load', self.addMovies)
+ addEvent('setting.save.automation.hour.after', self.setCrons)
+
+ def setCrons(self):
+ fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
+
def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True)
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
index 0dc01783..5239f1e5 100644
--- a/couchpotato/core/plugins/file/main.py
+++ b/couchpotato/core/plugins/file/main.py
@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
+from flask.helpers import send_file
+from werkzeug.exceptions import NotFound
import os.path
import time
import traceback
@@ -71,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: continue
+ if root == python_cache or 'minified' in filename or 'version' in filename: continue
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
@@ -81,11 +83,13 @@ class FileManager(Plugin):
def showCacheFile(self, filename = ''):
- cache_dir = Env.get('cache_dir')
- filename = os.path.basename(filename)
+ file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
- from flask.helpers import send_from_directory
- return send_from_directory(cache_dir, filename)
+ if not os.path.isfile(file_path):
+ log.error('File "%s" not found', file_path)
+ raise NotFound()
+
+ return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css
index 222b8efa..524b588d 100644
--- a/couchpotato/core/plugins/log/static/log.css
+++ b/couchpotato/core/plugins/log/static/log.css
@@ -1,17 +1,18 @@
.page.log .nav {
display: block;
text-align: center;
- padding: 20px 0;
+ padding: 0 0 30px;
margin: 0;
font-size: 20px;
position: fixed;
- width: 960px;
+ width: 100%;
bottom: 0;
+ left: 0;
background: #4E5969;
}
.page.log .nav li {
- display: inline;
+ display: inline-block;
padding: 5px 10px;
margin: 0;
cursor: pointer;
@@ -24,7 +25,17 @@
.page.log .nav li.active {
font-weight: bold;
cursor: default;
- font-size: 30px;
+ background: rgba(255,255,255,.1);
+ }
+
+ @media all and (max-width: 480px) {
+ .page.log .nav {
+ font-size: 14px;
+ }
+
+ .page.log .nav li {
+ padding: 5px;
+ }
}
.page.log .loading {
diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py
index 30f6ea68..46eee210 100644
--- a/couchpotato/core/plugins/manage/__init__.py
+++ b/couchpotato/core/plugins/manage/__init__.py
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'manage',
- 'label': 'movie library manager',
+ 'label': 'Movie Library Manager',
'description': 'Add your existing movie folders.',
'options': [
{
diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
index 51094899..3e791de1 100644
--- a/couchpotato/core/plugins/manage/main.py
+++ b/couchpotato/core/plugins/manage/main.py
@@ -185,6 +185,8 @@ class Manage(Plugin):
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
+ else:
+ self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
return addToLibrary
diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
index 5ec968c3..1ce68f08 100644
--- a/couchpotato/core/plugins/movie/main.py
+++ b/couchpotato/core/plugins/movie/main.py
@@ -108,9 +108,8 @@ class MoviePlugin(Plugin):
now = time.time()
week = 262080
- done_status = fireEvent('status.get', 'done', single = True)
- available_status = fireEvent('status.get', 'available', single = True)
- snatched_status = fireEvent('status.get', 'snatched', single = True)
+ done_status, available_status, snatched_status = \
+ fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
db = get_session()
@@ -316,7 +315,7 @@ class MoviePlugin(Plugin):
for title in movie.library.titles:
if title.default: default_title = title.title
- fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
+ 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))
@@ -367,10 +366,8 @@ class MoviePlugin(Plugin):
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status
- status_active = fireEvent('status.add', 'active', single = True)
- snatched_status = fireEvent('status.add', 'snatched', single = True)
- ignored_status = fireEvent('status.add', 'ignored', single = True)
- downloaded_status = fireEvent('status.add', 'downloaded', single = True)
+ status_active, snatched_status, ignored_status, done_status, downloaded_status = \
+ fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
default_profile = fireEvent('profile.default', single = True)
@@ -397,7 +394,7 @@ class MoviePlugin(Plugin):
# Clean snatched history
for release in m.releases:
- if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
+ if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
@@ -548,8 +545,7 @@ class MoviePlugin(Plugin):
def restatus(self, movie_id):
- active_status = fireEvent('status.get', 'active', single = True)
- done_status = fireEvent('status.get', 'done', single = True)
+ active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session()
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
index 3d5545e3..6f577012 100644
--- a/couchpotato/core/plugins/movie/static/list.js
+++ b/couchpotato/core/plugins/movie/static/list.js
@@ -1,13 +1,15 @@
var MovieList = new Class({
- Implements: [Options],
+ Implements: [Events, Options],
options: {
navigation: true,
limit: 50,
load_more: true,
+ loader: true,
menu: [],
- add_new: false
+ add_new: false,
+ force_view: false
},
movies: [],
@@ -42,8 +44,11 @@ var MovieList = new Class({
}) : null
);
- self.changeView(self.getSavedView() || self.options.view || 'details');
-
+ if($(window).getSize().x <= 480 && !self.options.force_view)
+ self.changeView('list');
+ else
+ self.changeView(self.getSavedView() || self.options.view || 'details');
+
self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -120,7 +125,7 @@ var MovieList = new Class({
if(!self.navigation_counter) return;
- self.navigation_counter.set('text', (count || 0));
+ self.navigation_counter.set('text', (count || 0) + ' movies');
},
@@ -144,65 +149,67 @@ var MovieList = new Class({
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
- self.current_view = self.getSavedView() || 'details';
- self.el.addClass(self.current_view+'_list')
+ self.el.addClass('with_navigation')
- self.navigation = new Element('div.alph_nav').adopt(
- self.navigation_actions = new Element('ul.inlay.actions.reversed'),
- self.navigation_counter = new Element('span.counter[title=Total]'),
- self.navigation_alpha = new Element('ul.numbers', {
- 'events': {
- 'click:relay(li)': function(e, el){
- self.movie_list.empty()
- self.activateLetter(el.get('data-letter'))
- self.getMovies()
+ self.navigation = new Element('div.alph_nav').grab(
+ new Element('div').adopt(
+ self.navigation_alpha = new Element('ul.numbers', {
+ 'events': {
+ 'click:relay(li)': function(e, el){
+ self.movie_list.empty()
+ self.activateLetter(el.get('data-letter'))
+ self.getMovies()
+ }
}
- }
- }),
- self.navigation_search_input = new Element('input.inlay', {
- 'placeholder': 'Search',
- '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_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)
+ }
+ })
+ )
)
)
).inject(self.el, 'top');
@@ -247,18 +254,19 @@ var MovieList = new Class({
});
// Get available chars and highlight
- Api.request('movie.available_chars', {
- 'data': Object.merge({
- 'status': self.options.status
- }, self.filter),
- 'onComplete': function(json){
-
- json.chars.split('').each(function(c){
- self.letters[c.capitalize()].addClass('available')
- })
-
- }
- });
+ if(self.navigation.isDisplayed() || self.navigation.isVisible())
+ Api.request('movie.available_chars', {
+ 'data': Object.merge({
+ 'status': self.options.status
+ }, self.filter),
+ 'onSuccess': function(json){
+
+ json.chars.split('').each(function(c){
+ self.letters[c.capitalize()].addClass('available')
+ })
+
+ }
+ });
// Add menu or hide
if (self.options.menu.length > 0)
@@ -266,17 +274,7 @@ var MovieList = new Class({
self.navigation_menu.addLink(menu_item);
})
else
- self.navigation_menu.hide()
-
- self.nav_scrollspy = new ScrollSpy({
- min: 10,
- onEnter: function(){
- self.navigation.addClass('float')
- },
- onLeave: function(){
- self.navigation.removeClass('float')
- }
- });
+ self.navigation_menu.hide();
},
@@ -475,12 +473,39 @@ var MovieList = new Class({
self.load_more.set('text', 'loading...');
}
+ if(self.movies.length == 0 && self.options.loader){
+
+ self.loader_first = new Element('div.loading').adopt(
+ new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
+ ).inject(self.el, 'top');
+
+ createSpinner(self.loader_first, {
+ radius: 4,
+ length: 4,
+ width: 1
+ });
+
+ self.el.setStyle('min-height', 93);
+
+ }
+
Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({
'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset
}, self.filter),
- 'onComplete': function(json){
+ 'onSuccess': function(json){
+
+ if(self.loader_first){
+ var lf = self.loader_first;
+ self.loader_first.addClass('hide')
+ self.loader_first = null;
+ setTimeout(function(){
+ lf.destroy();
+ }, 20000);
+ self.el.setStyle('min-height', null);
+ }
+
self.store(json.movies);
self.addMovies(json.movies, json.total);
if(self.scrollspy) {
@@ -488,7 +513,8 @@ var MovieList = new Class({
self.scrollspy.start();
}
- self.checkIfEmpty()
+ self.checkIfEmpty();
+ self.fireEvent('loaded');
}
});
},
@@ -515,10 +541,10 @@ var MovieList = new Class({
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
- self.description[is_empty ? 'hide' : 'show']()
+ self.description.setStyle('display', [is_empty ? 'none' : ''])
if(is_empty && self.options.on_empty_element){
- self.el.grab(self.options.on_empty_element);
+ self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
if(self.navigation)
self.navigation.hide();
diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js
index a56e9abd..cf30e8a5 100644
--- a/couchpotato/core/plugins/movie/static/movie.actions.js
+++ b/couchpotato/core/plugins/movie/static/movie.actions.js
@@ -89,41 +89,22 @@ MA.Release = new Class({
}
});
- if(self.movie.data.releases.length == 0){
+ if(self.movie.data.releases.length == 0)
self.el.hide()
- }
- else {
-
- var buttons_done = false;
-
- self.movie.data.releases.sortBy('-info.score').each(function(release){
- if(buttons_done) return;
-
- var status = Status.get(release.status_id);
-
- if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
- self.hide_on_click = false;
- self.show();
- buttons_done = true;
- }
-
- });
-
- }
+ else
+ self.showHelper();
},
- show: function(e){
+ createReleases: function(){
var self = this;
- if(e)
- (e).preventDefault();
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')
)
- ).inject(self.movie, 'top');
+ );
// Header
new Element('div.item.head').adopt(
@@ -238,9 +219,71 @@ MA.Release = new Class({
}
+ },
+
+ show: function(e){
+ var self = this;
+ if(e)
+ (e).preventDefault();
+
+ self.createReleases();
+ self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container);
},
+ showHelper: function(e){
+ var self = this;
+ if(e)
+ (e).preventDefault();
+
+ self.createReleases();
+ self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
+
+ if(self.next_release || self.last_release){
+
+ self.trynext_container.adopt(
+ self.next_release ? [new Element('a.icon.readd', {
+ 'text': self.last_release ? 'Download another release' : 'Download the best release',
+ 'events': {
+ 'click': self.tryNextRelease.bind(self)
+ }
+ }),
+ new Element('a.icon.download', {
+ 'text': 'pick one yourself',
+ 'events': {
+ 'click': function(){
+ self.movie.quality.fireEvent('click');
+ }
+ }
+ })] : null,
+ new Element('a.icon.completed', {
+ 'text': 'mark this movie done',
+ 'events': {
+ 'click': function(){
+ Api.request('movie.delete', {
+ 'data': {
+ 'id': self.movie.get('id'),
+ 'delete_from': 'wanted'
+ },
+ 'onComplete': function(){
+ var movie = $(self.movie);
+ movie.set('tween', {
+ 'duration': 300,
+ 'onComplete': function(){
+ self.movie.destroy()
+ }
+ });
+ movie.tween('height', 0);
+ }
+ });
+ }
+ }
+ })
+ )
+ }
+
+ },
+
get: function(release, type){
return release.info[type] || 'n/a'
},
@@ -251,14 +294,15 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
- icon.addClass('spinner');
+ self.movie.busy(true);
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
- icon.removeClass('spinner')
+ self.movie.busy(false);
+
if(json.success)
icon.addClass('completed');
else
@@ -281,6 +325,8 @@ MA.Release = new Class({
tryNextRelease: function(movie_id){
var self = this;
+ self.createReleases();
+
if(self.last_release)
self.ignore(self.last_release);
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
index 65fe5209..66c0ef00 100644
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -1,24 +1,73 @@
.movies {
- padding: 60px 0 20px;
+ padding: 10px 0 20px;
position: relative;
z-index: 3;
+ width: 100%;
}
+ .movies > div {
+ clear: both;
+ }
+
+ .movies.thumbs_list > div:not(.description) {
+ margin-right: -4px;
+ text-align: center;
+ }
+
+ .movies .loading {
+ display: block;
+ padding: 20px 0 0 0;
+ width: 100%;
+ z-index: 3;
+ transition: all .4s cubic-bezier(0.9,0,0.1,1);
+ height: 40px;
+ opacity: 1;
+ position: absolute;
+ text-align: center;
+ }
+ .movies .loading.hide {
+ height: 0;
+ padding: 0;
+ opacity: 0;
+ margin-top: -20px;
+ overflow: hidden;
+ }
+
+ .movies .loading .spinner {
+ display: inline-block;
+ }
+
+ .movies .loading .message {
+ margin: 0 20px;
+ }
+
.movies h2 {
margin-bottom: 20px;
}
+ @media all and (max-width: 480px) {
+ .movies h2 {
+ font-size: 25px;
+ margin-bottom: 10px;
+ }
+ }
+
.movies > .description {
position: absolute;
top: 30px;
right: 0;
font-style: italic;
- text-shadow: none;
opacity: 0.8;
}
.movies:hover > .description {
opacity: 1;
}
+
+ @media all and (max-width: 860px) {
+ .movies > .description {
+ display: none;
+ }
+ }
.movies.thumbs_list {
padding: 20px 0 20px;
@@ -28,19 +77,20 @@
padding-top: 6px;
}
- .movies.mass_edit_list {
- padding-top: 90px;
- }
-
.movies .movie {
position: relative;
border-radius: 4px;
margin: 10px 0;
+ padding-left: 20px;
overflow: hidden;
width: 100%;
height: 180px;
- transition: all 0.2s linear;
+ transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
+
+ .movies.details_list .movie {
+ padding-left: 120px;
+ }
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
@@ -48,13 +98,21 @@
}
.movies.thumbs_list .movie {
- width: 153px;
- height: 230px;
+ width: 16.66667%;
+ height: auto;
display: inline-block;
- margin: 0 8px 0 0;
+ margin: 0;
+ padding: 0;
+ vertical-align: top;
+ border-radius: 0;
+ box-shadow: none;
+ border: 0;
}
- .movies.thumbs_list .movie:nth-child(6n+6) {
- margin: 0;
+
+ @media all and (max-width: 800px) {
+ .movies.thumbs_list .movie {
+ width: 25%;
+ }
}
.movies .movie .mask {
@@ -82,8 +140,8 @@
.movies .data {
padding: 20px;
height: 100%;
- width: 840px;
- position: absolute;
+ width: 100%;
+ position: relative;
right: 0;
border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
@@ -92,14 +150,15 @@
.movies.mass_edit_list .movie .data {
height: 30px;
padding: 3px 0 3px 10px;
- width: 938px;
box-shadow: none;
border: 0;
- background: none;
+ background: #4e5969;
}
.movies.thumbs_list .data {
+ position: absolute;
left: 0;
+ top: 0;
width: 100%;
padding: 10px;
height: 100%;
@@ -144,8 +203,11 @@
height: 100%;
border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,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;
@@ -159,38 +221,72 @@
.movies.thumbs_list .poster {
width: 100%;
height: 100%;
+ transition: none;
}
.movies .poster img,
.options .poster img {
- width: 101%;
- height: 101%;
+ width: 100%;
+ height: 100%;
}
+ .movies.thumbs_list .poster img {
+ height: auto;
+ width: 100%;
+ top: 0;
+ bottom: 0;
+ border-radius: 0;
+ }
.movies .info {
position: relative;
height: 100%;
+ width: 100%;
}
.movies .info .title {
- display: inline;
- position: absolute;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
+ margin-top: 2px;
left: 0;
top: 0;
- width: 90%;
+ width: 100%;
+ padding-right: 80px;
transition: all 0.2s linear;
}
+ .movies .info .title span {
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+ top: -5px;
+ position: relative;
+ }
+
+ .movies.thumbs_list .info .title span {
+ white-space: normal;
+ overflow: auto;
+ height: auto;
+ text-align: left;
+ }
+
+ @media all and (max-width: 480px) {
+ .movies.thumbs_list .movie .info .title span,
+ .movies.thumbs_list .movie .info .year {
+ font-size: 15px;
+ line-height: 15px;
+ overflow: hidden;
+ }
+ }
+
.movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title {
font-size: 16px;
font-weight: normal;
- text-overflow: ellipsis;
width: auto;
- overflow: hidden;
-
}
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
@@ -202,25 +298,22 @@
.movies.thumbs_list .info .title {
font-size: 21px;
- text-shadow: 0 0 10px #000;
word-wrap: break-word;
+ padding: 0;
}
.movies .info .year {
position: absolute;
- font-size: 30px;
- margin-bottom: 10px;
color: #bbb;
- width: 10%;
right: 0;
- top: 0;
+ top: 1px;
text-align: right;
transition: all 0.2s linear;
+ font-weight: normal;
}
.movies.list_list .movie:not(.details_view) .info .year,
.movies.mass_edit_list .info .year {
- font-size: 16px;
- width: 6%;
+ font-size: 1.25em;
right: 10px;
}
@@ -232,16 +325,14 @@
top: auto;
right: auto;
color: #FFF;
- text-shadow: none;
- text-shadow: 0 0 6px #000;
}
.movies .info .description {
- position: absolute;
top: 30px;
clear: both;
- height: 80px;
+ bottom: 30px;
overflow: hidden;
+ position: absolute;
}
.movies .data:hover .description {
overflow: auto;
@@ -254,12 +345,17 @@
.movies .data .quality {
position: absolute;
- bottom: 0;
+ bottom: 2px;
display: block;
min-height: 20px;
- vertical-align: mid;
}
+ @media all and (max-width: 480px) {
+ .movies .data .quality {
+ display: none;
+ }
+ }
+
.movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality {
display: none;
@@ -275,9 +371,8 @@
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
- text-shadow: none;
font-weight: normal;
- margin: 0 2px;
+ margin: 0 4px 0 0;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
@@ -285,7 +380,7 @@
.movies.mass_edit_list .data .quality {
text-align: right;
right: 0;
- margin-right: 50px;
+ margin-right: 60px;
z-index: 1;
}
@@ -315,18 +410,40 @@
bottom: 20px;
right: 20px;
line-height: 0;
- margin-top: -25px;
+ top: 0;
+ opacity: 0;
+ display: none;
+ width: 0;
}
- .movies.thumbs_list .data .actions {
- bottom: 8px;
- right: 10px;
+ @media all and (max-width: 480px) {
+ .movies .data .actions {
+ display: none !important;
+ }
}
- .movies .data:hover .action { opacity: 0.6; }
- .movies .data:hover .action:hover { opacity: 1; }
- .movies.mass_edit_list .data .actions {
- display: none;
+ .movies .movie:hover .data .actions {
+ opacity: 1;
+ display: inline-block;
+ width: auto;
}
+
+ .movies.details_list .data .actions {
+ top: auto;
+ bottom: 18px;
+ }
+
+ .movies .movie:hover .actions {
+ opacity: 1;
+ display: inline-block;
+ }
+ .movies.thumbs_list .data .actions {
+ bottom: 2px;
+ right: 10px;
+ top: auto;
+ }
+
+ .movies .movie:hover .action { opacity: 0.6; }
+ .movies .movie:hover .action:hover { opacity: 1; }
.movies .data .action {
background-repeat: no-repeat;
@@ -335,11 +452,14 @@
width: 26px;
height: 26px;
padding: 3px;
- opacity: 0;
+ }
+
+ .movies.mass_edit_list .movie .data .actions {
+ display: none;
}
- .movies.list_list .movie:not(.details_view) .data:hover .actions,
- .movies.mass_edit_list .data:hover .actions {
+ .movies.list_list .movie:not(.details_view):hover .actions,
+ .movies.mass_edit_list .movie:hover .actions {
margin: 0;
background: #4e5969;
top: 2px;
@@ -354,7 +474,8 @@
font-size: 20px;
position: absolute;
padding: 70px 0 0;
- width: 100%;
+ left: 120px;
+ right: 0;
}
.movies .delete_container .cancel {
}
@@ -372,8 +493,8 @@
.movies .options {
position: absolute;
- margin-left: 120px;
- width: 840px;
+ right: 0;
+ left: 120px;
}
.movies .options .form {
@@ -396,7 +517,6 @@
.movies .options .table .item.ignored span {
text-decoration: line-through;
color: rgba(255,255,255,0.4);
- text-shadow: none;
}
.movies .options .table .item.ignored .delete {
background-image: url('../images/icon.undo.png');
@@ -430,7 +550,7 @@
overflow: hidden;
}
.movies .options .table .name {
- width: 350px;
+ width: 340px;
overflow: hidden;
text-align: left;
padding: 0 10px;
@@ -464,6 +584,9 @@
text-align: center;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
overflow: hidden;
+ left: 0;
+ position: absolute;
+ z-index: 10;
}
.movies .movie .trailer_container.hide {
height: 0 !important;
@@ -480,6 +603,7 @@
background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
+ z-index: 11;
}
.movies .movie .hide_trailer.hide {
top: -30px;
@@ -510,6 +634,54 @@
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
+ .movies .movie .trynext {
+ display: inline;
+ position: absolute;
+ right: 135px;
+ z-index: 2;
+ opacity: 0;
+ background: #4e5969;
+ min-width: 300px;
+ text-align: right;
+ height: 100%;
+ padding: 3px 0;
+ top: 0;
+ }
+
+ @media all and (max-width: 480px) {
+ .movies .movie .trynext {
+ display: none;
+ }
+ }
+ .movies.mass_edit_list .trynext { display: none; }
+ .wanted .movies .movie .trynext {
+ padding-right: 50px;
+ }
+ .movies .movie:hover .trynext {
+ opacity: 1;
+ }
+
+ .movies.details_list .movie .trynext {
+ background: #47515f;
+ padding: 0;
+ right: 0;
+ bottom: 35px;
+ height: auto;
+ }
+
+ .movies .movie .trynext a {
+ background-position: 5px center;
+ padding: 0 5px 0 25px;
+ margin-right: 10px;
+ color: #FFF;
+ border-radius: 2px;
+ }
+ .movies .movie .trynext a:last-child {
+ margin: 0;
+ }
+ .movies .movie .trynext a:hover {
+ background-color: #369545;
+ }
.movies .load_more {
display: block;
@@ -523,24 +695,32 @@
.movies .alph_nav {
transition: box-shadow .4s linear;
- position: fixed;
+ position: relative;
z-index: 4;
- top: 0;
- padding: 100px 60px 7px;
- width: 1080px;
- margin: 0 -60px;
- box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
- background: #4e5969;
+ top: 0px;
+ right: 0;
+ margin: 0 auto;
+ width: 100%;
+ padding: 10px 0;
}
- .movies .alph_nav.float {
- box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
- border-radius: 0;
+ @media all and (max-width: 480px) {
+ .movies .alph_nav {
+ display: none;
+ }
}
-.movies .alph_nav ul.numbers,
+ .movies .alph_nav > div {
+ position: relative;
+ max-width: 980px;
+ margin: 0 auto;
+ padding: 0;
+ min-height: 24px;
+ }
+
+.movies .alph_nav .numbers,
.movies .alph_nav .counter,
-.movies .alph_nav ul.actions {
+.movies .alph_nav .actions {
list-style: none;
padding: 0 0 1px;
margin: 0;
@@ -549,49 +729,62 @@
}
.movies .alph_nav .counter {
- width: 60px;
- text-align: center;
+ text-align: right;
+ position: absolute;
+ right: 270px;
+ background: #4e5969;
+ padding: 4px 10px;
}
.movies .alph_nav .numbers li,
.movies .alph_nav .actions li {
display: inline-block;
vertical-align: top;
- width: 20px;
height: 24px;
- line-height: 26px;
+ line-height: 23px;
text-align: center;
cursor: pointer;
color: rgba(255,255,255,0.2);
border: 1px solid transparent;
transition: all 0.1s ease-in-out;
- text-shadow: none;
}
- .movies .alph_nav .numbers li:first-child {
- width: 43px;
- }
+
+ @media all and (max-width: 900px) {
+ .movies .alph_nav .numbers {
+ display: none;
+ }
+ }
+
+ .movies .alph_nav .numbers li {
+ width: auto;
+ padding: 0 4px;
+ }
+
.movies .alph_nav li.available {
- color: rgba(255,255,255,0.8);
+ color: #FFF;
font-weight: bolder;
}
- .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
- color: #fff;
- font-size: 20px;
- line-height: 20px;
+ .movies .alph_nav li.active.available,
+ .movies .alph_nav li.available:hover {
+ background: rgba(255,255,255,.1);
}
- .movies .alph_nav input {
+ .movies .alph_nav .search {
padding: 6px 5px;
- margin: 0 0 0 6px;
- float: left;
- width: 155px;
+ 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);
}
.movies .alph_nav .actions {
margin: 0 6px 0 0;
-moz-user-select: none;
+ position: absolute;
+ right: 183px;
}
.movies .alph_nav .actions li {
border-radius: 1px;
@@ -675,10 +868,12 @@
}
.movies .alph_nav .more_menu {
- margin-left: 48px;
+ right: 0;
+ position: absolute;
}
.movies .alph_nav .more_menu > a {
+ background-color: #4e5969;
background-position: center -158px;
}
@@ -735,7 +930,6 @@
font-weight: bold;
display: inline-block;
text-transform: uppercase;
- text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
index ffdd14d1..95fd1de5 100644
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -22,7 +22,10 @@ var Movie = new Class({
addEvents: function(){
var self = this;
- App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
+ App.addEvent('movie.update.'+self.data.id, function(notification){
+ self.busy(false)
+ self.update.delay(2000, self, notification);
+ });
['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+self.data.id, function(notification){
@@ -57,17 +60,19 @@ var Movie = new Class({
var self = this;
if(!set_busy){
- if(self.spinner){
- self.mask.fade('out');
- setTimeout(function(){
- if(self.mask)
- self.mask.destroy();
- if(self.spinner)
- self.spinner.el.destroy();
- self.spinner = null;
- self.mask = null;
- }, 400);
- }
+ setTimeout(function(){
+ if(self.spinner){
+ self.mask.fade('out');
+ setTimeout(function(){
+ if(self.mask)
+ self.mask.destroy();
+ if(self.spinner)
+ self.spinner.el.destroy();
+ self.spinner = null;
+ self.mask = null;
+ }, 400);
+ }
+ }, 1000)
}
else if(!self.spinner) {
self.createMask();
@@ -126,12 +131,14 @@ var Movie = new Class({
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
- self.title = new Element('div.title', {
- 'text': self.getTitle() || 'n/a'
- }),
- self.year = new Element('div.year', {
- 'text': self.data.library.year || 'n/a'
- }),
+ new Element('div.title').adopt(
+ self.title = new Element('span', {
+ 'text': self.getTitle() || 'n/a'
+ }),
+ self.year = new Element('div.year', {
+ 'text': self.data.library.year || 'n/a'
+ })
+ ),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css
index 23d87b46..fb0cadd2 100644
--- a/couchpotato/core/plugins/movie/static/search.css
+++ b/couchpotato/core/plugins/movie/static/search.css
@@ -1,113 +1,153 @@
.search_form {
display: inline-block;
vertical-align: middle;
- width: 25%;
+ position: absolute;
+ right: 105px;
+ top: 0;
+ text-align: right;
+ height: 100%;
+ border-bottom: 4px solid transparent;
+ transition: all .4s cubic-bezier(0.9,0,0.1,1);
+ position: absolute;
+ z-index: 20;
+ border: 1px solid transparent;
+ border-width: 0 0 4px;
}
-
- .search_form input {
- padding: 4px 20px 4px 4px;
- margin: 0;
- font-size: 14px;
- width: 100%;
- height: 24px;
+ .search_form:hover {
+ border-color: #047792;
}
- .search_form input:focus {
- padding-right: 83px;
+
+ @media all and (max-width: 480px) {
+ .search_form {
+ right: 44px;
+ }
+ }
+
+ .search_form.focused,
+ .search_form.shown {
+ border-color: #04bce6;
+ }
+
+ .search_form .input {
+ height: 100%;
+ overflow: hidden;
+ width: 45px;
+ transition: all .4s cubic-bezier(0.9,0,0.1,1);
+ }
+
+ .search_form.focused .input,
+ .search_form.shown .input {
+ width: 380px;
+ background: #4e5969;
}
- .search_form .input .enter {
- background: #369545 url('../images/sprite.png') right -188px no-repeat;
- padding: 0 20px 0 4px;
- border-radius: 2px;
- text-transform: uppercase;
- font-size: 10px;
- margin-left: -78px;
- display: inline-block;
- opacity: 0;
- position: relative;
- top: -2px;
- cursor: pointer;
- vertical-align: middle;
- visibility: hidden;
- }
- .search_form.focused .input .enter {
- visibility: visible;
+ .search_form .input input {
+ border-radius: 0;
+ display: block;
+ width: 100%;
+ border: 0;
+ background: rgba(255,255,255,.08);
+ color: #FFF;
+ font-size: 25px;
+ height: 100%;
+ padding: 10px;
+ width: 100%;
+ opacity: 0;
+ padding: 0 40px 0 10px;
+ transition: all .4s ease-in-out .2s;
}
- .search_form.focused.filled .input .enter {
- opacity: 1;
+ .search_form.focused .input input,
+ .search_form.shown .input input {
+ opacity: 1;
+ }
+
+ @media all and (max-width: 480px) {
+ .search_form .input input {
+ font-size: 15px;
+ }
+
+ .search_form.focused .input,
+ .search_form.shown .input {
+ width: 277px;
+ }
}
-
- .search_form .input a {
- width: 17px;
- height: 20px;
- display: inline-block;
- margin: -2px 0 0 2px;
- top: 4px;
- right: 5px;
- background: url('../images/sprite.png') left -37px no-repeat;
- cursor: pointer;
- opacity: 0;
- transition: all 0.2s ease-in-out;
- vertical-align: middle;
+
+ .search_form .input a {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 44px;
+ height: 100%;
+ cursor: pointer;
+ vertical-align: middle;
+ text-align: center;
+ line-height: 66px;
+ font-size: 15px;
+ color: #FFF;
+ }
+
+ .search_form .input a:after {
+ content: "\e03e";
}
- .search_form.filled .input a {
- opacity: 1;
+ .search_form.shown.filled .input a:after {
+ content: "\e04e";
+ }
+
+ @media all and (max-width: 480px) {
+ .search_form .input a {
+ line-height: 44px;
+ }
}
.search_form .results_container {
+ text-align: left;
position: absolute;
background: #5c697b;
- margin: 6px 0 0 -230px;
+ margin: 4px 0 0;
width: 470px;
min-height: 140px;
- border-radius: 3px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none;
}
+ @media all and (max-width: 480px) {
+ .search_form .results_container {
+ width: 320px;
+ }
+ }
+ .search_form.focused.filled .results_container,
.search_form.shown.filled .results_container {
display: block;
}
- .search_form .results_container:before {
- content: ' ';
- height: 0;
- position: relative;
- width: 0;
- border: 10px solid transparent;
- border-bottom-color: #5c697b;
- display: block;
- top: -20px;
- left: 346px;
- }
-
.search_form .results {
max-height: 570px;
overflow-x: hidden;
- padding: 10px 0;
- margin-top: -18px;
}
.movie_result {
overflow: hidden;
- height: 140px;
+ height: 50px;
position: relative;
}
.movie_result .options {
position: absolute;
height: 100%;
- width: 100%;
top: 0;
- left: 0;
+ left: 30px;
+ right: 0;
+ padding: 13px;
border: 1px solid transparent;
border-width: 1px 0;
border-radius: 0;
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
}
+ .movie_result .options > .in_library_wanted {
+ margin-top: -7px;
+ }
.movie_result .options > div {
- padding: 0 15px;
border: 0;
}
@@ -122,6 +162,13 @@
}
.movie_result .options select[name=title] { width: 180px; }
.movie_result .options select[name=profile] { width: 90px; }
+
+ @media all and (max-width: 480px) {
+
+ .movie_result .options select[name=title] { width: 90px; }
+ .movie_result .options select[name=profile] { width: 60px; }
+
+ }
.movie_result .options .button {
vertical-align: middle;
@@ -130,25 +177,21 @@
.movie_result .options .message {
height: 100%;
- line-height: 140px;
font-size: 20px;
- text-align: center;
color: #fff;
+ line-height: 20px;
}
.movie_result .data {
- padding: 0 15px;
position: absolute;
height: 100%;
- width: 100%;
top: 0;
- left: 0;
+ left: 30px;
+ right: 0;
background: #5c697b;
cursor: pointer;
-
- border-bottom: 1px solid #333;
- border-top: 1px solid rgba(255,255,255, 0.15);
- transition: all .6s cubic-bezier(0.9,0,0.1,1);
+ border-top: 1px solid rgba(255,255,255, 0.08);
+ transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
.movie_result .data.open {
left: 100%;
@@ -158,53 +201,46 @@
.movie_result .in_wanted, .movie_result .in_library {
position: absolute;
- margin-top: 105px;
+ bottom: 2px;
+ left: 14px;
+ font-size: 11px;
}
.movie_result .thumbnail {
- width: 17%;
- display: inline-block;
- margin: 15px 3% 15px 0;
+ width: 34px;
+ min-height: 100%;
+ display: block;
+ margin: 0;
vertical-align: top;
- border-radius: 3px;
- box-shadow: 0 0 3px rgba(0,0,0,0.35);
}
.movie_result .info {
- width: 80%;
- display: inline-block;
- vertical-align: top;
- padding: 15px 0;
- height: 120px;
- overflow: hidden;
- }
-
- .movie_result .info .tagline {
- max-height: 70px;
- overflow: hidden;
- display: inline-block;
- }
-
- .movie_result .add +.info {
- margin-left: 20%;
+ position: absolute;
+ top: 20%;
+ left: 15px;
+ right: 60px;
+ vertical-align: middle;
}
.movie_result .info h2 {
+ font-weight: normal;
+ font-size: 20px;
+ display: block;
margin: 0;
- font-size: 17px;
- line-height: 20px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
}
.movie_result .info h2 span {
padding: 0 5px;
+ position: absolute;
+ right: -60px;
}
- .movie_result .info h2 span:before { content: "("; }
- .movie_result .info h2 span:after { content: ")"; }
-
.search_form .mask,
.movie_result .mask {
- border-radius: 3px;
position: absolute;
height: 100%;
width: 100%;
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index dd8e7b04..c61b1624 100644
--- a/couchpotato/core/plugins/movie/static/search.js
+++ b/couchpotato/core/plugins/movie/static/search.js
@@ -7,33 +7,30 @@ Block.Search = new Class({
create: function(){
var self = this;
+ var focus_timer = 0;
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
- self.input = new Element('input.inlay', {
+ self.input = new Element('input', {
'placeholder': 'Search & add a new movie',
'events': {
'keyup': self.keyup.bind(self),
'focus': function(){
+ if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
- (function(){
+ focus_timer = (function(){
self.el.removeClass('focused')
- }).delay(2000);
+ }).delay(100);
}
}
}),
- new Element('span.enter', {
+ new Element('a.icon2', {
'events': {
- 'click': self.keyup.bind(self)
- },
- 'text':'Enter'
- }),
- new Element('a', {
- 'events': {
- 'click': self.clear.bind(self)
+ 'click': self.clear.bind(self),
+ 'touchend': self.clear.bind(self)
}
})
),
@@ -59,13 +56,21 @@ Block.Search = new Class({
var self = this;
(e).preventDefault();
- self.last_q = '';
- self.input.set('value', '');
- self.input.focus()
+ if(self.last_q === ''){
+ self.input.blur()
+ self.last_q = null;
+ }
+ else {
- self.movies = []
- self.results.empty()
- self.el.removeClass('filled')
+ self.last_q = '';
+ self.input.set('value', '');
+ self.input.focus()
+
+ self.movies = []
+ self.results.empty()
+ self.el.removeClass('filled')
+
+ }
},
hideResults: function(bool){
@@ -92,8 +97,10 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
- if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
- self.autocomplete()
+ if(self.q() != self.last_q){
+ if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
+ self.autocomplete_timer = self.autocomplete.delay(300, self)
+ }
},
@@ -197,6 +204,11 @@ Block.Search.Item = new Class({
self.el = new Element('div.movie_result', {
'id': info.imdb
}).adopt(
+ self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
+ 'src': info.images.poster[0],
+ 'height': null,
+ 'width': null
+ }) : null,
self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'tween': {
@@ -207,11 +219,6 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self)
}
}).adopt(
- self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
- 'src': info.images.poster[0],
- 'height': null,
- 'width': null
- }) : null,
new Element('div.info').adopt(
self.title = new Element('h2', {
'text': info.titles[0]
@@ -219,28 +226,11 @@ Block.Search.Item = new Class({
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : null
- ),
- self.tagline = new Element('span.tagline', {
- 'text': info.tagline ? info.tagline : info.plot,
- 'title': info.tagline ? info.tagline : info.plot
- }),
- self.director = self.info.director ? new Element('span.director', {
- 'text': 'Director:' + info.director
- }) : null,
- self.starring = info.actors ? new Element('span.actors', {
- 'text': 'Starring:'
- }) : null
+ )
)
)
)
- if(info.actors){
- Object.each(info.actors, function(actor){
- new Element('span', {
- 'text': actor
- }).inject(self.starring)
- })
- }
info.titles.each(function(title){
self.alternativeTitle({
@@ -319,12 +309,9 @@ Block.Search.Item = new Class({
}
self.options_el.grab(
- new Element('div').adopt(
- self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
- 'src': info.images.poster[0],
- 'height': null,
- 'width': null
- }) : null,
+ new Element('div', {
+ 'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
+ }).adopt(
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', {
diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index 4caa54f7..3dd89d9f 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Profile, ProfileType
+from couchpotato.core.settings.model import Profile, ProfileType, Movie
log = CPLog(__name__)
@@ -30,6 +30,21 @@ class ProfilePlugin(Plugin):
})
addEvent('app.initialize', self.fill, priority = 90)
+ addEvent('app.load', self.forceDefaults)
+
+ def forceDefaults(self):
+
+ # Get all active movies without profile
+ active_status = fireEvent('status.get', 'active', single = True)
+
+ db = get_session()
+ movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
+
+ if len(movies) > 0:
+ default_profile = self.default()
+ for movie in movies:
+ movie.profile_id = default_profile.get('id')
+ db.commit()
def allView(self):
@@ -129,6 +144,9 @@ class ProfilePlugin(Plugin):
db.delete(p)
db.commit()
+ # Force defaults on all empty profile movies
+ self.forceDefaults()
+
success = True
except Exception, e:
message = log.error('Failed deleting Profile: %s', e)
diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css
index 9d50d2fd..cdb33945 100644
--- a/couchpotato/core/plugins/profile/static/profile.css
+++ b/couchpotato/core/plugins/profile/static/profile.css
@@ -6,15 +6,25 @@
border-bottom: 1px solid rgba(255,255,255,0.2);
}
-.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
+.profile {
+ border-bottom: 1px solid rgba(255,255,255,0.2);
+ position: relative;
+}
.profile > .delete {
- height: 20px;
- width: 20px;
position: absolute;
- margin-left: 690px;
- padding: 14px;
+ padding: 25px 20px;
background-position: center;
+ right: 0;
+ cursor: pointer;
+ opacity: 0.6;
+ }
+ .profile > .delete:hover {
+ opacity: 1;
+ }
+
+ .profile .ctrlHolder:hover {
+ background: none;
}
.profile .qualities {
@@ -34,7 +44,8 @@
.profile .wait_for {
position: absolute;
- margin: -45px 0 0 437px;
+ right: 60px;
+ top: 0;
}
.profile .wait_for input {
@@ -61,6 +72,10 @@
margin-right: 10px;
}
+ .profile .type .check {
+ margin-top: -1px;
+ }
+
.profile .quality_type select {
width: 186px;
margin-left: -1px;
@@ -71,13 +86,13 @@
}
.profile .types .type .handle {
- background: url('./handle.png') center;
+ background: url('../../static/profile_plugin/handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
- cursor: grab;
- cursor: -moz-grab;
cursor: -webkit-grab;
+ cursor: -moz-grab;
+ cursor: grab;
margin: 0;
}
@@ -105,9 +120,9 @@
}
#profile_ordering li {
- cursor: grab;
- cursor: -moz-grab;
cursor: -webkit-grab;
+ cursor: -moz-grab;
+ cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
@@ -126,7 +141,7 @@
}
#profile_ordering li .handle {
- background: url('./handle.png') center;
+ background: url('../../static/profile_plugin/handle.png') center;
width: 20px;
float: right;
}
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index 4cd5b8de..cf0b046c 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -19,12 +19,12 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
- {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
- {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
+ {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
+ {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'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'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
+ {'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': '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']},
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index 02843f85..6618a8be 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -41,12 +41,15 @@ class Release(Plugin):
addEvent('release.clean', self.clean)
def add(self, group):
+
db = get_session()
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
+
+ done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
+
# Add movie
- done_status = fireEvent('status.get', 'done', single = True)
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not movie:
movie = Movie(
@@ -58,7 +61,6 @@ class Release(Plugin):
db.commit()
# Add Release
- snatched_status = fireEvent('status.get', 'snatched', single = True)
rel = db.query(Relea).filter(
or_(
Relea.identifier == identifier,
@@ -76,15 +78,19 @@ class Release(Plugin):
db.commit()
# Add each file type
+ added_files = []
for type in group['files']:
for cur_file in group['files'][type]:
added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie')
- try:
- added_file = db.query(File).filter_by(id = added_file.get('id')).one()
- rel.files.append(added_file)
- db.commit()
- except Exception, e:
- log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
+ added_files.append(added_file.get('id'))
+
+ # Add the release files in batch
+ try:
+ added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
+ rel.files.extend(added_files)
+ db.commit()
+ except Exception, e:
+ log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
fireEvent('movie.restatus', movie.id)
@@ -147,8 +153,7 @@ class Release(Plugin):
rel = db.query(Relea).filter_by(id = id).first()
if rel:
- ignored_status = fireEvent('status.get', 'ignored', single = True)
- available_status = fireEvent('status.get', 'available', single = True)
+ ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit()
@@ -160,7 +165,8 @@ class Release(Plugin):
db = get_session()
id = getParam('id')
- status_snatched = fireEvent('status.add', 'snatched', single = True)
+
+ snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
rel = db.query(Relea).filter_by(id = id).first()
if rel:
@@ -168,6 +174,8 @@ class Release(Plugin):
for info in rel.info:
item[info.identifier] = info.value
+ fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
+
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
@@ -182,8 +190,14 @@ class Release(Plugin):
}), manual = True, single = True)
if success:
- rel.status_id = status_snatched.get('id')
- db.commit()
+ db.expunge_all()
+ rel = db.query(Relea).filter_by(id = id).first() # Get release again
+
+ if rel.status_id != done_status.get('id'):
+ rel.status_id = snatched_status.get('id')
+ db.commit()
+
+ fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({
'success': success
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index 6ce21922..f3f7f66c 100644
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -13,7 +13,7 @@ rename_options = {
'thename': 'The Moviename',
'year': 'Year (2011)',
'first': 'First letter (M)',
- 'quality': 'Quality (720P)',
+ 'quality': 'Quality (720p)',
'video': 'Video (x264)',
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',
@@ -112,6 +112,15 @@ config = [{
'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
},
+ {
+ 'name': 'file_action',
+ 'label': 'Torrent File Action',
+ 'default': 'move',
+ 'type': 'dropdown',
+ 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')],
+ 'description': 'Define which kind of file operation you want to use for torrents. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.',
+ 'advanced': True,
+ },
{
'advanced': True,
'name': 'ntfs_permission',
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index b7062157..30a57a7b 100644
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -2,12 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss
-from couchpotato.core.helpers.request import jsonified
+from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
- getImdb
+ getImdb, link, symlink
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library, File, Profile, Release
+from couchpotato.core.settings.model import Library, File, Profile, Release, \
+ ReleaseInfo
from couchpotato.environment import Env
import errno
import os
@@ -18,7 +19,6 @@ import traceback
log = CPLog(__name__)
-
class Renamer(Plugin):
renaming_started = False
@@ -27,7 +27,12 @@ class Renamer(Plugin):
def __init__(self):
addApiView('renamer.scan', self.scanView, docs = {
- 'desc': 'For the renamer to check for new files to rename',
+ 'desc': 'For the renamer to check for new files to rename in a folder',
+ 'params': {
+ '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'},
+ },
})
addEvent('renamer.scan', self.scan)
@@ -35,22 +40,42 @@ class Renamer(Plugin):
addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched)
+ addEvent('app.load', self.setCrons)
- if self.conf('run_every') > 0:
- fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
+ # Enable / disable interval
+ addEvent('setting.save.renamer.enabled.after', self.setCrons)
+ addEvent('setting.save.renamer.run_every.after', self.setCrons)
+ addEvent('setting.save.renamer.force_every.after', self.setCrons)
- if self.conf('force_every') > 0:
- fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
+ def setCrons(self):
+
+ fireEvent('schedule.remove', 'renamer.check_snatched')
+ if self.isEnabled() and self.conf('run_every') > 0:
+ fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
+
+ fireEvent('schedule.remove', 'renamer.check_snatched_forced')
+ if self.isEnabled() and self.conf('force_every') > 0:
+ fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
+
+ return True
def scanView(self):
- fireEventAsync('renamer.scan')
+ params = getParams()
+ movie_folder = params.get('movie_folder', None)
+ downloader = params.get('downloader', None)
+ download_id = params.get('download_id', None)
+
+ fireEventAsync('renamer.scan',
+ movie_folder = movie_folder,
+ download_info = {'id': download_id, 'downloader': downloader} if download_id else None
+ )
return jsonified({
'success': True
})
- def scan(self):
+ def scan(self, movie_folder = None, download_info = None):
if self.isDisabled():
return
@@ -60,17 +85,42 @@ class Renamer(Plugin):
return
# Check to see if the "to" folder is inside the "from" folder.
- if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
- log.debug('"To" and "From" have to exist.')
+ if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
+ l = log.debug if movie_folder else log.error
+ l('Both the "To" and "From" have to exist.')
return
elif self.conf('from') in self.conf('to'):
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
return
-
- groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
+ elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
+ log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
+ return
self.renaming_started = True
+ # make sure the movie folder name is included in the search
+ folder = None
+ files = []
+ if movie_folder:
+ log.info('Scanning movie folder %s...', movie_folder)
+ movie_folder = movie_folder.rstrip(os.path.sep)
+ folder = os.path.dirname(movie_folder)
+
+ # Get all files from the specified folder
+ try:
+ for root, folders, names in os.walk(movie_folder):
+ files.extend([os.path.join(root, name) for name in names])
+ except:
+ log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
+
+ db = get_session()
+
+ # Extend the download info with info stored in the downloaded release
+ download_info = self.extendDownloadInfo(download_info)
+
+ groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
+ files = files, download_info = download_info, return_ignored = False, single = True)
+
destination = self.conf('to')
folder_name = self.conf('folder_name')
file_name = self.conf('file_name')
@@ -79,12 +129,8 @@ class Renamer(Plugin):
separator = self.conf('separator')
# Statusses
- done_status = fireEvent('status.get', 'done', single = True)
- active_status = fireEvent('status.get', 'active', single = True)
- downloaded_status = fireEvent('status.get', 'downloaded', single = True)
- snatched_status = fireEvent('status.get', 'snatched', single = True)
-
- db = get_session()
+ done_status, active_status, downloaded_status, snatched_status = \
+ fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
for group_identifier in groups:
@@ -304,7 +350,7 @@ class Renamer(Plugin):
else:
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
- # Add _EXISTS_ to the parent dir
+ # Add exists tag to the .ignore file
self.tagDir(group, 'exists')
# Notify on rename fail
@@ -325,7 +371,8 @@ class Renamer(Plugin):
db.commit()
# Remove leftover files
- if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers:
+ if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
+ not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)):
log.debug('Removing leftover files')
for current_file in group['files']['leftover']:
remove_files.append(current_file)
@@ -350,7 +397,7 @@ class Renamer(Plugin):
os.remove(src)
parent_dir = os.path.normpath(os.path.dirname(src))
- if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
+ if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir:
delete_folders.append(parent_dir)
except:
@@ -375,12 +422,15 @@ class Renamer(Plugin):
self.makeDir(os.path.dirname(dst))
try:
- self.moveFile(src, dst)
+ self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info))
group['renamed_files'].append(dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename')
+ if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
+ self.tagDir(group, 'renamed already')
+
# Remove matching releases
for release in remove_releases:
log.debug('Removing release %s', release.identifier)
@@ -426,36 +476,40 @@ class Renamer(Plugin):
return rename_files
+ # This adds a file to ignore / tag a release so it is ignored later
def tagDir(self, group, tag):
- rename_files = {}
+ ignore_file = None
+ for movie_file in sorted(list(group['files']['movie'])):
+ ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0]
+ break
- if group['dirname']:
- rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
- else: # Add it to filename
- for file_type in group['files']:
- for rename_me in group['files'][file_type]:
- filename = os.path.basename(rename_me)
- rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
+ text = """This file is from CouchPotato
+It has marked this release as "%s"
+This file hides the release from the renamer
+Remove it if you want it to be renamed (again, or at least let it try again)
+""" % tag
- for src in rename_files:
- if rename_files[src]:
- dst = rename_files[src]
- log.info('Renaming "%s" to "%s"', (src, dst))
+ if ignore_file:
+ self.createFile(ignore_file, text)
- # Create dir
- self.makeDir(os.path.dirname(dst))
- try:
- self.moveFile(src, dst)
- except:
- log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
- raise
-
- def moveFile(self, old, dest):
+ def moveFile(self, old, dest, forcemove = False):
dest = ss(dest)
try:
- shutil.move(old, dest)
+ if forcemove:
+ shutil.move(old, dest)
+ elif self.conf('file_action') == 'hardlink':
+ link(old, dest)
+ elif self.conf('file_action') == 'symlink':
+ symlink(old, dest)
+ elif self.conf('file_action') == 'copy':
+ shutil.copy(old, dest)
+ elif self.conf('file_action') == 'move_symlink':
+ shutil.move(old, dest)
+ symlink(dest, old)
+ else:
+ shutil.move(old, dest)
try:
os.chmod(dest, Env.getPermission('file'))
@@ -524,16 +578,14 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
+
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
- snatched_status = fireEvent('status.get', 'snatched', single = True)
- ignored_status = fireEvent('status.get', 'ignored', single = True)
- failed_status = fireEvent('status.get', 'failed', single = True)
-
- done_status = fireEvent('status.get', 'done', single = True)
+ snatched_status, ignored_status, failed_status, done_status = \
+ fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
@@ -571,8 +623,16 @@ class Renamer(Plugin):
found = False
for item in statuses:
- if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
+ found_release = False
+ if rel_dict['info'].get('download_id'):
+ if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
+ log.debug('Found release by id: %s', item['id'])
+ found_release = True
+ else:
+ if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
+ found_release = True
+ if found_release:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
@@ -580,16 +640,18 @@ class Renamer(Plugin):
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
+ rel.status_id = failed_status.get('id')
+ rel.last_edit = int(time.time())
+ db.commit()
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
- else:
- rel.status_id = failed_status.get('id')
- rel.last_edit = int(time.time())
- db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
- scan_required = True
+ if item['id'] and item['downloader'] and item['folder']:
+ fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item)
+ else:
+ scan_required = True
found = True
break
@@ -606,3 +668,38 @@ class Renamer(Plugin):
self.checking_snatched = False
return True
+
+ def extendDownloadInfo(self, download_info):
+
+ rls = None
+
+ if download_info and download_info.get('id') and download_info.get('downloader'):
+
+ db = get_session()
+
+ rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all()
+ rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all()
+
+ for rlsnfo_dwnld in rlsnfo_dwnlds:
+ for rlsnfo_id in rlsnfo_ids:
+ if rlsnfo_id.release == rlsnfo_dwnld.release:
+ rls = rlsnfo_id.release
+ break
+ if rls: break
+
+ if not rls:
+ log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader')))
+
+ if rls:
+
+ rls_dict = rls.to_dict({'info':{}})
+ download_info.update({
+ 'imdb_id': rls.movie.library.identifier,
+ 'quality': rls.quality.identifier,
+ 'type': rls_dict.get('info', {}).get('type')
+ })
+
+ return download_info
+
+ def downloadIsTorrent(self, download_info):
+ return download_info and download_info.get('type') in ['torrent', 'torrent_magnet']
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index b822bc0f..79fe0018 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -11,6 +11,7 @@ from subliminal.videos import Video
import enzyme
import os
import re
+import threading
import time
import traceback
@@ -74,7 +75,7 @@ class Scanner(Plugin):
'hdtv': ['hdtv']
}
- clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
+ clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@@ -100,7 +101,7 @@ class Scanner(Plugin):
addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber)
- def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
+ def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
folder = ss(os.path.normpath(folder))
@@ -118,8 +119,7 @@ class Scanner(Plugin):
try:
files = []
for root, dirs, walk_files in os.walk(folder):
- for filename in walk_files:
- files.append(os.path.join(root, filename))
+ files.extend(os.path.join(root, filename) for filename in walk_files)
except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
@@ -177,17 +177,25 @@ class Scanner(Plugin):
# Group files minus extension
+ ignored_identifiers = []
for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
log.debug('Grouping files: %s', identifier)
+ has_ignored = 0
for file_path in group['unsorted_files']:
- wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
+ ext = getExt(file_path)
+ wo_ext = file_path[:-(len(ext) + 1)]
found_files = set([i for i in leftovers if wo_ext in i])
group['unsorted_files'].extend(found_files)
leftovers = leftovers - found_files
+ has_ignored += 1 if ext == 'ignore' else 0
+
+ if has_ignored > 0:
+ ignored_identifiers.append(identifier)
+
# Break if CP wants to shut down
if self.shuttingDown():
break
@@ -313,6 +321,11 @@ class Scanner(Plugin):
del movie_files
+ # Make sure only one movie was found if a download ID is provided
+ if download_info and not len(valid_files) == 1:
+ log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
+ download_info = None
+
# Determine file types
processed_movies = {}
total_found = len(valid_files)
@@ -322,15 +335,17 @@ class Scanner(Plugin):
except:
break
+ if return_ignored is False and identifier in ignored_identifiers:
+ log.debug('Ignore file found, ignoring release: %s' % identifier)
+ continue
+
# Group extra (and easy) files first
- # images = self.getImages(group['unsorted_files'])
group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
- #'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']),
}
@@ -345,7 +360,7 @@ class Scanner(Plugin):
continue
log.debug('Getting metadata for %s', identifier)
- group['meta_data'] = self.getMetaData(group, folder = folder)
+ group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
# Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@@ -375,7 +390,7 @@ class Scanner(Plugin):
del group['unsorted_files']
# Determine movie
- group['library'] = self.determineMovie(group)
+ group['library'] = self.determineMovie(group, download_info = download_info)
if not group['library']:
log.error('Unable to determine movie: %s', group['identifiers'])
else:
@@ -388,6 +403,11 @@ class Scanner(Plugin):
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
+ # Wait for all the async events calm down a bit
+ while threading.activeCount() > 100 and not self.shuttingDown():
+ log.debug('Too many threads active, waiting a few seconds')
+ time.sleep(10)
+
if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
@@ -395,7 +415,7 @@ class Scanner(Plugin):
return processed_movies
- def getMetaData(self, group, folder = ''):
+ def getMetaData(self, group, folder = '', download_info = None):
data = {}
files = list(group['files']['movie'])
@@ -417,9 +437,13 @@ 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)
if not data['quality']:
- data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
+ if download_info and download_info.get('quality'):
+ data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
+ else:
+ 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'
@@ -495,17 +519,22 @@ class Scanner(Plugin):
return detected_languages
- def determineMovie(self, group):
- imdb_id = None
+ def determineMovie(self, group, download_info = None):
+
+ # Get imdb id from downloader
+ imdb_id = download_info and download_info.get('imdb_id')
+ if imdb_id:
+ log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
files = group['files']
# Check for CP(imdb_id) string in the file paths
- for cur_file in files['movie']:
- imdb_id = self.getCPImdb(cur_file)
- if imdb_id:
- log.debug('Found movie via CP tag: %s', cur_file)
- break
+ if not imdb_id:
+ for cur_file in files['movie']:
+ imdb_id = self.getCPImdb(cur_file)
+ if imdb_id:
+ log.debug('Found movie via CP tag: %s', cur_file)
+ break
# Check and see if nfo contains the imdb-id
if not imdb_id:
diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py
index 3d5a9e69..c9345fc0 100644
--- a/couchpotato/core/plugins/score/scores.py
+++ b/couchpotato/core/plugins/score/scores.py
@@ -116,13 +116,13 @@ def sizeScore(size):
def providerScore(provider):
- if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
- return 20
- if provider in ['Newznab']:
- return 10
+ try:
+ score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0))
+ except:
+ score = 0
- return 0
+ return score
def duplicateScore(nzb_name, movie_name):
diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
index a6dd6913..aec419a2 100644
--- a/couchpotato/core/plugins/searcher/__init__.py
+++ b/couchpotato/core/plugins/searcher/__init__.py
@@ -25,12 +25,13 @@ config = [{
'label': 'Required words',
'default': '',
'placeholder': 'Example: DTS, AC3 & English',
- 'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
+ 'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_words',
'label': 'Ignored words',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
+ 'description': 'Ignores releases that match any of these sets. (Works like explained above)'
},
{
'name': 'preferred_method',
@@ -40,6 +41,14 @@ config = [{
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
},
+ {
+ 'name': 'always_search',
+ 'default': False,
+ 'advanced': True,
+ 'type': 'bool',
+ 'label': 'Always search',
+ 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
+ },
],
}, {
'tab': 'searcher',
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
index 1f320eab..dba0ea7e 100644
--- a/couchpotato/core/plugins/searcher/main.py
+++ b/couchpotato/core/plugins/searcher/main.py
@@ -50,7 +50,12 @@ class Searcher(Plugin):
}"""},
})
- # Schedule cronjob
+ addEvent('app.load', self.setCrons)
+ addEvent('setting.save.searcher.cron_day.after', self.setCrons)
+ addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
+ addEvent('setting.save.searcher.cron_minute.after', self.setCrons)
+
+ def setCrons(self):
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self):
@@ -141,8 +146,7 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
- available_status = fireEvent('status.get', 'available', single = True)
- ignored_status = fireEvent('status.get', 'ignored', single = True)
+ available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
found_releases = []
@@ -157,7 +161,7 @@ class Searcher(Plugin):
ret = False
for quality_type in movie['profile']['types']:
- if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
+ if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue
@@ -285,10 +289,10 @@ class Searcher(Plugin):
if filedata == 'try_next':
return filedata
- successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
-
- if successful:
+ download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
+ log.debug('Downloader result: %s', download_result)
+ if download_result:
try:
# Mark release as snatched
db = get_session()
@@ -298,6 +302,15 @@ class Searcher(Plugin):
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
+
+ # Save download-id info if returned
+ if isinstance(download_result, dict):
+ for key in download_result:
+ rls_info = ReleaseInfo(
+ identifier = 'download_%s' % key,
+ value = toUnicode(download_result.get(key))
+ )
+ rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@@ -333,7 +346,7 @@ class Searcher(Plugin):
return True
- log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
+ log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', '')))
return False
@@ -357,7 +370,7 @@ class Searcher(Plugin):
return search_types
- def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
+ def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb')
@@ -370,8 +383,9 @@ class Searcher(Plugin):
movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
- required_words = splitString(self.conf('required_words').lower())
+ # Make sure it has required words
+ required_words = splitString(self.conf('required_words').lower())
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
@@ -381,19 +395,24 @@ class Searcher(Plugin):
log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False
+ # Ignore releases
ignored_words = splitString(self.conf('ignored_words').lower())
- blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
- if self.conf('ignored_words') and blacklisted:
- log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
+ ignored_match = 0
+ for ignored_set in ignored_words:
+ ignored = splitString(ignored_set, '&')
+ 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']))
return False
+ # Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
- #qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py
index c01caef5..c496c2de 100644
--- a/couchpotato/core/plugins/status/main.py
+++ b/couchpotato/core/plugins/status/main.py
@@ -25,13 +25,14 @@ class StatusPlugin(Plugin):
'available': 'Available',
'suggest': 'Suggest',
}
+ status_cached = {}
def __init__(self):
- addEvent('status.add', self.add)
- addEvent('status.get', self.add) # Alias for .add
+ addEvent('status.get', self.get)
addEvent('status.get_by_id', self.getById)
addEvent('status.all', self.all)
addEvent('app.initialize', self.fill)
+ addEvent('app.load', self.all) # Cache all statuses
addApiView('status.list', self.list, docs = {
'desc': 'Check for available update',
@@ -67,26 +68,40 @@ class StatusPlugin(Plugin):
s = status.to_dict()
temp.append(s)
- #db.close()
+ # Update cache
+ self.status_cached[status.identifier] = s
+
return temp
- def add(self, identifier):
+ def get(self, identifiers):
+
+ if not isinstance(identifiers, (list)):
+ identifiers = [identifiers]
db = get_session()
+ return_list = []
- s = db.query(Status).filter_by(identifier = identifier).first()
- if not s:
- s = Status(
- identifier = identifier,
- label = toUnicode(identifier.capitalize())
- )
- db.add(s)
- db.commit()
+ for identifier in identifiers:
- status_dict = s.to_dict()
+ if self.status_cached.get(identifier):
+ return_list.append(self.status_cached.get(identifier))
+ continue
- #db.close()
- return status_dict
+ s = db.query(Status).filter_by(identifier = identifier).first()
+ if not s:
+ s = Status(
+ identifier = identifier,
+ label = toUnicode(identifier.capitalize())
+ )
+ db.add(s)
+ db.commit()
+
+ status_dict = s.to_dict()
+
+ self.status_cached[identifier] = status_dict
+ return_list.append(status_dict)
+
+ return return_list if len(identifiers) > 1 else return_list[0]
def fill(self):
diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py
index 686d385e..bbd40853 100644
--- a/couchpotato/core/plugins/subtitle/__init__.py
+++ b/couchpotato/core/plugins/subtitle/__init__.py
@@ -20,7 +20,7 @@ config = [{
},
{
'name': 'languages',
- 'description': 'Comma separated, 2 letter country code. Example: en, nl',
+ 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at on Wikipedia',
},
# {
# 'name': 'automatic',
diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py
index 282b3482..d8496b30 100644
--- a/couchpotato/core/plugins/trailer/__init__.py
+++ b/couchpotato/core/plugins/trailer/__init__.py
@@ -22,14 +22,14 @@ config = [{
'name': 'quality',
'default': '720p',
'type': 'dropdown',
- 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
+ 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
},
{
'name': 'name',
'label': 'Naming',
'default': '-trailer',
'advanced': True,
- 'description': 'Use to use above settings.'
+ 'description': 'Use <filename> to use above settings.'
},
],
},
diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py
index 4ab51e78..1a8955fb 100644
--- a/couchpotato/core/plugins/trailer/main.py
+++ b/couchpotato/core/plugins/trailer/main.py
@@ -22,10 +22,15 @@ class Trailer(Plugin):
return False
for trailer in trailers.get(self.conf('quality'), []):
- filename = self.conf('name').replace('', group['filename']) + ('.%s' % getExt(trailer))
+
+ ext = getExt(trailer)
+ filename = self.conf('name').replace('', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext))
destination = os.path.join(group['destination_dir'], filename)
if not os.path.isfile(destination):
- fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
+ trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
+ if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
+ os.unlink(trailer_file)
+ continue
else:
log.debug('Trailer already exists: %s', destination)
diff --git a/couchpotato/core/plugins/userscript/static/userscript.css b/couchpotato/core/plugins/userscript/static/userscript.css
index 304dfa74..d08953a6 100644
--- a/couchpotato/core/plugins/userscript/static/userscript.css
+++ b/couchpotato/core/plugins/userscript/static/userscript.css
@@ -5,6 +5,7 @@
bottom: 0;
left: 0;
right: 0;
+ padding: 0;
}
.page.userscript .frame.loading {
@@ -12,3 +13,26 @@
font-size: 20px;
padding: 20px;
}
+
+ .page.userscript .movie_result {
+ height: 140px;
+ }
+ .page.userscript .movie_result .thumbnail {
+ width: 90px;
+ }
+ .page.userscript .movie_result .options {
+ left: 90px;
+ padding: 54px 15px;
+ }
+
+ .page.userscript .movie_result .year {
+ display: none;
+ }
+
+ .page.userscript .movie_result .options select[name="title"] {
+ width: 190px;
+ }
+
+ .page.userscript .movie_result .options select[name="profile"] {
+ width: 70px;
+ }
diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js
index d6d5983c..b7849766 100644
--- a/couchpotato/core/plugins/userscript/static/userscript.js
+++ b/couchpotato/core/plugins/userscript/static/userscript.js
@@ -63,28 +63,19 @@ var UserscriptSettingTab = new Class({
self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){
- // See if userscript can be installed
- var userscript = false;
- try {
- if(Components.interfaces.gmIGreasemonkeyService)
- userscript = true
- }
- catch(e){
- userscript = Browser.chrome === true;
- }
-
var host_url = window.location.protocol + '//' + window.location.host;
self.settings.createGroup({
'name': 'userscript',
- 'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''),
+ 'label': 'Install the bookmarklet or userscript',
'description': 'Easily add movies via imdb.com, appletrailers and more'
}).inject(self.settings.tabs.automation.content, 'top').adopt(
- (userscript ? [new Element('a.userscript.button', {
+ new Element('a.userscript.button', {
'text': 'Install userscript',
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
- 'target': '_self'
- }), new Element('span.or[text=or]')] : null),
+ 'target': '_blank'
+ }),
+ new Element('span.or[text=or]'),
new Element('span.bookmarklet').adopt(
new Element('a.button.green', {
'text': '+CouchPotato',
diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js
index c30fad5b..c3dd17dc 100644
--- a/couchpotato/core/plugins/userscript/template.js
+++ b/couchpotato/core/plugins/userscript/template.js
@@ -1,4 +1,9 @@
// ==UserScript==
+//
+// If you can read this, you need to enable or install the Greasemonkey add-on for firefox
+// If you are using Chrome, download this file and drag it to the extensions tab
+// Other browsers, use the bookmarklet
+//
// @name CouchPotato UserScript
// @description Add movies like a real CouchPotato
// @grant none
diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css
index 8d50d9de..c27c1d83 100644
--- a/couchpotato/core/plugins/wizard/static/wizard.css
+++ b/couchpotato/core/plugins/wizard/static/wizard.css
@@ -1,49 +1,60 @@
.page.wizard .uniForm {
- width: 80%;
- margin: 0 auto 30px;
+ margin: 0 0 30px;
+ width: 83%;
}
.page.wizard h1 {
- padding: 10px 30px;
- margin: 0;
+ padding: 10px 0;
+ margin: 0 5px;
display: block;
font-size: 30px;
margin-top: 80px;
}
.page.wizard .description {
- padding: 10px 30px;
- font-size: 18px;
+ padding: 10px 5px;
+ font-size: 1.45em;
+ line-height: 1.4em;
display: block;
}
.page.wizard .tab_wrapper {
background: #5c697b;
- padding: 10px 0;
- font-size: 18px;
+ height: 65px;
+ font-size: 1.75em;
position: fixed;
top: 0;
margin: 0;
width: 100%;
- min-width: 960px;
left: 0;
z-index: 2;
- box-shadow: 0 0 50px rgba(0,0,0,0.55);
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.page.wizard .tab_wrapper .tabs {
- text-align: center;
padding: 0;
- margin: 0;
+ margin: 0 auto;
display: block;
+ height: 100%;
+ width: 100%;
+ max-width: 960px;
}
.page.wizard .tabs li {
display: inline-block;
+ height: 100%;
}
.page.wizard .tabs li a {
padding: 20px 10px;
+ height: 100%;
+ display: block;
+ color: #FFF;
+ font-weight: normal;
+ border-bottom: 4px solid transparent;
}
+
+ .page.wizard .tabs li:hover a { border-color: #047792; }
+ .page.wizard .tabs li.done a { border-color: #04bce6; }
.page.wizard .tab_wrapper .pointer {
border-right: 10px solid transparent;
@@ -61,27 +72,13 @@
.page.wizard form > div {
min-height: 300px;
}
-.page.wizard .wgroup_finish {
- height: 300px;
-}
- .page.wizard .wgroup_finish h1 {
- text-align: center;
- }
- .page.wizard .wgroup_finish .wizard_support,
- .page.wizard .wgroup_finish .description {
- font-size: 25px;
- line-height: 120%;
- margin: 20px 0;
- text-align: center;
- }
- .page.wizard .button.green {
- padding: 20px;
- font-size: 25px;
- margin: 10px 30px 80px;
- display: block;
- text-align: center;
- }
+.page.wizard .button.green {
+ padding: 20px;
+ font-size: 25px;
+ margin: 10px 0 80px;
+ display: block;
+}
.page.wizard .tab_nzb_providers {
margin: 20px 0 0 0;
diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js
index 12ae4771..71910e27 100644
--- a/couchpotato/core/plugins/wizard/static/wizard.js
+++ b/couchpotato/core/plugins/wizard/static/wizard.js
@@ -9,27 +9,12 @@ Page.Wizard = new Class({
headers: {
'welcome': {
'title': 'Welcome to the new CouchPotato',
- 'description': 'To get started, fill in each of the following settings as much as you can.
Maybe first start with importing your movies from the previous CouchPotato',
+ 'description': 'To get started, fill in each of the following settings as much as you can.',
'content': new Element('div', {
'styles': {
'margin': '0 0 0 30px'
}
- }).adopt(
- new Element('div', {
- 'html': 'Select the data.db. It should be in your CouchPotato root directory.'
- }),
- self.import_iframe = new Element('iframe', {
- 'styles': {
- 'height': 40,
- 'width': 300,
- 'border': 0,
- 'overflow': 'hidden'
- }
- })
- ),
- 'event': function(){
- self.import_iframe.set('src', Api.createUrl('v1.import'))
- }
+ })
},
'general': {
'title': 'General',
@@ -178,7 +163,7 @@ Page.Wizard = new Class({
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
- ).inject(tabs);
+ ).inject(tabs)
}
else
@@ -214,13 +199,7 @@ Page.Wizard = new Class({
self.el.getElement('.t_searcher').hide();
// Add pointer
- new Element('.tab_wrapper').wraps(tabs).adopt(
- self.pointer = new Element('.pointer', {
- 'tween': {
- 'transition': 'quint:in:out'
- }
- })
- );
+ new Element('.tab_wrapper').wraps(tabs);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
@@ -232,16 +211,18 @@ Page.Wizard = new Class({
if(!t) return;
var func = function(){
- var ct = t.getCoordinates();
- self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
+ // Activate all previous ones
+ self.groups.each(function(groups2, nr2){
+ var t2 = self.el.getElement('.t_'+groups2);
+ t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done');
+ })
g.tween('opacity', 1);
}
if(nr == 0)
func();
-
- var ss = new ScrollSpy( {
+ new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2);
diff --git a/couchpotato/core/providers/automation/base.py b/couchpotato/core/providers/automation/base.py
index 8d08f913..6aef58a1 100644
--- a/couchpotato/core/providers/automation/base.py
+++ b/couchpotato/core/providers/automation/base.py
@@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
+from couchpotato.core.helpers.variable import splitString
import time
log = CPLog(__name__)
@@ -59,7 +60,26 @@ class Automation(Provider):
type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type)
if type_value < type_min:
- log.info('%s too low for %s, need %s has %s', (minimal_type, movie['imdb'], type_min, type_value))
+ log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value))
+ return False
+
+ movie_genres = [genre.lower() for genre in movie['genres']]
+ required_genres = splitString(self.getMinimal('required_genres').lower())
+ ignored_genres = splitString(self.getMinimal('ignored_genres').lower())
+
+ req_match = 0
+ for req_set in required_genres:
+ req = splitString(req_set, '&')
+ 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'])
+ 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))
return False
return True
diff --git a/couchpotato/core/providers/automation/letterboxd/__init__.py b/couchpotato/core/providers/automation/letterboxd/__init__.py
new file mode 100644
index 00000000..f2b8486b
--- /dev/null
+++ b/couchpotato/core/providers/automation/letterboxd/__init__.py
@@ -0,0 +1,34 @@
+from .main import Letterboxd
+
+def start():
+ return Letterboxd()
+
+config = [{
+ 'name': 'letterboxd',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'watchlist_providers',
+ 'name': 'letterboxd_automation',
+ 'label': 'Letterboxd',
+ 'description': 'Import movies from any public Letterboxd watchlist',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'Username',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/providers/automation/letterboxd/main.py
new file mode 100644
index 00000000..7bae2ad5
--- /dev/null
+++ b/couchpotato/core/providers/automation/letterboxd/main.py
@@ -0,0 +1,49 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.helpers.variable import tryInt, splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.automation.base import Automation
+import re
+
+log = CPLog(__name__)
+
+
+class Letterboxd(Automation):
+
+ url = 'http://letterboxd.com/%s/watchlist/'
+ pattern = re.compile(r'(.*)\((\d*)\)')
+
+ def getIMDBids(self):
+
+ urls = splitString(self.conf('automation_urls'))
+
+ if len(urls) == 0:
+ return []
+
+ movies = []
+
+ for movie in self.getWatchlist():
+ imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
+ movies.append(imdb_id)
+
+ return movies
+
+ def getWatchlist(self):
+
+ enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
+ urls = splitString(self.conf('automation_urls'))
+
+ index = -1
+ movies = []
+ for username in urls:
+
+ index += 1
+ if not enablers[index]:
+ continue
+
+ soup = BeautifulSoup(self.getHTMLData(self.url % username))
+
+ for movie in soup.find_all('a', attrs = { 'class': 'frame' }):
+ match = filter(None, self.pattern.split(movie['title']))
+ movies.append({'title': match[0], 'year': match[1] })
+
+ return movies
diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py
index 855f3c72..1f0df964 100644
--- a/couchpotato/core/providers/base.py
+++ b/couchpotato/core/providers/base.py
@@ -104,6 +104,7 @@ class YarrProvider(Provider):
try:
cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
+ opener.addheaders = []
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams())
diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py
index f0f98e0e..12d1e325 100644
--- a/couchpotato/core/providers/movie/_modifier/main.py
+++ b/couchpotato/core/providers/movie/_modifier/main.py
@@ -52,8 +52,7 @@ class MovieResultModifier(Plugin):
if l:
# Statuses
- active_status = fireEvent('status.get', 'active', single = True)
- done_status = fireEvent('status.get', 'done', single = True)
+ active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
for movie in l.movies:
if movie.status_id == active_status['id']:
diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/movie/couchpotatoapi/main.py
index fc16e8ab..2beb7e28 100644
--- a/couchpotato/core/providers/movie/couchpotatoapi/main.py
+++ b/couchpotato/core/providers/movie/couchpotatoapi/main.py
@@ -2,9 +2,11 @@ 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
+from couchpotato.environment import Env
import time
log = CPLog(__name__)
@@ -18,19 +20,37 @@ class CouchPotatoApi(MovieProvider):
'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',
}
http_time_between_calls = 0
api_version = 1
def __init__(self):
- #addApiView('movie.suggest', self.suggestView)
-
addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate)
addEvent('movie.suggest', self.suggest)
addEvent('movie.is_movie', self.isMovie)
+ addEvent('cp.source_url', self.getSourceUrl)
+ addEvent('cp.messages', self.getMessages)
+
+ def getMessages(self, last_check = 0):
+
+ data = self.getJsonData(self.urls['messages'] % tryUrlencode({
+ 'last_check': last_check,
+ }), headers = self.getRequestHeaders(), cache_timeout = 10)
+
+ return data
+
+ def getSourceUrl(self, repo = None, repo_name = None, branch = None):
+ return self.getJsonData(self.urls['updater'] % tryUrlencode({
+ 'repo': repo,
+ 'name': repo_name,
+ 'branch': branch,
+ }), headers = self.getRequestHeaders())
+
def search(self, q, limit = 12):
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
@@ -96,4 +116,5 @@ class CouchPotatoApi(MovieProvider):
'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': self.api_version,
'X-CP-Time': time.time(),
+ 'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats
}
diff --git a/couchpotato/core/providers/nzb/binsearch/__init__.py b/couchpotato/core/providers/nzb/binsearch/__init__.py
index f4288b11..d3288604 100644
--- a/couchpotato/core/providers/nzb/binsearch/__init__.py
+++ b/couchpotato/core/providers/nzb/binsearch/__init__.py
@@ -18,6 +18,14 @@ config = [{
'name': 'enabled',
'type': 'enabler',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/ftdworld/__init__.py b/couchpotato/core/providers/nzb/ftdworld/__init__.py
index ca60ac4d..37aedb2a 100644
--- a/couchpotato/core/providers/nzb/ftdworld/__init__.py
+++ b/couchpotato/core/providers/nzb/ftdworld/__init__.py
@@ -27,6 +27,14 @@ config = [{
'default': '',
'type': 'password',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py
index 9047d200..90f81cfc 100644
--- a/couchpotato/core/providers/nzb/newznab/__init__.py
+++ b/couchpotato/core/providers/nzb/newznab/__init__.py
@@ -12,9 +12,10 @@ config = [{
'list': 'nzb_providers',
'name': 'newznab',
'order': 10,
- 'description': 'Enable NewzNab providers such as NZB.su, \
+ 'description': 'Enable NewzNab such as NZB.su, \
NZBs.org, DOGnzb.cr, \
- Spotweb or NZBGeek',
+ Spotweb, NZBGeek, \
+ SmackDown, NZBFinder',
'wizard': True,
'options': [
{
@@ -23,20 +24,27 @@ config = [{
},
{
'name': 'use',
- 'default': '0,0,0,0'
+ 'default': '0,0,0,0,0,0'
},
{
'name': 'host',
- 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info',
+ 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
'description': 'The hostname of your newznab provider',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'default': '0,0,0,0,0,0',
+ 'description': 'Starting score for each release found via this provider.',
+ },
{
'name': 'api_key',
- 'default': ',,,',
+ 'default': ',,,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',
- 'combine': ['use', 'host', 'api_key'],
+ 'combine': ['use', 'host', 'api_key', 'extra_score'],
},
],
},
diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
index 414e7112..11107c1f 100644
--- a/couchpotato/core/providers/nzb/newznab/main.py
+++ b/couchpotato/core/providers/nzb/newznab/main.py
@@ -1,6 +1,6 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
-from couchpotato.core.helpers.variable import cleanHost, splitString
+from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import ResultList
from couchpotato.core.providers.nzb.base import NZBProvider
@@ -76,6 +76,7 @@ class Newznab(NZBProvider, RSS):
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
'content': self.getTextElement(nzb, 'description'),
+ 'score': host['extra_score'],
})
def getHosts(self):
@@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS):
uses = splitString(str(self.conf('use')))
hosts = splitString(self.conf('host'))
api_keys = splitString(self.conf('api_key'))
+ extra_score = splitString(self.conf('extra_score'))
list = []
for nr in range(len(hosts)):
list.append({
'use': uses[nr],
'host': hosts[nr],
- 'api_key': api_keys[nr]
+ 'api_key': api_keys[nr],
+ 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
})
return list
diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/providers/nzb/nzbclub/__init__.py
index 7859fe9c..9955462c 100644
--- a/couchpotato/core/providers/nzb/nzbclub/__init__.py
+++ b/couchpotato/core/providers/nzb/nzbclub/__init__.py
@@ -18,6 +18,14 @@ config = [{
'name': 'enabled',
'type': 'enabler',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/providers/nzb/nzbindex/__init__.py
index 29eb0d38..aa8de4dd 100644
--- a/couchpotato/core/providers/nzb/nzbindex/__init__.py
+++ b/couchpotato/core/providers/nzb/nzbindex/__init__.py
@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/nzbsrus/__init__.py b/couchpotato/core/providers/nzb/nzbsrus/__init__.py
index 3a042784..863e6a37 100644
--- a/couchpotato/core/providers/nzb/nzbsrus/__init__.py
+++ b/couchpotato/core/providers/nzb/nzbsrus/__init__.py
@@ -35,6 +35,14 @@ config = [{
'label': 'English only',
'description': 'Only search for English spoken movies on Nzbsrus',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/nzbx/__init__.py b/couchpotato/core/providers/nzb/nzbx/__init__.py
index 9ce9226b..9cf80638 100644
--- a/couchpotato/core/providers/nzb/nzbx/__init__.py
+++ b/couchpotato/core/providers/nzb/nzbx/__init__.py
@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
index 287ced49..fe80518c 100644
--- a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
+++ b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
@@ -27,6 +27,14 @@ config = [{
'label': 'Api Key',
'default': '',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'default': 20,
+ 'type': 'int',
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/iptorrents/__init__.py b/couchpotato/core/providers/torrent/iptorrents/__init__.py
index bca8ce69..24f9772b 100644
--- a/couchpotato/core/providers/torrent/iptorrents/__init__.py
+++ b/couchpotato/core/providers/torrent/iptorrents/__init__.py
@@ -34,6 +34,14 @@ config = [{
'type': 'bool',
'description': 'Only search for [FreeLeech] torrents.',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py
index 8ddb1f4a..999dbb1b 100644
--- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py
+++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py
@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py
index 06be7a89..a9f687c3 100644
--- a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py
+++ b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py
@@ -37,6 +37,14 @@ config = [{
{
'name': 'passkey',
'default': '',
+ },
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 20,
+ 'description': 'Starting score for each release found via this provider.',
}
],
}
diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py
index 2c356e20..3c27cf48 100644
--- a/couchpotato/core/providers/torrent/publichd/__init__.py
+++ b/couchpotato/core/providers/torrent/publichd/__init__.py
@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/providers/torrent/sceneaccess/__init__.py
index e12bf8bd..baad57f6 100644
--- a/couchpotato/core/providers/torrent/sceneaccess/__init__.py
+++ b/couchpotato/core/providers/torrent/sceneaccess/__init__.py
@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 20,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/scenehd/__init__.py b/couchpotato/core/providers/torrent/scenehd/__init__.py
index 10c5e385..3cd2132e 100644
--- a/couchpotato/core/providers/torrent/scenehd/__init__.py
+++ b/couchpotato/core/providers/torrent/scenehd/__init__.py
@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
index 38169084..f2394dd6 100644
--- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py
+++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
@@ -24,6 +24,14 @@ config = [{
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
+ },
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
}
],
}
diff --git a/couchpotato/core/providers/torrent/torrentday/__init__.py b/couchpotato/core/providers/torrent/torrentday/__init__.py
index 1a4d3c7f..de715b53 100644
--- a/couchpotato/core/providers/torrent/torrentday/__init__.py
+++ b/couchpotato/core/providers/torrent/torrentday/__init__.py
@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 0,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/providers/torrent/torrentleech/__init__.py
index d96ac064..fa048d50 100644
--- a/couchpotato/core/providers/torrent/torrentleech/__init__.py
+++ b/couchpotato/core/providers/torrent/torrentleech/__init__.py
@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'type': 'int',
+ 'default': 20,
+ 'description': 'Starting score for each release found via this provider.',
+ }
],
},
],
diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py
index 6de18fbd..4247c530 100644
--- a/couchpotato/core/providers/torrent/torrentleech/main.py
+++ b/couchpotato/core/providers/torrent/torrentleech/main.py
@@ -74,3 +74,6 @@ class TorrentLeech(TorrentProvider):
'remember_me': 'on',
'login': 'submit',
})
+
+ def loginSuccess(self, output):
+ return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
diff --git a/couchpotato/core/providers/userscript/criticker/__init__.py b/couchpotato/core/providers/userscript/criticker/__init__.py
new file mode 100644
index 00000000..129d878f
--- /dev/null
+++ b/couchpotato/core/providers/userscript/criticker/__init__.py
@@ -0,0 +1,6 @@
+from .main import Criticker
+
+def start():
+ return Criticker()
+
+config = []
diff --git a/couchpotato/core/providers/userscript/criticker/main.py b/couchpotato/core/providers/userscript/criticker/main.py
new file mode 100644
index 00000000..680d9a36
--- /dev/null
+++ b/couchpotato/core/providers/userscript/criticker/main.py
@@ -0,0 +1,6 @@
+from couchpotato.core.providers.userscript.base import UserscriptBase
+
+
+class Criticker(UserscriptBase):
+
+ includes = ['http://www.criticker.com/film/*']
diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py
index 00f77a64..45443fcb 100644
--- a/couchpotato/core/settings/__init__.py
+++ b/couchpotato/core/settings/__init__.py
@@ -189,6 +189,9 @@ class Settings(object):
self.set(section, option, (new_value if new_value else value).encode('unicode_escape'))
self.save()
+ # After save (for re-interval etc)
+ fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
+
return jsonified({
'success': True,
})
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index 141ee4fe..6c8deb1e 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -76,7 +76,7 @@ class LibraryTitle(Entity):
title = Field(Unicode)
simple_title = Field(Unicode, index = True)
- default = Field(Boolean, index = True)
+ default = Field(Boolean, default = False, index = True)
language = OneToMany('Language')
libraries = ManyToOne('Library')
@@ -141,12 +141,12 @@ class Status(Entity):
class Quality(Entity):
- """Quality name of a release, DVD, 720P, DVD-Rip etc"""
+ """Quality name of a release, DVD, 720p, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(20))
- order = Field(Integer, index = True)
+ order = Field(Integer, default = 0, index = True)
size_min = Field(Integer)
size_max = Field(Integer)
@@ -160,21 +160,27 @@ class Profile(Entity):
using_options(order_by = 'order')
label = Field(Unicode(50))
- order = Field(Integer, index = True)
- core = Field(Boolean)
- hide = Field(Boolean)
+ order = Field(Integer, default = 0, index = True)
+ core = Field(Boolean, default = False)
+ hide = Field(Boolean, default = False)
movie = OneToMany('Movie')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
+ def to_dict(self, deep = {}, exclude = []):
+ orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude)
+ orig_dict['core'] = orig_dict.get('core') or False
+ orig_dict['hide'] = orig_dict.get('hide') or False
+
+ return orig_dict
class ProfileType(Entity):
""""""
using_options(order_by = 'order')
- order = Field(Integer, index = True)
- finish = Field(Boolean)
- wait_for = Field(Integer)
+ order = Field(Integer, default = 0, index = True)
+ finish = Field(Boolean, default = True)
+ wait_for = Field(Integer, default = 0)
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@@ -185,7 +191,7 @@ class File(Entity):
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer, default = 1)
- available = Field(Boolean)
+ available = Field(Boolean, default = True)
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index de538485..26522178 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -190,9 +190,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
- if current_db_version < latest_db_version and not development:
- log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
- upgrade(db, repo)
+ if current_db_version < latest_db_version:
+ if development:
+ log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.')
+ else:
+ log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
+ upgrade(db, repo)
# Configure Database
from couchpotato.core.settings.model import setup
@@ -238,7 +241,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from tornado.ioloop import IOLoop
web_container = WSGIContainer(app)
web_container._log = _log
- loop = IOLoop.instance()
+ loop = IOLoop.current()
+
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
diff --git a/couchpotato/static/fonts/Elusive-Icons.eot b/couchpotato/static/fonts/Elusive-Icons.eot
new file mode 100755
index 00000000..282e6089
Binary files /dev/null and b/couchpotato/static/fonts/Elusive-Icons.eot differ
diff --git a/couchpotato/static/fonts/Elusive-Icons.svg b/couchpotato/static/fonts/Elusive-Icons.svg
new file mode 100755
index 00000000..bfc6c9b7
--- /dev/null
+++ b/couchpotato/static/fonts/Elusive-Icons.svg
@@ -0,0 +1,298 @@
+
+
+
\ No newline at end of file
diff --git a/couchpotato/static/fonts/Elusive-Icons.ttf b/couchpotato/static/fonts/Elusive-Icons.ttf
new file mode 100755
index 00000000..ae31768f
Binary files /dev/null and b/couchpotato/static/fonts/Elusive-Icons.ttf differ
diff --git a/couchpotato/static/fonts/Elusive-Icons.woff b/couchpotato/static/fonts/Elusive-Icons.woff
new file mode 100755
index 00000000..7f974bd4
Binary files /dev/null and b/couchpotato/static/fonts/Elusive-Icons.woff differ
diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.eot b/couchpotato/static/fonts/OpenSans-Bold-webfont.eot
new file mode 100755
index 00000000..e1c76744
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Bold-webfont.eot differ
diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.svg b/couchpotato/static/fonts/OpenSans-Bold-webfont.svg
new file mode 100755
index 00000000..364b3686
--- /dev/null
+++ b/couchpotato/static/fonts/OpenSans-Bold-webfont.svg
@@ -0,0 +1,146 @@
+
+
+
\ No newline at end of file
diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.ttf b/couchpotato/static/fonts/OpenSans-Bold-webfont.ttf
new file mode 100755
index 00000000..2d94f062
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Bold-webfont.ttf differ
diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.woff b/couchpotato/static/fonts/OpenSans-Bold-webfont.woff
new file mode 100755
index 00000000..cd86852d
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Bold-webfont.woff differ
diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.eot b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.eot
new file mode 100755
index 00000000..f44ac9a3
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.eot differ
diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg
new file mode 100755
index 00000000..8392240a
--- /dev/null
+++ b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg
@@ -0,0 +1,146 @@
+
+
+
\ No newline at end of file
diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf
new file mode 100755
index 00000000..f74e0e3c
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf differ
diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff
new file mode 100755
index 00000000..f3248c11
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff differ
diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.eot b/couchpotato/static/fonts/OpenSans-Italic-webfont.eot
new file mode 100755
index 00000000..277c1899
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Italic-webfont.eot differ
diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.svg b/couchpotato/static/fonts/OpenSans-Italic-webfont.svg
new file mode 100755
index 00000000..29c7497f
--- /dev/null
+++ b/couchpotato/static/fonts/OpenSans-Italic-webfont.svg
@@ -0,0 +1,146 @@
+
+
+
\ No newline at end of file
diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.ttf b/couchpotato/static/fonts/OpenSans-Italic-webfont.ttf
new file mode 100755
index 00000000..63f187e9
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Italic-webfont.ttf differ
diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.woff b/couchpotato/static/fonts/OpenSans-Italic-webfont.woff
new file mode 100755
index 00000000..469a29bb
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Italic-webfont.woff differ
diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.eot b/couchpotato/static/fonts/OpenSans-Regular-webfont.eot
new file mode 100755
index 00000000..dd6fd2cb
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Regular-webfont.eot differ
diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.svg b/couchpotato/static/fonts/OpenSans-Regular-webfont.svg
new file mode 100755
index 00000000..01038bb1
--- /dev/null
+++ b/couchpotato/static/fonts/OpenSans-Regular-webfont.svg
@@ -0,0 +1,146 @@
+
+
+
\ No newline at end of file
diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.ttf b/couchpotato/static/fonts/OpenSans-Regular-webfont.ttf
new file mode 100755
index 00000000..05951e7b
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Regular-webfont.ttf differ
diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.woff b/couchpotato/static/fonts/OpenSans-Regular-webfont.woff
new file mode 100755
index 00000000..274664b2
Binary files /dev/null and b/couchpotato/static/fonts/OpenSans-Regular-webfont.woff differ
diff --git a/couchpotato/static/images/couch.png b/couchpotato/static/images/couch.png
index 3bc445e7..5b7efc70 100644
Binary files a/couchpotato/static/images/couch.png and b/couchpotato/static/images/couch.png differ
diff --git a/couchpotato/static/images/gear.png b/couchpotato/static/images/gear.png
index f1d63bad..d2aad53c 100644
Binary files a/couchpotato/static/images/gear.png and b/couchpotato/static/images/gear.png differ
diff --git a/couchpotato/static/images/icon.download.png b/couchpotato/static/images/icon.download.png
index e64e9997..d8283918 100644
Binary files a/couchpotato/static/images/icon.download.png and b/couchpotato/static/images/icon.download.png differ
diff --git a/couchpotato/static/images/icon.readd.png b/couchpotato/static/images/icon.readd.png
index dacb432f..b509fca9 100644
Binary files a/couchpotato/static/images/icon.readd.png and b/couchpotato/static/images/icon.readd.png differ
diff --git a/couchpotato/static/images/notify.couch.large.png b/couchpotato/static/images/notify.couch.large.png
new file mode 100644
index 00000000..e918c049
Binary files /dev/null and b/couchpotato/static/images/notify.couch.large.png differ
diff --git a/couchpotato/static/images/notify.couch.medium.png b/couchpotato/static/images/notify.couch.medium.png
new file mode 100644
index 00000000..939aca40
Binary files /dev/null and b/couchpotato/static/images/notify.couch.medium.png differ
diff --git a/couchpotato/static/images/xbmc-notify.png b/couchpotato/static/images/notify.couch.small.png
similarity index 100%
rename from couchpotato/static/images/xbmc-notify.png
rename to couchpotato/static/images/notify.couch.small.png
diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png
index 5ba4d00e..0ef30413 100644
Binary files a/couchpotato/static/images/sprite.png and b/couchpotato/static/images/sprite.png differ
diff --git a/couchpotato/static/scripts/block/menu.js b/couchpotato/static/scripts/block/menu.js
index 4dc143d4..cdd7875b 100644
--- a/couchpotato/static/scripts/block/menu.js
+++ b/couchpotato/static/scripts/block/menu.js
@@ -15,7 +15,7 @@ Block.Menu = new Class({
self.wrapper = new Element('div.wrapper').adopt(
self.more_option_ul = new Element('ul')
),
- new Element('a.button.onlay', {
+ new Element('a.button.onlay' + (self.options.button_class ? '.' + self.options.button_class : ''), {
'events': {
'click': function(){
self.el.toggleClass('show')
diff --git a/couchpotato/static/scripts/block/navigation.js b/couchpotato/static/scripts/block/navigation.js
index 85f20c49..feda3795 100644
--- a/couchpotato/static/scripts/block/navigation.js
+++ b/couchpotato/static/scripts/block/navigation.js
@@ -5,7 +5,17 @@ Block.Navigation = new Class({
create: function(){
var self = this;
+ var settings_added = false;
self.el = new Element('div.navigation').adopt(
+ self.foldout = new Element('a.foldout.icon2.menu', {
+ 'events': {
+ 'click': self.toggleMenu.bind(self)
+ }
+ }).grab(new Element('span.overlay')),
+ self.logo = new Element('a.logo', {
+ 'text': 'CouchPotato',
+ 'href': App.createUrl('')
+ }),
self.nav = new Element('ul'),
self.backtotop = new Element('a.backtotop', {
'text': 'back to top',
@@ -28,19 +38,50 @@ Block.Navigation = new Class({
onEnter: function(){
self.backtotop.fade('in')
}
+ });
+
+ self.nav.addEvents({
+ 'click:relay(a)': function(){
+ if($(document.body).getParent().hasClass('menu_shown'))
+ self.toggleMenu();
+ }
})
},
addTab: function(name, tab){
- var self = this
+ var self = this;
- return new Element('li.tab_'+(name || 'unknown')).adopt(
+ return new Element('li.tab_'+(name || 'unknown')).grab(
new Element('a', tab)
).inject(self.nav)
},
+ toggleMenu: function(e){
+ var self = this,
+ body = $(document.body),
+ html = body.getParent();
+
+ // Copy over settings menu
+ 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))
+ );
+ }
+ });
+
+ self.added = true;
+ }
+
+ html.toggleClass('menu_shown');
+
+ },
+
activate: function(name){
var self = this;
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index d2e8fa00..673323b6 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -61,7 +61,7 @@ var CouchPotato = new Class({
new Element('div').adopt(
self.block.navigation = new Block.Navigation(self, {}),
self.block.search = new Block.Search(self, {}),
- self.block.more = new Block.Menu(self, {})
+ self.block.more = new Block.Menu(self, {'button_class': 'icon2.cog'})
)
),
self.content = new Element('div.content'),
@@ -285,23 +285,15 @@ var CouchPotato = new Class({
createUserscriptButtons: function(){
- var userscript = false;
- try {
- if(Components.interfaces.gmIGreasemonkeyService)
- userscript = true
- }
- catch(e){
- userscript = Browser.chrome === true;
- }
-
var host_url = window.location.protocol + '//' + window.location.host;
return new Element('div.group_userscript').adopt(
- (userscript ? [new Element('a.userscript.button', {
+ new Element('a.userscript.button', {
'text': 'Install userscript',
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
- 'target': '_self'
- }), new Element('span.or[text=or]')] : null),
+ 'target': '_blank'
+ }),
+ new Element('span.or[text=or]'),
new Element('span.bookmarklet').adopt(
new Element('a.button.orange', {
'text': '+CouchPotato',
diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js
deleted file mode 100644
index b6d9812a..00000000
--- a/couchpotato/static/scripts/library/prefix_free.js
+++ /dev/null
@@ -1,487 +0,0 @@
-/**
- * StyleFix 1.0.3 & PrefixFree 1.0.7
- * @author Lea Verou
- * MIT license
- */
-
-(function(){
-
-if(!window.addEventListener) {
- return;
-}
-
-var self = window.StyleFix = {
- link: function(link) {
- try {
- // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets
- if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) {
- return;
- }
- }
- catch(e) {
- return;
- }
-
- var url = link.href || link.getAttribute('data-href'),
- base = url.replace(/[^\/]+$/, ''),
- base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0],
- base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0],
- base_query = /^([^?]*)\??/.exec(url)[1],
- parent = link.parentNode,
- xhr = new XMLHttpRequest(),
- process;
-
- xhr.onreadystatechange = function() {
- if(xhr.readyState === 4) {
- process();
- }
- };
-
- process = function() {
- var css = xhr.responseText;
-
- if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) {
- css = self.fix(css, true, link);
-
- // Convert relative URLs to absolute, if needed
- if(base) {
- css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) {
- if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative
- return $0;
- }
- else if(/^\/\//.test(url)) { // Scheme-relative
- // May contain sequences like /../ and /./ but those DO work
- return 'url("' + base_scheme + url + '")';
- }
- else if(/^\//.test(url)) { // Domain-relative
- return 'url("' + base_domain + url + '")';
- }
- else if(/^\?/.test(url)) { // Query-relative
- return 'url("' + base_query + url + '")';
- }
- else {
- // Path-relative
- return 'url("' + base + url + '")';
- }
- });
-
- // behavior URLs shoudn’t be converted (Issue #19)
- // base should be escaped before added to RegExp (Issue #81)
- var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1");
- css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1');
- }
-
- var style = document.createElement('style');
- style.textContent = css;
- style.media = link.media;
- style.disabled = link.disabled;
- style.setAttribute('data-href', link.getAttribute('href'));
-
- parent.insertBefore(style, link);
- parent.removeChild(link);
-
- style.media = link.media; // Duplicate is intentional. See issue #31
- }
- };
-
- try {
- xhr.open('GET', url);
- xhr.send(null);
- } catch (e) {
- // Fallback to XDomainRequest if available
- if (typeof XDomainRequest != "undefined") {
- xhr = new XDomainRequest();
- xhr.onerror = xhr.onprogress = function() {};
- xhr.onload = process;
- xhr.open("GET", url);
- xhr.send(null);
- }
- }
-
- link.setAttribute('data-inprogress', '');
- },
-
- styleElement: function(style) {
- if (style.hasAttribute('data-noprefix')) {
- return;
- }
- var disabled = style.disabled;
-
- style.textContent = self.fix(style.textContent, true, style);
-
- style.disabled = disabled;
- },
-
- styleAttribute: function(element) {
- var css = element.getAttribute('style');
-
- css = self.fix(css, false, element);
-
- element.setAttribute('style', css);
- },
-
- process: function() {
- // Linked stylesheets
- $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link);
-
- // Inline stylesheets
- $('style').forEach(StyleFix.styleElement);
-
- // Inline styles
- $('[style]').forEach(StyleFix.styleAttribute);
- },
-
- register: function(fixer, index) {
- (self.fixers = self.fixers || [])
- .splice(index === undefined? self.fixers.length : index, 0, fixer);
- },
-
- fix: function(css, raw, element) {
- for(var i=0; i -1) {
- // Gradients are supported with a prefix, convert angles to legacy
- css = css.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig, function ($0, delim, repeating, deg) {
- return delim + (repeating || '') + 'linear-gradient(' + (90-deg) + 'deg';
- });
- }
-
- css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css);
- css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css);
- css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css);
-
- // Prefix properties *inside* values (issue #8)
- if (self.properties.length) {
- var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi');
-
- css = fix('valueProperties', '\\b', ':(.+?);', function($0) {
- return $0.replace(regex, prefix + "$1")
- }, css);
- }
-
- if(raw) {
- css = fix('selectors', '', '\\b', self.prefixSelector, css);
- css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css);
- }
-
- // Fix double prefixing
- css = css.replace(RegExp('-' + prefix, 'g'), '-');
-
- // Prefix wildcard
- css = css.replace(/-\*-(?=[a-z]+)/gi, self.prefix);
-
- return css;
- },
-
- property: function(property) {
- return (self.properties.indexOf(property)? self.prefix : '') + property;
- },
-
- value: function(value, property) {
- value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value);
- value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value);
-
- // TODO properties inside values
-
- return value;
- },
-
- // Warning: Prefixes no matter what, even if the selector is supported prefix-less
- prefixSelector: function(selector) {
- return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix })
- },
-
- // Warning: Prefixes no matter what, even if the property is supported prefix-less
- prefixProperty: function(property, camelCase) {
- var prefixed = self.prefix + property;
-
- return camelCase? StyleFix.camelCase(prefixed) : prefixed;
- }
-};
-
-/**************************************
- * Properties
- **************************************/
-(function() {
- var prefixes = {},
- properties = [],
- shorthands = {},
- style = getComputedStyle(document.documentElement, null),
- dummy = document.createElement('div').style;
-
- // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those.
- var iterate = function(property) {
- if(property.charAt(0) === '-') {
- properties.push(property);
-
- var parts = property.split('-'),
- prefix = parts[1];
-
- // Count prefix uses
- prefixes[prefix] = ++prefixes[prefix] || 1;
-
- // This helps determining shorthands
- while(parts.length > 3) {
- parts.pop();
-
- var shorthand = parts.join('-');
-
- if(supported(shorthand) && properties.indexOf(shorthand) === -1) {
- properties.push(shorthand);
- }
- }
- }
- },
- supported = function(property) {
- return StyleFix.camelCase(property) in dummy;
- }
-
- // Some browsers have numerical indices for the properties, some don't
- if(style.length > 0) {
- for(var i=0; iMaybe add a movie.',
+ 'events': {
+ 'click': function(){
+ $(document.body).getElement('.search_form input').focus();
+ }
+ }
+ })
+ ),
'filter': {
'release_status': 'snatched,available'
}
@@ -35,11 +46,11 @@ Page.Home = new Class({
self.soon_list = new MovieList({
'navigation': false,
'identifier': 'soon',
- 'limit': 18,
+ 'limit': 12,
'title': 'Available soon',
'description': 'These are being searched for and should be available soon as they will be released on DVD in the next few weeks.',
'on_empty_element': new Element('div').adopt(
- new Element('h1', {'text': 'Available soon'}),
+ new Element('h2', {'text': 'Available soon'}),
new Element('span', {'text': 'There are no movies available soon. Add some movies, so you have something to watch later.'})
),
'filter': {
@@ -48,9 +59,41 @@ Page.Home = new Class({
'actions': [MA.IMDB, MA.Refresh],
'load_more': false,
'view': 'thumbs',
+ 'force_view': true,
'api_call': 'dashboard.soon'
});
+ // Make all thumbnails the same size
+ self.soon_list.addEvent('loaded', function(){
+ var images = $(self.soon_list).getElements('img'),
+ timer,
+ lowest = null;
+
+ 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)
+ }
+ });
+
+ $(window).addEvent('resize', function(){
+ if(timer) clearTimeout(timer);
+ timer = (function(){
+ var lowest;
+ images.each(function(img){
+ var height = img.getSize().y;
+ if(!lowest || lowest > height)
+ lowest = height;
+ });
+ images.getParent().setStyle('height', lowest);
+ }).delay(300);
+ });
+ });
+
// Still not available
self.late_list = new MovieList({
'navigation': false,
@@ -58,10 +101,10 @@ Page.Home = new Class({
'limit': 50,
'title': 'Still not available',
'description': 'Try another quality profile or maybe add more providers in Settings.',
- 'on_empty_element': new Element('div'),
'filter': {
'late': true
},
+ 'loader': false,
'load_more': false,
'view': 'list',
'actions': [MA.IMDB, MA.Trailer, MA.Edit, MA.Refresh, MA.Delete],
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index ddb2abf1..6e563bff 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -297,7 +297,7 @@ Page.Settings = new Class({
return group_el
},
-
+
createList: function(content_container){
return new Element('div.option_list').grab(
new Element('h3', {
@@ -1283,6 +1283,7 @@ Option.Combined = new Class({
self.values = {}
self.inputs = {}
self.items = []
+ self.labels = {}
self.options.combine.each(function(name){
@@ -1302,9 +1303,10 @@ Option.Combined = new Class({
var head = new Element('div.head').inject(self.combined_list)
Object.each(self.inputs, function(input, name){
+ self.labels[name] = input.getPrevious().get('text')
new Element('abbr', {
'class': name,
- 'text': input.getPrevious().get('text'),
+ 'text': self.labels[name],
//'title': input.getNext().get('text')
}).inject(head)
})
@@ -1367,7 +1369,7 @@ Option.Combined = new Class({
value_count++;
new Element('input[type=text].inlay.'+name, {
'value': value,
- 'placeholder': name,
+ 'placeholder': self.labels[name] || name,
'events': {
'keyup': self.saveCombined.bind(self),
'change': self.saveCombined.bind(self)
diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index 6e329973..eabd1465 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -30,7 +30,7 @@ Page.Wanted = new Class({
$(self.wanted).inject(self.el);
// Check if search is in progress
- self.startProgressInterval();
+ self.startProgressInterval.delay(4000, self);
}
},
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index 3ad502b9..02dda3d1 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -1,19 +1,14 @@
-html {
+body, html {
color: #fff;
font-size: 12px;
line-height: 1.5;
- font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
+ font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
height: 100%;
- text-shadow: 0 1px 0 #000;
-}
-
-body {
margin: 0;
padding: 0;
background: #4e5969;
- overflow-y: scroll;
- height: 100%;
}
+ body { overflow-y: scroll; }
body.noscroll { overflow: hidden; }
#clean {
@@ -32,14 +27,16 @@ pre {
}
input, textarea {
- font-size: 12px;
+ font-size: 1em;
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
}
+input:focus, textarea:focus {
+ outline: none;
+}
input:-moz-placeholder, textarea:-moz-placeholder {
color: rgba(255, 255, 255, 0.6);
}
-
::-webkit-input-placeholder, ::-webkit-textarea-placeholder {
color: rgba(255, 255, 255, 0.6);
}
@@ -59,53 +56,30 @@ a:hover { color: #f3f3f3; }
.page {
display: none;
- width: 960px;
+ width: 100%;
+ max-width: 980px;
margin: 0 auto;
- line-height: 24px;
- padding: 0 0 20px;
+ line-height: 1.5em;
+ padding: 0 15px 20px;
}
.page.active { display: block; }
- .page .noticeMe {
- background-color: lightgoldenrodyellow;
- display: block;
- padding: 20px 10px;
- margin: 0 -10px 40px;
- font-size: 19px;
- text-align: center;
- }
-
.content {
clear:both;
- padding: 80px 0 10px;
+ padding: 65px 0 10px;
+ background: #4e5969;
}
-h2 {
- font-size: 30px;
- padding: 0;
- margin: 20px 0 0 0;
-}
-
-.footer {
- text-align:center;
- padding: 50px 0 0 0;
- color: #999;
- font-size: 10px;
- clear: both;
-}
-
- .footer .check {
- color: #333;
+ @media all and (max-width: 480px) {
+ .content {
+ padding-top: 40px;
+ }
}
-#toTop {
- background: black;
- position: fixed;
- bottom: 0;
- right: 0;
- padding: 10px 10px 10px 40px;
- background: #f7f7f7 url('../images/toTop.gif') no-repeat 10px center;
- border-radius: 5px 0 0 0;
+h2 {
+ font-size: 2.5em;
+ padding: 0;
+ margin: 20px 0 0 0;
}
form {
@@ -126,6 +100,12 @@ body > .spinner, .mask{
width: 100%;
padding: 200px;
}
+
+ @media all and (max-width: 480px) {
+ body > .mask {
+ padding: 20px;
+ }
+ }
.button {
background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x;
@@ -134,10 +114,7 @@ body > .spinner, .mask{
text-decoration: none;
font-weight: bold;
line-height: 1;
- border-radius: 5px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.5);
- text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
- border-bottom: 1px solid rgba(0,0,0,0.25);
+ border-radius: 2px;
cursor: pointer;
}
.button.red { background-color: #ff0000; }
@@ -165,132 +142,302 @@ body > .spinner, .mask{
.icon.spinner { background-image: url('../images/icon.spinner.gif'); }
.icon.attention { background-image: url('../images/icon.attention.png'); }
+.icon2 {
+ display: inline-block;
+ background: center no-repeat;
+ font-family: 'Elusive-Icons';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ font-size: 15px;
+}
+
+.icon2.cog:before { content: "\e109"; }
+.icon2.eye-open:before { content: "\e09d"; }
+.icon2.search:before { content: "\e03e"; }
+.icon2.return-key:before { content: "\e111"; }
+.icon2.close:before { content: "\e04e"; }
+.icon2.menu:before {
+ content: "\e076 \e076 \e076";
+ line-height: 6px;
+ transform: scaleX(2);
+ width: 20px;
+ font-size: 10px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
/*** Navigation ***/
.header {
- background: #4e5969;
- padding: 10px 0;
- height: 80px;
+ height: 66px;
position: fixed;
margin: 0;
width: 100%;
z-index: 5;
- box-shadow: 0 20px 30px -30px rgba(0,0,0,0.05);
- transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1);
+ background: #4e5969;
+ box-shadow: 0 0 10px rgba(0,0,0,.1);
+ transition: all .4s ease-in-out;
}
.header.with_shadow {
- box-shadow: 0 20px 30px -30px rgba(0,0,0,0.3);
+ background-color: #46505e;
+ }
+
+ @media all and (max-width: 480px) {
+ .header {
+ height: 44px;
+ }
}
.header > div {
- width: 960px;
+ width: 100%;
+ max-width: 980px;
margin: 0 auto;
- overflow: hidden;
+ position: relative;
+ height: 100%;
+ padding: 0 15px;
}
+
.header .navigation {
display: inline-block;
vertical-align: middle;
- width: 67.2%;
+ position: absolute;
+ height: 100%;
+ left: 0;
+ bottom: 0;
}
- .header .navigation ul {
- margin: 0;
- padding: 0;
- }
- .header .navigation li {
- color: #fff;
- display: inline-block;
- font-size:20px;
- font-weight: bold;
- margin: 0;
- text-align: center;
- }
-
- .header .navigation li a {
- display: block;
- padding: 15px;
- position: relative;
- }
- .header .navigation li:first-child a { padding-left: 10px; }
- .header .navigation li span {
- display: block;
- margin-top: 5px;
- }
-
- .header .navigation li a:after {
- content: '';
- display: inline-block;
- height: 2px;
- width: 76%;
- left: 12%;
- position: absolute;
- top: 46px;
- background-color: #46505e;
- outline: none;
- box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
- transition: all .4s cubic-bezier(0.9,0,0.1,1);
- }
-
- .header .navigation li:hover a:after { background-color: #047792; }
- .header .navigation li.active a:after { background-color: #04bce6; }
-
- .header .navigation li.disabled { color: #e5e5e5; }
- .header .navigation li a { color: #fff; }
-
- .header .navigation .backtotop {
- opacity: 0;
- display: block;
- width: 80px;
- left: 50%;
- position: absolute;
+
+ .header .foldout {
+ width: 44px;
+ height: 100%;
text-align: center;
- margin: -10px 0 0 -40px;
- background: #4e5969;
- padding: 5px 0;
- border-radius: 0 0 5px 5px;
- color: rgba(255,255,255,.4);
- text-shadow: none;
- font-weight: normal;
+ border-right: 1px solid rgba(255,255,255,.07);
+ display: none;
+ vertical-align: top;
+ line-height: 42px;
+ color: #FFF;
+ }
+
+ .header .logo {
+ display: inline-block;
+ font-size: 1.75em;
+ padding: 15px 30px 0 15px;
+ height: 100%;
+ vertical-align: middle;
+ border-right: 1px solid rgba(255,255,255,.07);
+ color: #FFF;
+ font-weight: normal;
+ vertical-align: top;
+ }
+
+ @media all and (max-width: 480px) {
+ .header .foldout {
+ display: inline-block;
+ }
+
+ .header .logo {
+ padding-top: 7px;
+ border: 0;
+ }
+ }
+
+ @media all and (min-width: 481px) and (max-width: 640px) {
+
+ .header .logo {
+ display: none;
+ }
+
+ }
+
+ .header .navigation ul {
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ }
+
+ .header .navigation li {
+ color: #fff;
+ display: inline-block;
+ font-size: 1.75em;
+ margin: 0;
+ text-align: center;
+ height: 100%;
+ border: 1px solid rgba(255,255,255,.07);
+ border-width: 0 0 0 1px;
+ }
+ .header .navigation li:first-child {
+ border: none;
+ }
+
+ .header .navigation li a {
+ display: block;
+ padding: 15px;
+ position: relative;
+ height: 100%;
+ border: 1px solid transparent;
+ border-width: 0 0 4px 0;
+ font-weight: normal;
+ }
+
+ .header .navigation li:hover a { border-color: #047792; }
+ .header .navigation li.active a { border-color: #04bce6; }
+
+ .header .navigation li.disabled { color: #e5e5e5; }
+ .header .navigation li a { color: #fff; }
+
+ .header .navigation .backtotop {
+ opacity: 0;
+ display: block;
+ width: 80px;
+ left: 50%;
+ position: fixed;
+ bottom: 0;
+ text-align: center;
+ margin: -10px 0 0 -40px;
+ background: #4e5969;
+ padding: 5px 0;
+ color: rgba(255,255,255,.4);
+ font-weight: normal;
+ }
+ .header:hover .navigation .backtotop { color: #fff; }
+
+ @media all and (max-width: 480px) {
+
+ body {
+ position: absolute;
+ width: 100%;
+ transition: all .5s cubic-bezier(0.9,0,0.1,1);
+ left: 0;
+ }
+
+ .menu_shown body {
+ left: 160px;
+ }
+
+ .header .navigation {
+ height: 100%;
+ }
+
+ .menu_shown .header .navigation .overlay {
+ position: fixed;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ left: 160px;
+ }
+
+ .header .navigation ul {
+ width: 160px;
+ position: fixed;
+ left: -160px;
+ background: rgba(0,0,0,.5);
+ transition: all .5s cubic-bezier(0.9,0,0.1,1);
+ }
+
+ .menu_shown .header .navigation ul {
+ left: 0;
+ }
+
+ .header .navigation ul li {
+ display: block;
+ text-align: left;
+ border-width: 1px 0 0 0;
+ height: 44px;
+ }
+ .header .navigation ul li a {
+ border-width: 0 4px 0 0;
+ padding: 5px 20px;
+ }
+
+ .header .navigation ul li.separator {
+ background-color: rgba(255,255,255, .07);
+ height: 5px;
+ }
}
- .header:hover .navigation .backtotop { color: #fff; }
.header .more_menu {
- margin-left: 12px;
+ position: absolute;
+ right: 15px;
+ height: 100%;
+ border-left: 1px solid rgba(255,255,255,.07);
}
+
+ @media all and (max-width: 480px) {
+ .header .more_menu {
+ display: none;
+ }
+ }
+
+ .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;
- margin-left: -110px;
- }
- .header .more_menu .wrapper:before {
- margin-left: -34px;
+ margin-left: -106px;
+ margin-top: 66px;
}
+
+ @media all and (max-width: 480px) {
+ .header .more_menu .button {
+ line-height: 44px;
+ }
+
+ .header .more_menu .wrapper {
+ margin-top: 44px;
+ }
+ }
.header .more_menu .red { color: red; }
.header .more_menu .orange { color: orange; }
.badge {
position: absolute;
- width: 14px;
- height: 14px;
+ width: 20px;
+ height: 20px;
text-align: center;
- line-height: 14px;
- border-radius: 50%;
- font-size: 8px;
- margin: -5px 0 0 15px;
- box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7);
+ line-height: 20px;
+ margin: 0;
background-color: #1b79b8;
- text-shadow: none;
- background-image: -*-linear-gradient(0deg, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%);
+ top: 0;
+ right: 0;
}
+
+ .header .notification_menu {
+ right: 60px;
+ display: block;
+ }
+
+ @media all and (max-width: 480px) {
+ .header .notification_menu {
+ right: 0;
+ }
+ }
.header .notification_menu .wrapper {
width: 300px;
- margin-left: -260px;
+ margin-left: -255px;
text-align: left;
}
- .header .notification_menu .wrapper:before {
- left: 296px;
- }
-
.header .notification_menu ul {
max-height: 300px;
overflow: auto;
@@ -310,7 +457,7 @@ body > .spinner, .mask{
.header .notification_menu li:last-child > span { border: 0; }
.header .notification_menu li .added {
display: block;
- font-size: 10px;
+ font-size: .85em;
color: #aaa;
text-align: ;
}
@@ -319,25 +466,27 @@ body > .spinner, .mask{
text-align: center;
}
- .header .message.update {
+ .message.update {
text-align: center;
- position: relative;
- top: -70px;
- padding: 2px 0;
+ position: fixed;
+ padding: 10px;
background: #ff6134;
- font-size: 12px;
- border-radius: 0 0 5px 5px;
- box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
+ font-size: 15px;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 19;
}
- .header .message a {
- padding: 0 10px;
+ .message.update a {
+ padding: 0 5px;
}
/*** Global Styles ***/
.check {
display: inline-block;
- vertical-align: middle;
+ vertical-align: top;
+ margin-top: 4px;
height: 16px;
width: 16px;
cursor: pointer;
@@ -362,8 +511,8 @@ body > .spinner, .mask{
border-radius:30px;
box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
- background: url('../images/sprite.png') no-repeat 94% -53px, -*-linear-gradient(
- 270deg,
+ background: url('../images/sprite.png') no-repeat 94% -53px, linear-gradient(
+ 180deg,
#5b9bd1 0%,
#406db8 100%
);
@@ -438,8 +587,8 @@ body > .spinner, .mask{
border: 1px solid #252930;
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
background: rgb(55,62,74);
- background-image: -*-linear-gradient(
- 90deg,
+ background-image: linear-gradient(
+ 0,
rgb(55,62,74) 0%,
rgb(73,83,98) 100%
);
@@ -455,13 +604,9 @@ body > .spinner, .mask{
display: block;
width: 600px;
padding: 20px;
- background: #f5f5f5;
position:fixed;
- z-index:101;
+ z-index: 101;
text-align: center;
- background: #5c697b;
- border-radius: 3px;
- box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.question h3 {
@@ -473,7 +618,6 @@ body > .spinner, .mask{
.question .hint {
font-size: 14px;
color: #ccc;
- text-shadow: none;
}
.question .answer {
@@ -496,10 +640,11 @@ body > .spinner, .mask{
background-color: #4c5766;
}
- .more_menu {
- display: inline-block;
- vertical-align: middle;
- }
+.more_menu {
+ display: inline-block;
+ vertical-align: middle;
+ overflow: visible;
+}
.more_menu > a {
display: block;
@@ -509,57 +654,45 @@ body > .spinner, .mask{
border: 1px solid rgba(0,0,0,0.3);
transition: all 0.3s ease-in-out;
}
- .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) {
- background-color: #406db8;
- }
-
+ .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) {
+ background-color: #406db8;
+ }
+
.more_menu .wrapper {
display: none;
- border: 1px solid #333;
background: rgba(255,255,255,0.98);
- border-radius: 3px;
- padding: 4px !important;
+ top: 0;
+ right: 0;
+ padding: 4px;
+ margin: 26px 0 0 0;
position: absolute;
- z-index: 9;
- margin: 32px 0 0 -145px;
+ z-index: 90;
width: 185px;
- box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4);
+ box-shadow: 0 20px 20px -5px rgba(0,0,0,0.1);
text-align: center;
color: #000;
- text-shadow: none;
- background-image: -*-linear-gradient(
- 45deg,
+ background-image: linear-gradient(
+ -45deg,
rgb(200,200,200) 0%,
rgb(255,255,255) 100%
);
}
-
- .more_menu .wrapper:before {
- content: ' ';
- height: 0;
- position: relative;
- width: 0;
- border: 6px solid transparent;
- border-bottom-color: #fff;
- display: block;
- top: -16px;
- left: 146px;
- }
+
.more_menu.show .wrapper {
display: block;
}
-
+
.more_menu ul {
padding: 0;
- margin: -12px 0 0 0;
+ margin: 0;
list-style: none;
}
-
+
.more_menu .wrapper li {
width: 100%;
height: auto;
}
-
+
.more_menu .wrapper li a {
display: block;
border-bottom: 1px solid rgba(255,255,255,0.2);
@@ -571,7 +704,7 @@ body > .spinner, .mask{
padding: 3px 0;
color: #000;
}
-
+
.more_menu .wrapper li:last-child a {
border: none;
}
@@ -583,41 +716,109 @@ body > .spinner, .mask{
position: fixed;
right: 0;
bottom: 0;
- padding: 2px;
- width: 240px;
+ width: 320px;
z-index: 20;
overflow: hidden;
font-size: 14px;
font-weight: bold;
}
+ @media all and (max-width: 480px) {
+ .messages {
+ width: 100%;
+ }
+ }
.messages .message {
- text-align: center;
- border-radius: 2px;
- margin: 2px 0 0 0;
- height: 0;
overflow: hidden;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
- box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
- background-image: -*-linear-gradient(
- 270deg,
- #5b9bd1 0%,
- #406db8 100%
- );
+ background: #5b9bd1;
width: 100%;
- padding: 0 5px;
- visibility: hidden;
+ position: relative;
+ margin: 1px 0 0;
max-height: 0;
+ padding: 0 30px 0 20px;
+ font-size: 1.1em;
+ font-weight: normal;
+ transform: scale(0);
}
+ .messages .message.sticky {
+ background-color: #c84040;
+ }
.messages .message.show {
- visibility: visible;
- height: auto;
- padding-top: 3px;
- padding-bottom: 3px;
- min-height: 1px;
- max-height: 400px;
+ max-height: 100px;
+ padding: 15px 30px 15px 20px;
+ transform: scale(1);
}
.messages .message.hide {
- margin-left: 240px;
- opacity: 0;
- }
\ No newline at end of file
+ max-height: 0;
+ padding: 0 20px;
+ margin: 0;
+ transform: scale(0);
+ }
+ .messages .close {
+ position: absolute;
+ padding: 10px 8px;
+ top: 0;
+ right: 0;
+ color: #FFF;
+ }
+
+/* Fonts */
+@font-face {
+ font-family: 'Elusive-Icons';
+ src:url('../fonts/Elusive-Icons.eot');
+ src:url('../fonts/Elusive-Icons.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/Elusive-Icons.woff') format('woff'),
+ url('../fonts/Elusive-Icons.ttf') format('truetype'),
+ url('../fonts/Elusive-Icons.svg#Elusive-Icons') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'OpenSans';
+ src: url('../fonts/OpenSans-Regular-webfont.eot');
+ src: url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/OpenSans-Regular-webfont.woff') format('woff'),
+ url('../fonts/OpenSans-Regular-webfont.ttf') format('truetype'),
+ url('../fonts/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'OpenSans';
+ src: url('../fonts/OpenSans-Italic-webfont.eot');
+ src: url('../fonts/OpenSans-Italic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/OpenSans-Italic-webfont.woff') format('woff'),
+ url('../fonts/OpenSans-Italic-webfont.ttf') format('truetype'),
+ url('../fonts/OpenSans-Italic-webfont.svg#OpenSansItalic') format('svg');
+ font-weight: normal;
+ font-style: italic;
+
+}
+
+@font-face {
+ font-family: 'OpenSans';
+ src: url('../fonts/OpenSans-Bold-webfont.eot');
+ src: url('../fonts/OpenSans-Bold-webfont.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/OpenSans-Bold-webfont.woff') format('woff'),
+ url('../fonts/OpenSans-Bold-webfont.ttf') format('truetype'),
+ url('../fonts/OpenSans-Bold-webfont.svg#OpenSansBold') format('svg');
+ font-weight: bold;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'OpenSans';
+ src: url('../fonts/OpenSans-BoldItalic-webfont.eot');
+ src: url('../fonts/OpenSans-BoldItalic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('../fonts/OpenSans-BoldItalic-webfont.woff') format('woff'),
+ url('../fonts/OpenSans-BoldItalic-webfont.ttf') format('truetype'),
+ url('../fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic') format('svg');
+ font-weight: bold;
+ font-style: italic;
+
+}
\ No newline at end of file
diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css
index 3af7dba7..7bfeb3aa 100644
--- a/couchpotato/static/style/settings.css
+++ b/couchpotato/static/style/settings.css
@@ -1,5 +1,9 @@
+.page.settings {
+ min-width: 960px;
+}
+
.page.settings:after {
- content: ".";
+ content: "";
display: block;
clear: both;
visibility: hidden;
@@ -9,15 +13,15 @@
.page.settings .tabs {
float: left;
- width: 20%;
- font-size: 20px;
+ width: 14.7%;
+ font-size: 17px;
text-align: right;
list-style: none;
- padding: 40px 0;
+ padding: 35px 0;
margin: 0;
min-height: 470px;
- background-image: -*-linear-gradient(
- 20deg,
+ background-image: linear-gradient(
+ 76deg,
rgba(0,0,0,0) 50%,
rgba(0,0,0,0.3) 100%
);
@@ -60,9 +64,9 @@
.page.settings .containers {
- width: 80%;
+ width: 84%;
float: left;
- padding: 20px 2%;
+ padding: 40px 0 40px 2%;
min-height: 300px;
}
@@ -133,7 +137,12 @@
display: inline-block;
vertical-align: middle;
padding-left: 2%;
+ line-height: 14px;
}
+
+ .page .check {
+ margin-top: 6px;
+ }
.page .check + .formHint {
float: none;
@@ -142,6 +151,7 @@
padding-left: 1% !important;
height: 24px;
vertical-align: middle;
+ line-height: 24px;
}
.page .ctrlHolder label {
@@ -161,6 +171,10 @@
margin-bottom: 20px;
}
+ .page .option_list .check {
+ margin-top: 5px;
+ }
+
.page .option_list .enabler {
padding: 0;
margin-left: 5px !important;
@@ -238,8 +252,11 @@
display: block;
text-align: right;
height: 20px;
- margin: 0;
+ margin: 0 0 -37px;
}
+ .page .advanced_toggle .check {
+ margin: 0;
+ }
.page .advanced_toggle span { padding: 0 5px; }
.page.show_advanced .advanced_toggle {
color: #edc07f;
@@ -437,16 +454,16 @@
border-radius: 2px;
}
.page .tag_input > ul:hover > li.choice {
- background: -*-linear-gradient(
- 270deg,
+ background: linear-gradient(
+ 180deg,
rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0.1) 100%
);
}
.page .tag_input > ul > li.choice:hover,
.page .tag_input > ul > li.choice.selected {
- background: -*-linear-gradient(
- 270deg,
+ background: linear-gradient(
+ 180deg,
#5b9bd1 0%,
#406db8 100%
);
@@ -484,8 +501,8 @@
margin: -9px 0 0 -16px;
border-radius: 30px 30px 0 0;
cursor: pointer;
- background: url('../images/icon.delete.png') no-repeat center 2px, -*-linear-gradient(
- 270deg,
+ background: url('../images/icon.delete.png') no-repeat center 2px, linear-gradient(
+ 180deg,
#5b9bd1 0%,
#5b9bd1 100%
);
@@ -513,8 +530,21 @@
display: none;
}
.page .combined_table .head abbr.host {
- margin-right: 197px;
+ margin-right: 190px;
}
+ .page .combined_table .head abbr.api_key {
+ margin-right: 171px;
+ }
+ .page .combined_table .head .extra_score,
+ .page .combined_table .extra_score {
+ width: 70px;
+ text-align: center;
+ display: none;
+ }
+ .page.show_advanced .combined_table .head .extra_score,
+ .page.show_advanced .combined_table .extra_score {
+ display: inline-block;
+ }
.page .combined_table .ctrlHolder {
padding-top: 2px;
@@ -525,6 +555,9 @@
.page .combined_table .ctrlHolder > * {
margin: 0 10px 0 0;
}
+ .page .combined_table .ctrlHolder > .check {
+ margin-top: 6px;
+ }
.page .combined_table .ctrlHolder .delete {
display: none;
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
deleted file mode 100644
index 1d618066..00000000
--- a/couchpotato/templates/_desktop.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
- {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
- {% endfor %}
- {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
- {% endfor %}
-
- {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
- {% endfor %}
- {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
- {% endfor %}
-
-
-
-
-
-
-
- CouchPotato
-
-
-
\ No newline at end of file
diff --git a/couchpotato/templates/_mobile.html b/couchpotato/templates/_mobile.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html
index 217d6bfc..cf666a65 100644
--- a/couchpotato/templates/index.html
+++ b/couchpotato/templates/index.html
@@ -1 +1,92 @@
-{% extends "_desktop.html" %}
+
+
+
+
+
+
+ {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
+ {% endfor %}
+ {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
+ {% endfor %}
+
+ {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
+ {% endfor %}
+ {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
+ {% endfor %}
+
+
+
+
+
+
+
+ CouchPotato
+
+
+
\ No newline at end of file
diff --git a/libs/cssprefixer/__init__.py b/libs/cssprefixer/__init__.py
new file mode 100755
index 00000000..05674329
--- /dev/null
+++ b/libs/cssprefixer/__init__.py
@@ -0,0 +1,20 @@
+# CSSPrefixer
+# Copyright 2010-2012 Greg V.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import engine
+import rules
+from engine import process
+
+__all__ = ('process', 'engine', 'rules')
diff --git a/libs/cssprefixer/engine.py b/libs/cssprefixer/engine.py
new file mode 100755
index 00000000..894f6e7f
--- /dev/null
+++ b/libs/cssprefixer/engine.py
@@ -0,0 +1,117 @@
+# CSSPrefixer
+# Copyright 2010-2012 Greg V.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cssutils
+import re
+from rules import rules as tr_rules
+from rules import prefixRegex
+
+
+keyframesRegex = re.compile(r'@keyframes\s?\w+\s?{(.*)}')
+blockRegex = re.compile(r'\w+\s?\{(.*)\}')
+
+
+def magic(ruleset, debug, minify, filt, parser):
+ if isinstance(ruleset, cssutils.css.CSSUnknownRule):
+ if ruleset.cssText.startswith('@keyframes'):
+ inner = parser.parseString(keyframesRegex.split(ruleset.cssText.replace('\n', ''))[1])
+ # BUG: doesn't work when minified
+ s = '' if minify else '\n'
+ return '@-webkit-keyframes {' + s + \
+ ''.join([magic(rs, debug, minify, ['webkit'], parser) for rs in inner]) \
+ + '}' + s + '@-moz-keyframes {' + s + \
+ ''.join([magic(rs, debug, minify, ['moz'], parser) for rs in inner]) \
+ + '}' + s + ruleset.cssText
+ elif ruleset.cssText.startswith('from') or ruleset.cssText.startswith('to'):
+ return ''.join([magic(rs, debug, minify, filt, parser)
+ for rs in parser.parseString(blockRegex.sub(r'\1', ruleset.cssText.replace('\n', ''))[1])])
+ else:
+ return
+ elif hasattr(ruleset, 'style'): # Comments don't
+ ruleSet = set()
+ rules = list()
+ children = list(ruleset.style.children())
+ ruleset.style = cssutils.css.CSSStyleDeclaration() # clear out the styles that were there
+ for rule in children:
+ if not hasattr(rule, 'name'): # comments don't have name
+ rules.append(rule)
+ continue
+ name = prefixRegex.sub('', rule.name)
+ if name in tr_rules:
+ rule.name = name
+ if rule.cssText in ruleSet:
+ continue
+ ruleSet.add(rule.cssText)
+ rules.append(rule)
+
+ ruleset.style.seq._readonly = False
+ for rule in rules:
+ if not hasattr(rule, 'name'):
+ ruleset.style.seq.append(rule, 'Comment')
+ continue
+ processor = None
+ try: # try except so if anything goes wrong we don't lose the original property
+ if rule.name in tr_rules:
+ processor = tr_rules[rule.name](rule)
+ [ruleset.style.seq.append(prop, 'Property') for prop in processor.get_prefixed_props(filt) if prop]
+ # always add the original rule
+ if processor and hasattr(processor, 'get_base_prop'):
+ ruleset.style.seq.append(processor.get_base_prop(), 'Property')
+ else:
+ ruleset.style.seq.append(rule, 'Property')
+ except:
+ if debug:
+ print 'warning with ' + str(rule)
+ ruleset.style.seq.append(rule, 'Property')
+ ruleset.style.seq._readonly = True
+ elif hasattr(ruleset, 'cssRules'):
+ for subruleset in ruleset:
+ magic(subruleset, debug, minify, filt, parser)
+ cssText = ruleset.cssText
+ if not cssText: # blank rules return None so return an empty string
+ return
+ if minify or not hasattr(ruleset, 'style'):
+ return unicode(cssText)
+ return unicode(cssText) + '\n'
+
+
+def process(string, debug = False, minify = False, filt = ['webkit', 'moz', 'o', 'ms'], **prefs):
+ loglevel = 'DEBUG' if debug else 'ERROR'
+ parser = cssutils.CSSParser(loglevel = 'CRITICAL')
+ if minify:
+ cssutils.ser.prefs.useMinified()
+ else:
+ cssutils.ser.prefs.useDefaults()
+
+ # use the passed in prefs
+ for key, value in prefs.iteritems():
+ if hasattr(cssutils.ser.prefs, key):
+ cssutils.ser.prefs.__dict__[key] = value
+
+ results = []
+ sheet = parser.parseString(string)
+ for ruleset in sheet.cssRules:
+ cssText = magic(ruleset, debug, minify, filt, parser)
+ if cssText:
+ results.append(cssText)
+
+ # format with newlines based on minify
+ joinStr = '' if minify else '\n'
+
+ # Not using sheet.cssText - it's buggy:
+ # it skips some prefixed properties.
+ return joinStr.join(results).rstrip()
+
+__all__ = ['process']
diff --git a/libs/cssprefixer/rules.py b/libs/cssprefixer/rules.py
new file mode 100755
index 00000000..156d4f18
--- /dev/null
+++ b/libs/cssprefixer/rules.py
@@ -0,0 +1,271 @@
+# CSSPrefixer
+# Copyright 2010-2012 Greg V.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import cssutils
+
+prefixRegex = re.compile('^(-o-|-ms-|-moz-|-webkit-)')
+
+
+class BaseReplacementRule(object):
+ vendor_prefixes = ['moz', 'webkit']
+
+ def __init__(self, prop):
+ self.prop = prop
+
+ def get_prefixed_props(self, filt):
+ for prefix in [p for p in self.vendor_prefixes if p in filt]:
+ yield cssutils.css.Property(
+ name='-%s-%s' % (prefix, self.prop.name),
+ value=self.prop.value,
+ priority=self.prop.priority
+ )
+
+ @staticmethod
+ def should_prefix():
+ return True
+
+
+class FullReplacementRule(BaseReplacementRule):
+ vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o', 'ms'])
+
+
+class BaseAndIEReplacementRule(BaseReplacementRule):
+ vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['ms'])
+
+
+class BaseAndOperaReplacementRule(BaseReplacementRule):
+ vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o'])
+
+
+class WebkitReplacementRule(BaseReplacementRule):
+ vendor_prefixes = ['webkit']
+
+
+class OperaAndIEReplacementRule(BaseReplacementRule):
+ vendor_prefixes = ['ms', 'o']
+
+
+class MozReplacementRule(BaseReplacementRule):
+ vendor_prefixes = ['moz']
+
+
+class BorderRadiusReplacementRule(BaseReplacementRule):
+ """
+ Mozilla's Gecko engine uses different syntax for rounded corners.
+ """
+ vendor_prefixes = ['webkit']
+
+ def get_prefixed_props(self, filt):
+ for prop in BaseReplacementRule.get_prefixed_props(self, filt):
+ yield prop
+ if 'moz' in filt:
+ name = '-moz-' + self.prop.name.replace('top-left-radius', 'radius-topleft') \
+ .replace('top-right-radius', 'radius-topright') \
+ .replace('bottom-right-radius', 'radius-bottomright') \
+ .replace('bottom-left-radius', 'radius-bottomleft')
+ yield cssutils.css.Property(
+ name=name,
+ value=self.prop.value,
+ priority=self.prop.priority
+ )
+
+
+class DisplayReplacementRule(BaseReplacementRule):
+ """
+ Flexible Box Model stuff.
+ CSSUtils parser doesn't support duplicate properties, so that's dirty.
+ """
+ def get_prefixed_props(self, filt):
+ if self.prop.value == 'box': # only add prefixes if the value is box
+ for prefix in [p for p in self.vendor_prefixes if p in filt]:
+ yield cssutils.css.Property(
+ name='display',
+ value='-%s-box' % prefix,
+ priority=self.prop.priority
+ )
+
+
+class TransitionReplacementRule(BaseReplacementRule):
+ vendor_prefixes = ['moz', 'o', 'webkit']
+
+ def __get_prefixed_prop(self, prefix=None):
+ name = self.prop.name
+ if prefix:
+ name = '-%s-%s' % (prefix, self.prop.name)
+ newValues = []
+ for value in self.prop.value.split(','):
+ parts = value.strip().split(' ')
+ parts[0] = prefixRegex.sub('', parts[0])
+ if parts[0] in rules and prefix and rules[parts[0]].should_prefix():
+ parts[0] = '-%s-%s' % (prefix, parts[0])
+ newValues.append(' '.join(parts))
+ return cssutils.css.Property(
+ name=name,
+ value=', '.join(newValues),
+ priority=self.prop.priority
+ )
+
+ def get_prefixed_props(self, filt):
+ for prefix in [p for p in self.vendor_prefixes if p in filt]:
+ yield self.__get_prefixed_prop(prefix)
+
+ def get_base_prop(self):
+ return self.__get_prefixed_prop()
+
+
+class GradientReplacementRule(BaseReplacementRule):
+ vendor_prefixes = ['moz', 'o', 'webkit']
+
+ def __iter_values(self):
+ valueSplit = self.prop.value.split(',')
+ index = 0
+ # currentString = ''
+ while(True):
+ if index >= len(valueSplit):
+ break
+ rawValue = valueSplit[index].strip()
+ snip = prefixRegex.sub('', rawValue)
+ if snip.startswith('linear-gradient'):
+ values = [re.sub('^linear-gradient\(', '', snip)]
+ if valueSplit[index + 1].strip().endswith(')'):
+ values.append(re.sub('\)+$', '', valueSplit[index + 1].strip()))
+ else:
+ values.append(valueSplit[index + 1].strip())
+ values.append(re.sub('\)+$', '', valueSplit[index + 2].strip()))
+ if len(values) == 2:
+ yield {
+ 'start': values[0],
+ 'end': values[1]
+ }
+ else:
+ yield {
+ 'pos': values[0],
+ 'start': values[1],
+ 'end': values[2]
+ }
+ index += len(values)
+ elif snip.startswith('gradient'):
+ yield {
+ 'start': re.sub('\)+$', '', valueSplit[index + 4].strip()),
+ 'end': re.sub('\)+$', '', valueSplit[index + 6].strip()),
+ }
+ index += 7
+ else:
+ # not a gradient so just yield the raw string
+ yield rawValue
+ index += 1
+
+ def __get_prefixed_prop(self, values, prefix=None):
+ gradientName = 'linear-gradient'
+ if prefix:
+ gradientName = '-%s-%s' % (prefix, gradientName)
+ newValues = []
+ for value in values:
+ if isinstance(value, dict):
+ if 'pos' in value:
+ newValues.append(gradientName + '(%(pos)s, %(start)s, %(end)s)' % value)
+ else:
+ newValues.append(gradientName + '(%(start)s, %(end)s)' % value)
+ else:
+ newValues.append(value)
+ return cssutils.css.Property(
+ name=self.prop.name,
+ value=', '.join(newValues),
+ priority=self.prop.priority
+ )
+
+ def get_prefixed_props(self, filt):
+ values = list(self.__iter_values())
+ needPrefix = False
+ for value in values: # check if there are any gradients
+ if isinstance(value, dict):
+ needPrefix = True
+ break
+ if needPrefix:
+ for prefix in [p for p in self.vendor_prefixes if p in filt]:
+ yield self.__get_prefixed_prop(values, prefix)
+ if prefix == 'webkit':
+ newValues = []
+ for value in values:
+ if isinstance(value, dict):
+ newValues.append('-webkit-gradient(linear, left top, left bottom, color-stop(0, %(start)s), color-stop(1, %(end)s))' % value)
+ else:
+ newValues.append(value)
+ yield cssutils.css.Property(
+ name=self.prop.name,
+ value=', '.join(newValues),
+ priority=self.prop.priority
+ )
+ else:
+ yield None
+
+ def get_base_prop(self):
+ values = self.__iter_values()
+ return self.__get_prefixed_prop(values)
+
+rules = {
+ 'border-radius': BaseReplacementRule,
+ 'border-top-left-radius': BorderRadiusReplacementRule,
+ 'border-top-right-radius': BorderRadiusReplacementRule,
+ 'border-bottom-right-radius': BorderRadiusReplacementRule,
+ 'border-bottom-left-radius': BorderRadiusReplacementRule,
+ 'border-image': FullReplacementRule,
+ 'box-shadow': BaseReplacementRule,
+ 'box-sizing': MozReplacementRule,
+ 'box-orient': BaseAndIEReplacementRule,
+ 'box-direction': BaseAndIEReplacementRule,
+ 'box-ordinal-group': BaseAndIEReplacementRule,
+ 'box-align': BaseAndIEReplacementRule,
+ 'box-flex': BaseAndIEReplacementRule,
+ 'box-flex-group': BaseReplacementRule,
+ 'box-pack': BaseAndIEReplacementRule,
+ 'box-lines': BaseAndIEReplacementRule,
+ 'user-select': BaseReplacementRule,
+ 'user-modify': BaseReplacementRule,
+ 'margin-start': BaseReplacementRule,
+ 'margin-end': BaseReplacementRule,
+ 'padding-start': BaseReplacementRule,
+ 'padding-end': BaseReplacementRule,
+ 'column-count': BaseReplacementRule,
+ 'column-gap': BaseReplacementRule,
+ 'column-rule': BaseReplacementRule,
+ 'column-rule-color': BaseReplacementRule,
+ 'column-rule-style': BaseReplacementRule,
+ 'column-rule-width': BaseReplacementRule,
+ 'column-span': WebkitReplacementRule,
+ 'column-width': BaseReplacementRule,
+ 'columns': WebkitReplacementRule,
+
+ 'background-clip': WebkitReplacementRule,
+ 'background-origin': WebkitReplacementRule,
+ 'background-size': WebkitReplacementRule,
+ 'background-image': GradientReplacementRule,
+ 'background': GradientReplacementRule,
+
+ 'text-overflow': OperaAndIEReplacementRule,
+
+ 'transition': TransitionReplacementRule,
+ 'transition-delay': BaseAndOperaReplacementRule,
+ 'transition-duration': BaseAndOperaReplacementRule,
+ 'transition-property': TransitionReplacementRule,
+ 'transition-timing-function': BaseAndOperaReplacementRule,
+ 'transform': FullReplacementRule,
+ 'transform-origin': FullReplacementRule,
+
+ 'display': DisplayReplacementRule,
+ 'appearance': WebkitReplacementRule,
+ 'hyphens': BaseReplacementRule,
+}
diff --git a/libs/cssutils/__init__.py b/libs/cssutils/__init__.py
new file mode 100755
index 00000000..8157ce57
--- /dev/null
+++ b/libs/cssutils/__init__.py
@@ -0,0 +1,385 @@
+#!/usr/bin/env python
+"""cssutils - CSS Cascading Style Sheets library for Python
+
+ Copyright (C) 2004-2013 Christof Hoeke
+
+ cssutils is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see .
+
+
+A Python package to parse and build CSS Cascading Style Sheets. DOM only, not
+any rendering facilities!
+
+Based upon and partly implementing the following specifications :
+
+`CSS 2.1 `__
+ General CSS rules and properties are defined here
+`CSS 2.1 Errata `__
+ A few errata, mainly the definition of CHARSET_SYM tokens
+`CSS3 Module: Syntax `__
+ Used in parts since cssutils 0.9.4. cssutils tries to use the features from
+ CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some
+ parts are from CSS 2.1
+`MediaQueries `__
+ MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in
+ @import and @media rules.
+`Namespaces `__
+ Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5
+ for dev version
+`CSS3 Module: Pages Media `__
+ Most properties of this spec are implemented including MarginRules
+`Selectors `__
+ The selector syntax defined here (and not in CSS 2.1) should be parsable
+ with cssutils (*should* mind though ;) )
+
+`DOM Level 2 Style CSS `__
+ DOM for package css. 0.9.8 removes support for CSSValue and related API,
+ see PropertyValue and Value API for now
+`DOM Level 2 Style Stylesheets `__
+ DOM for package stylesheets
+`CSSOM `__
+ A few details (mainly the NamespaceRule DOM) is taken from here. Plan is
+ to move implementation to the stuff defined here which is newer but still
+ no REC so might change anytime...
+
+
+The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax
+(W3C Working Draft 13 August 2003) `__ which
+itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as
+possible but uses some (helpful) parts of the CSS 2.1 tokenizer.
+
+I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least
+be able to parse both grammars including some more real world cases (some CSS
+hacks are actually parsed and serialized). Both official grammars are not final
+nor bugfree but still feasible. cssutils aim is not to be fully compliant to
+any CSS specification (the specifications seem to be in a constant flow anyway)
+but cssutils *should* be able to read and write as many as possible CSS
+stylesheets "in the wild" while at the same time implement the official APIs
+which are well documented. Some minor extensions are provided as well.
+
+Please visit http://cthedot.de/cssutils/ for more details.
+
+
+Tested with Python 2.7.3 and 3.3 on Windows 8 64bit.
+
+
+This library may be used ``from cssutils import *`` which
+import subpackages ``css`` and ``stylesheets``, CSSParser and
+CSSSerializer classes only.
+
+Usage may be::
+
+ >>> from cssutils import *
+ >>> parser = CSSParser()
+ >>> sheet = parser.parseString(u'a { color: red}')
+ >>> print sheet.cssText
+ a {
+ color: red
+ }
+
+"""
+__all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer']
+__docformat__ = 'restructuredtext'
+__author__ = 'Christof Hoeke with contributions by Walter Doerwald'
+__date__ = '$LastChangedDate:: $:'
+
+VERSION = '0.9.10'
+
+__version__ = '%s $Id$' % VERSION
+
+import sys
+if sys.version_info < (2,6):
+ bytes = str
+
+import codec
+import os.path
+import urllib
+import urlparse
+import xml.dom
+
+# order of imports is important (partly circular)
+from . import util
+import errorhandler
+log = errorhandler.ErrorHandler()
+
+import css
+import stylesheets
+from parse import CSSParser
+
+from serialize import CSSSerializer
+ser = CSSSerializer()
+
+from profiles import Profiles
+profile = Profiles(log=log)
+
+# used by Selector defining namespace prefix '*'
+_ANYNS = -1
+
+class DOMImplementationCSS(object):
+ """This interface allows the DOM user to create a CSSStyleSheet
+ outside the context of a document. There is no way to associate
+ the new CSSStyleSheet with a document in DOM Level 2.
+
+ This class is its *own factory*, as it is given to
+ xml.dom.registerDOMImplementation which simply calls it and receives
+ an instance of this class then.
+ """
+ _features = [
+ ('css', '1.0'),
+ ('css', '2.0'),
+ ('stylesheets', '1.0'),
+ ('stylesheets', '2.0')
+ ]
+
+ def createCSSStyleSheet(self, title, media):
+ """
+ Creates a new CSSStyleSheet.
+
+ title of type DOMString
+ The advisory title. See also the Style Sheet Interfaces
+ section.
+ media of type DOMString
+ The comma-separated list of media associated with the new style
+ sheet. See also the Style Sheet Interfaces section.
+
+ returns
+ CSSStyleSheet: A new CSS style sheet.
+
+ TODO: DOMException
+ SYNTAX_ERR: Raised if the specified media string value has a
+ syntax error and is unparsable.
+ """
+ return css.CSSStyleSheet(title=title, media=media)
+
+ def createDocument(self, *args):
+ # not needed to HTML, also not for CSS?
+ raise NotImplementedError
+
+ def createDocumentType(self, *args):
+ # not needed to HTML, also not for CSS?
+ raise NotImplementedError
+
+ def hasFeature(self, feature, version):
+ return (feature.lower(), unicode(version)) in self._features
+
+xml.dom.registerDOMImplementation('cssutils', DOMImplementationCSS)
+
+
+def parseString(*a, **k):
+ return CSSParser().parseString(*a, **k)
+parseString.__doc__ = CSSParser.parseString.__doc__
+
+def parseFile(*a, **k):
+ return CSSParser().parseFile(*a, **k)
+parseFile.__doc__ = CSSParser.parseFile.__doc__
+
+def parseUrl(*a, **k):
+ return CSSParser().parseUrl(*a, **k)
+parseUrl.__doc__ = CSSParser.parseUrl.__doc__
+
+def parseStyle(*a, **k):
+ return CSSParser().parseStyle(*a, **k)
+parseStyle.__doc__ = CSSParser.parseStyle.__doc__
+
+# set "ser", default serializer
+def setSerializer(serializer):
+ """Set the global serializer used by all class in cssutils."""
+ global ser
+ ser = serializer
+
+def getUrls(sheet):
+ """Retrieve all ``url(urlstring)`` values (in e.g.
+ :class:`cssutils.css.CSSImportRule` or :class:`cssutils.css.CSSValue`
+ objects of given `sheet`.
+
+ :param sheet:
+ :class:`cssutils.css.CSSStyleSheet` object whose URLs are yielded
+
+ This function is a generator. The generated URL values exclude ``url(`` and
+ ``)`` and surrounding single or double quotes.
+ """
+ for importrule in (r for r in sheet if r.type == r.IMPORT_RULE):
+ yield importrule.href
+
+ def styleDeclarations(base):
+ "recursive generator to find all CSSStyleDeclarations"
+ if hasattr(base, 'cssRules'):
+ for rule in base.cssRules:
+ for s in styleDeclarations(rule):
+ yield s
+ elif hasattr(base, 'style'):
+ yield base.style
+
+ for style in styleDeclarations(sheet):
+ for p in style.getProperties(all=True):
+ for v in p.propertyValue:
+ if v.type == 'URI':
+ yield v.uri
+
+def replaceUrls(sheetOrStyle, replacer, ignoreImportRules=False):
+ """Replace all URLs in :class:`cssutils.css.CSSImportRule` or
+ :class:`cssutils.css.CSSValue` objects of given `sheetOrStyle`.
+
+ :param sheetOrStyle:
+ a :class:`cssutils.css.CSSStyleSheet` or a
+ :class:`cssutils.css.CSSStyleDeclaration` which is changed in place
+ :param replacer:
+ a function which is called with a single argument `url` which
+ is the current value of each url() excluding ``url(``, ``)`` and
+ surrounding (single or double) quotes.
+ :param ignoreImportRules:
+ if ``True`` does not call `replacer` with URLs from @import rules.
+ """
+ if not ignoreImportRules and not isinstance(sheetOrStyle,
+ css.CSSStyleDeclaration):
+ for importrule in (r for r in sheetOrStyle if r.type == r.IMPORT_RULE):
+ importrule.href = replacer(importrule.href)
+
+ def styleDeclarations(base):
+ "recursive generator to find all CSSStyleDeclarations"
+ if hasattr(base, 'cssRules'):
+ for rule in base.cssRules:
+ for s in styleDeclarations(rule):
+ yield s
+ elif hasattr(base, 'style'):
+ yield base.style
+ elif isinstance(sheetOrStyle, css.CSSStyleDeclaration):
+ # base is a style already
+ yield base
+
+ for style in styleDeclarations(sheetOrStyle):
+ for p in style.getProperties(all=True):
+ for v in p.propertyValue:
+ if v.type == v.URI:
+ v.uri = replacer(v.uri)
+
+def resolveImports(sheet, target=None):
+ """Recurcively combine all rules in given `sheet` into a `target` sheet.
+ @import rules which use media information are tried to be wrapped into
+ @media rules so keeping the media information. This may not work in
+ all instances (if e.g. an @import rule itself contains an @import rule
+ with different media infos or if it contains rules which may not be
+ used inside an @media block like @namespace rules.). In these cases
+ the @import rule is kept as in the original sheet and a WARNING is issued.
+
+ :param sheet:
+ in this given :class:`cssutils.css.CSSStyleSheet` all import rules are
+ resolved and added to a resulting *flat* sheet.
+ :param target:
+ A :class:`cssutils.css.CSSStyleSheet` object which will be the
+ resulting *flat* sheet if given
+ :returns: given `target` or a new :class:`cssutils.css.CSSStyleSheet`
+ object
+ """
+ if not target:
+ target = css.CSSStyleSheet(href=sheet.href,
+ media=sheet.media,
+ title=sheet.title)
+
+ def getReplacer(targetbase):
+ "Return a replacer which uses base to return adjusted URLs"
+ basesch, baseloc, basepath, basequery, basefrag = urlparse.urlsplit(targetbase)
+ basepath, basepathfilename = os.path.split(basepath)
+
+ def replacer(uri):
+ scheme, location, path, query, fragment = urlparse.urlsplit(uri)
+ if not scheme and not location and not path.startswith(u'/'):
+ # relative
+ path, filename = os.path.split(path)
+ combined = os.path.normpath(os.path.join(basepath, path, filename))
+ return urllib.pathname2url(combined)
+ else:
+ # keep anything absolute
+ return uri
+
+ return replacer
+
+ for rule in sheet.cssRules:
+ if rule.type == rule.CHARSET_RULE:
+ pass
+ elif rule.type == rule.IMPORT_RULE:
+ log.info(u'Processing @import %r' % rule.href, neverraise=True)
+
+ if rule.hrefFound:
+ # add all rules of @import to current sheet
+ target.add(css.CSSComment(cssText=u'/* START @import "%s" */'
+ % rule.href))
+
+ try:
+ # nested imports
+ importedSheet = resolveImports(rule.styleSheet)
+ except xml.dom.HierarchyRequestErr, e:
+ log.warn(u'@import: Cannot resolve target, keeping rule: %s'
+ % e, neverraise=True)
+ target.add(rule)
+ else:
+ # adjust relative URI references
+ log.info(u'@import: Adjusting paths for %r' % rule.href,
+ neverraise=True)
+ replaceUrls(importedSheet,
+ getReplacer(rule.href),
+ ignoreImportRules=True)
+
+ # might have to wrap rules in @media if media given
+ if rule.media.mediaText == u'all':
+ mediaproxy = None
+ else:
+ keepimport = False
+ for r in importedSheet:
+ # check if rules present which may not be
+ # combined with media
+ if r.type not in (r.COMMENT,
+ r.STYLE_RULE,
+ r.IMPORT_RULE):
+ keepimport = True
+ break
+ if keepimport:
+ log.warn(u'Cannot combine imported sheet with'
+ u' given media as other rules then'
+ u' comments or stylerules found %r,'
+ u' keeping %r' % (r,
+ rule.cssText),
+ neverraise=True)
+ target.add(rule)
+ continue
+
+ # wrap in @media if media is not `all`
+ log.info(u'@import: Wrapping some rules in @media '
+ u' to keep media: %s'
+ % rule.media.mediaText, neverraise=True)
+ mediaproxy = css.CSSMediaRule(rule.media.mediaText)
+
+ for r in importedSheet:
+ if mediaproxy:
+ mediaproxy.add(r)
+ else:
+ # add to top sheet directly but are difficult anyway
+ target.add(r)
+
+ if mediaproxy:
+ target.add(mediaproxy)
+
+ else:
+ # keep @import as it is
+ log.error(u'Cannot get referenced stylesheet %r, keeping rule'
+ % rule.href, neverraise=True)
+ target.add(rule)
+
+ else:
+ target.add(rule)
+
+ return target
+
+
+if __name__ == '__main__':
+ print __doc__
diff --git a/libs/cssutils/_codec2.py b/libs/cssutils/_codec2.py
new file mode 100755
index 00000000..d0ae617f
--- /dev/null
+++ b/libs/cssutils/_codec2.py
@@ -0,0 +1,584 @@
+#!/usr/bin/env python
+"""Python codec for CSS."""
+__docformat__ = 'restructuredtext'
+__author__ = 'Walter Doerwald'
+__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $'
+
+import codecs
+import marshal
+
+# We're using bits to store all possible candidate encodings (or variants, i.e.
+# we have two bits for the variants of UTF-16 and two for the
+# variants of UTF-32).
+#
+# Prefixes for various CSS encodings
+# UTF-8-SIG xEF xBB xBF
+# UTF-16 (LE) xFF xFE ~x00|~x00
+# UTF-16 (BE) xFE xFF
+# UTF-16-LE @ x00 @ x00
+# UTF-16-BE x00 @
+# UTF-32 (LE) xFF xFE x00 x00
+# UTF-32 (BE) x00 x00 xFE xFF
+# UTF-32-LE @ x00 x00 x00
+# UTF-32-BE x00 x00 x00 @
+# CHARSET @ c h a ...
+
+
+def detectencoding_str(input, final=False):
+ """
+ Detect the encoding of the byte string ``input``, which contains the
+ beginning of a CSS file. This function returns the detected encoding (or
+ ``None`` if it hasn't got enough data), and a flag that indicates whether
+ that encoding has been detected explicitely or implicitely. To detect the
+ encoding the first few bytes are used (or if ``input`` is ASCII compatible
+ and starts with a charset rule the encoding name from the rule). "Explicit"
+ detection means that the bytes start with a BOM or a charset rule.
+
+ If the encoding can't be detected yet, ``None`` is returned as the encoding.
+ ``final`` specifies whether more data will be available in later calls or
+ not. If ``final`` is true, ``detectencoding_str()`` will never return
+ ``None`` as the encoding.
+ """
+
+ # A bit for every candidate
+ CANDIDATE_UTF_8_SIG = 1
+ CANDIDATE_UTF_16_AS_LE = 2
+ CANDIDATE_UTF_16_AS_BE = 4
+ CANDIDATE_UTF_16_LE = 8
+ CANDIDATE_UTF_16_BE = 16
+ CANDIDATE_UTF_32_AS_LE = 32
+ CANDIDATE_UTF_32_AS_BE = 64
+ CANDIDATE_UTF_32_LE = 128
+ CANDIDATE_UTF_32_BE = 256
+ CANDIDATE_CHARSET = 512
+
+ candidates = 1023 # all candidates
+
+ li = len(input)
+ if li>=1:
+ # Check first byte
+ c = input[0]
+ if c != "\xef":
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != "\xff":
+ candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE)
+ if c != "\xfe":
+ candidates &= ~CANDIDATE_UTF_16_AS_BE
+ if c != "@":
+ candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET)
+ if c != "\x00":
+ candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE)
+ if li>=2:
+ # Check second byte
+ c = input[1]
+ if c != "\xbb":
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != "\xfe":
+ candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE)
+ if c != "\xff":
+ candidates &= ~CANDIDATE_UTF_16_AS_BE
+ if c != "\x00":
+ candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE)
+ if c != "@":
+ candidates &= ~CANDIDATE_UTF_16_BE
+ if c != "c":
+ candidates &= ~CANDIDATE_CHARSET
+ if li>=3:
+ # Check third byte
+ c = input[2]
+ if c != "\xbf":
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != "c":
+ candidates &= ~CANDIDATE_UTF_16_LE
+ if c != "\x00":
+ candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE)
+ if c != "\xfe":
+ candidates &= ~CANDIDATE_UTF_32_AS_BE
+ if c != "h":
+ candidates &= ~CANDIDATE_CHARSET
+ if li>=4:
+ # Check fourth byte
+ c = input[3]
+ if input[2:4] == "\x00\x00":
+ candidates &= ~CANDIDATE_UTF_16_AS_LE
+ if c != "\x00":
+ candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE)
+ if c != "\xff":
+ candidates &= ~CANDIDATE_UTF_32_AS_BE
+ if c != "@":
+ candidates &= ~CANDIDATE_UTF_32_BE
+ if c != "a":
+ candidates &= ~CANDIDATE_CHARSET
+ if candidates == 0:
+ return ("utf-8", False)
+ if not (candidates & (candidates-1)): # only one candidate remaining
+ if candidates == CANDIDATE_UTF_8_SIG and li >= 3:
+ return ("utf-8-sig", True)
+ elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2:
+ return ("utf-16", True)
+ elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2:
+ return ("utf-16", True)
+ elif candidates == CANDIDATE_UTF_16_LE and li >= 4:
+ return ("utf-16-le", False)
+ elif candidates == CANDIDATE_UTF_16_BE and li >= 2:
+ return ("utf-16-be", False)
+ elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4:
+ return ("utf-32", True)
+ elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4:
+ return ("utf-32", True)
+ elif candidates == CANDIDATE_UTF_32_LE and li >= 4:
+ return ("utf-32-le", False)
+ elif candidates == CANDIDATE_UTF_32_BE and li >= 4:
+ return ("utf-32-be", False)
+ elif candidates == CANDIDATE_CHARSET and li >= 4:
+ prefix = '@charset "'
+ if input[:len(prefix)] == prefix:
+ pos = input.find('"', len(prefix))
+ if pos >= 0:
+ return (input[len(prefix):pos], True)
+ # if this is the last call, and we haven't determined an encoding yet,
+ # we default to UTF-8
+ if final:
+ return ("utf-8", False)
+ return (None, False) # dont' know yet
+
+
+def detectencoding_unicode(input, final=False):
+ """
+ Detect the encoding of the unicode string ``input``, which contains the
+ beginning of a CSS file. The encoding is detected from the charset rule
+ at the beginning of ``input``. If there is no charset rule, ``"utf-8"``
+ will be returned.
+
+ If the encoding can't be detected yet, ``None`` is returned. ``final``
+ specifies whether more data will be available in later calls or not. If
+ ``final`` is true, ``detectencoding_unicode()`` will never return ``None``.
+ """
+ prefix = u'@charset "'
+ if input.startswith(prefix):
+ pos = input.find(u'"', len(prefix))
+ if pos >= 0:
+ return (input[len(prefix):pos], True)
+ elif final or not prefix.startswith(input):
+ # if this is the last call, and we haven't determined an encoding yet,
+ # (or the string definitely doesn't start with prefix) we default to UTF-8
+ return ("utf-8", False)
+ return (None, False) # don't know yet
+
+
+def _fixencoding(input, encoding, final=False):
+ """
+ Replace the name of the encoding in the charset rule at the beginning of
+ ``input`` with ``encoding``. If ``input`` doesn't starts with a charset
+ rule, ``input`` will be returned unmodified.
+
+ If the encoding can't be found yet, ``None`` is returned. ``final``
+ specifies whether more data will be available in later calls or not.
+ If ``final`` is true, ``_fixencoding()`` will never return ``None``.
+ """
+ prefix = u'@charset "'
+ if len(input) > len(prefix):
+ if input.startswith(prefix):
+ pos = input.find(u'"', len(prefix))
+ if pos >= 0:
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = u"utf-8"
+ return prefix + encoding + input[pos:]
+ # we haven't seen the end of the encoding name yet => fall through
+ else:
+ return input # doesn't start with prefix, so nothing to fix
+ elif not prefix.startswith(input) or final:
+ # can't turn out to be a @charset rule later (or there is no "later")
+ return input
+ if final:
+ return input
+ return None # don't know yet
+
+
+def decode(input, errors="strict", encoding=None, force=True):
+ if encoding is None or not force:
+ (_encoding, explicit) = detectencoding_str(input, True)
+ if _encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not force) or encoding is None: # Take the encoding from the input
+ encoding = _encoding
+ (input, consumed) = codecs.getdecoder(encoding)(input, errors)
+ return (_fixencoding(input, unicode(encoding), True), consumed)
+
+
+def encode(input, errors="strict", encoding=None):
+ consumed = len(input)
+ if encoding is None:
+ encoding = detectencoding_unicode(input, True)[0]
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, u"utf-8", True)
+ else:
+ input = _fixencoding(input, unicode(encoding), True)
+ if encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ encoder = codecs.getencoder(encoding)
+ return (encoder(input, errors)[0], consumed)
+
+
+def _bytes2int(bytes):
+ # Helper: convert an 8 bit string into an ``int``.
+ i = 0
+ for byte in bytes:
+ i = (i<<8) + ord(byte)
+ return i
+
+
+def _int2bytes(i):
+ # Helper: convert an ``int`` into an 8-bit string.
+ v = []
+ while i:
+ v.insert(0, chr(i&0xff))
+ i >>= 8
+ return "".join(v)
+
+
+if hasattr(codecs, "IncrementalDecoder"):
+ class IncrementalDecoder(codecs.IncrementalDecoder):
+ def __init__(self, errors="strict", encoding=None, force=True):
+ self.decoder = None
+ self.encoding = encoding
+ self.force = force
+ codecs.IncrementalDecoder.__init__(self, errors)
+ # Store ``errors`` somewhere else,
+ # because we have to hide it in a property
+ self._errors = errors
+ self.buffer = u"".encode()
+ self.headerfixed = False
+
+ def iterdecode(self, input):
+ for part in input:
+ result = self.decode(part, False)
+ if result:
+ yield result
+ result = self.decode("", True)
+ if result:
+ yield result
+
+ def decode(self, input, final=False):
+ # We're doing basically the same as a ``BufferedIncrementalDecoder``,
+ # but since the buffer is only relevant until the encoding has been
+ # detected (in which case the buffer of the underlying codec might
+ # kick in), we're implementing buffering ourselves to avoid some
+ # overhead.
+ if self.decoder is None:
+ input = self.buffer + input
+ # Do we have to detect the encoding from the input?
+ if self.encoding is None or not self.force:
+ (encoding, explicit) = detectencoding_str(input, final)
+ if encoding is None: # no encoding determined yet
+ self.buffer = input # retry the complete input on the next call
+ return u"" # no encoding determined yet, so no output
+ elif encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input
+ self.encoding = encoding
+ self.buffer = "" # drop buffer, as the decoder might keep its own
+ decoder = codecs.getincrementaldecoder(self.encoding)
+ self.decoder = decoder(self._errors)
+ if self.headerfixed:
+ return self.decoder.decode(input, final)
+ # If we haven't fixed the header yet,
+ # the content of ``self.buffer`` is a ``unicode`` object
+ output = self.buffer + self.decoder.decode(input, final)
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newoutput = _fixencoding(output, unicode(encoding), final)
+ if newoutput is None:
+ # retry fixing the @charset rule (but keep the decoded stuff)
+ self.buffer = output
+ return u""
+ self.headerfixed = True
+ return newoutput
+
+ def reset(self):
+ codecs.IncrementalDecoder.reset(self)
+ self.decoder = None
+ self.buffer = u"".encode()
+ self.headerfixed = False
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the real decoder too
+ if self.decoder is not None:
+ self.decoder.errors = errors
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+ def getstate(self):
+ if self.decoder is not None:
+ state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate())
+ else:
+ state = (self.encoding, self.buffer, self.headerfixed, False, None)
+ return ("", _bytes2int(marshal.dumps(state)))
+
+ def setstate(self, state):
+ state = _int2bytes(marshal.loads(state[1])) # ignore buffered input
+ self.encoding = state[0]
+ self.buffer = state[1]
+ self.headerfixed = state[2]
+ if state[3] is not None:
+ self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors)
+ self.decoder.setstate(state[4])
+ else:
+ self.decoder = None
+
+
+if hasattr(codecs, "IncrementalEncoder"):
+ class IncrementalEncoder(codecs.IncrementalEncoder):
+ def __init__(self, errors="strict", encoding=None):
+ self.encoder = None
+ self.encoding = encoding
+ codecs.IncrementalEncoder.__init__(self, errors)
+ # Store ``errors`` somewhere else,
+ # because we have to hide it in a property
+ self._errors = errors
+ self.buffer = u""
+
+ def iterencode(self, input):
+ for part in input:
+ result = self.encode(part, False)
+ if result:
+ yield result
+ result = self.encode(u"", True)
+ if result:
+ yield result
+
+ def encode(self, input, final=False):
+ if self.encoder is None:
+ input = self.buffer + input
+ if self.encoding is not None:
+ # Replace encoding in the @charset rule with the specified one
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newinput = _fixencoding(input, unicode(encoding), final)
+ if newinput is None: # @charset rule incomplete => Retry next time
+ self.buffer = input
+ return ""
+ input = newinput
+ else:
+ # Use encoding from the @charset declaration
+ self.encoding = detectencoding_unicode(input, final)[0]
+ if self.encoding is not None:
+ if self.encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ info = codecs.lookup(self.encoding)
+ encoding = self.encoding
+ if self.encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, u"utf-8", True)
+ self.encoder = info.incrementalencoder(self._errors)
+ self.buffer = u""
+ else:
+ self.buffer = input
+ return ""
+ return self.encoder.encode(input, final)
+
+ def reset(self):
+ codecs.IncrementalEncoder.reset(self)
+ self.encoder = None
+ self.buffer = u""
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors ``must be done on the real encoder too
+ if self.encoder is not None:
+ self.encoder.errors = errors
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+ def getstate(self):
+ if self.encoder is not None:
+ state = (self.encoding, self.buffer, True, self.encoder.getstate())
+ else:
+ state = (self.encoding, self.buffer, False, None)
+ return _bytes2int(marshal.dumps(state))
+
+ def setstate(self, state):
+ state = _int2bytes(marshal.loads(state))
+ self.encoding = state[0]
+ self.buffer = state[1]
+ if state[2] is not None:
+ self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors)
+ self.encoder.setstate(state[4])
+ else:
+ self.encoder = None
+
+
+class StreamWriter(codecs.StreamWriter):
+ def __init__(self, stream, errors="strict", encoding=None, header=False):
+ codecs.StreamWriter.__init__(self, stream, errors)
+ self.streamwriter = None
+ self.encoding = encoding
+ self._errors = errors
+ self.buffer = u""
+
+ def encode(self, input, errors='strict'):
+ li = len(input)
+ if self.streamwriter is None:
+ input = self.buffer + input
+ li = len(input)
+ if self.encoding is not None:
+ # Replace encoding in the @charset rule with the specified one
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newinput = _fixencoding(input, unicode(encoding), False)
+ if newinput is None: # @charset rule incomplete => Retry next time
+ self.buffer = input
+ return ("", 0)
+ input = newinput
+ else:
+ # Use encoding from the @charset declaration
+ self.encoding = detectencoding_unicode(input, False)[0]
+ if self.encoding is not None:
+ if self.encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors)
+ encoding = self.encoding
+ if self.encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, u"utf-8", True)
+ self.buffer = u""
+ else:
+ self.buffer = input
+ return ("", 0)
+ return (self.streamwriter.encode(input, errors)[0], li)
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the streamwriter too
+ if self.streamwriter is not None:
+ self.streamwriter.errors = errors
+ self._errors = errors
+
+ errors = property(_geterrors, _seterrors)
+
+
+class StreamReader(codecs.StreamReader):
+ def __init__(self, stream, errors="strict", encoding=None, force=True):
+ codecs.StreamReader.__init__(self, stream, errors)
+ self.streamreader = None
+ self.encoding = encoding
+ self.force = force
+ self._errors = errors
+
+ def decode(self, input, errors='strict'):
+ if self.streamreader is None:
+ if self.encoding is None or not self.force:
+ (encoding, explicit) = detectencoding_str(input, False)
+ if encoding is None: # no encoding determined yet
+ return (u"", 0) # no encoding determined yet, so no output
+ elif encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input
+ self.encoding = encoding
+ streamreader = codecs.getreader(self.encoding)
+ streamreader = streamreader(self.stream, self._errors)
+ (output, consumed) = streamreader.decode(input, errors)
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newoutput = _fixencoding(output, unicode(encoding), False)
+ if newoutput is not None:
+ self.streamreader = streamreader
+ return (newoutput, consumed)
+ return (u"", 0) # we will create a new streamreader on the next call
+ return self.streamreader.decode(input, errors)
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the streamreader too
+ if self.streamreader is not None:
+ self.streamreader.errors = errors
+ self._errors = errors
+
+ errors = property(_geterrors, _seterrors)
+
+
+if hasattr(codecs, "CodecInfo"):
+ # We're running on Python 2.5 or better
+ def search_function(name):
+ if name == "css":
+ return codecs.CodecInfo(
+ name="css",
+ encode=encode,
+ decode=decode,
+ incrementalencoder=IncrementalEncoder,
+ incrementaldecoder=IncrementalDecoder,
+ streamwriter=StreamWriter,
+ streamreader=StreamReader,
+ )
+else:
+ # If we're running on Python 2.4, define the utf-8-sig codec here
+ def utf8sig_encode(input, errors='strict'):
+ return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input))
+
+ def utf8sig_decode(input, errors='strict'):
+ prefix = 0
+ if input[:3] == codecs.BOM_UTF8:
+ input = input[3:]
+ prefix = 3
+ (output, consumed) = codecs.utf_8_decode(input, errors, True)
+ return (output, consumed+prefix)
+
+ class UTF8SigStreamWriter(codecs.StreamWriter):
+ def reset(self):
+ codecs.StreamWriter.reset(self)
+ try:
+ del self.encode
+ except AttributeError:
+ pass
+
+ def encode(self, input, errors='strict'):
+ self.encode = codecs.utf_8_encode
+ return utf8sig_encode(input, errors)
+
+ class UTF8SigStreamReader(codecs.StreamReader):
+ def reset(self):
+ codecs.StreamReader.reset(self)
+ try:
+ del self.decode
+ except AttributeError:
+ pass
+
+ def decode(self, input, errors='strict'):
+ if len(input) < 3 and codecs.BOM_UTF8.startswith(input):
+ # not enough data to decide if this is a BOM
+ # => try again on the next call
+ return (u"", 0)
+ self.decode = codecs.utf_8_decode
+ return utf8sig_decode(input, errors)
+
+ def search_function(name):
+ import encodings
+ name = encodings.normalize_encoding(name)
+ if name == "css":
+ return (encode, decode, StreamReader, StreamWriter)
+ elif name == "utf_8_sig":
+ return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter)
+
+
+codecs.register(search_function)
+
+
+# Error handler for CSS escaping
+
+def cssescape(exc):
+ if not isinstance(exc, UnicodeEncodeError):
+ raise TypeError("don't know how to handle %r" % exc)
+ return (u"".join(u"\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end)
+
+codecs.register_error("cssescape", cssescape)
diff --git a/libs/cssutils/_codec3.py b/libs/cssutils/_codec3.py
new file mode 100755
index 00000000..878a7b29
--- /dev/null
+++ b/libs/cssutils/_codec3.py
@@ -0,0 +1,608 @@
+#!/usr/bin/env python
+"""Python codec for CSS."""
+__docformat__ = 'restructuredtext'
+__author__ = 'Walter Doerwald'
+__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $'
+
+import sys
+import codecs
+import marshal
+
+# We're using bits to store all possible candidate encodings (or variants, i.e.
+# we have two bits for the variants of UTF-16 and two for the
+# variants of UTF-32).
+#
+# Prefixes for various CSS encodings
+# UTF-8-SIG xEF xBB xBF
+# UTF-16 (LE) xFF xFE ~x00|~x00
+# UTF-16 (BE) xFE xFF
+# UTF-16-LE @ x00 @ x00
+# UTF-16-BE x00 @
+# UTF-32 (LE) xFF xFE x00 x00
+# UTF-32 (BE) x00 x00 xFE xFF
+# UTF-32-LE @ x00 x00 x00
+# UTF-32-BE x00 x00 x00 @
+# CHARSET @ c h a ...
+
+
+def chars(bytestring):
+ return ''.join(chr(byte) for byte in bytestring)
+
+
+def detectencoding_str(input, final=False):
+ """
+ Detect the encoding of the byte string ``input``, which contains the
+ beginning of a CSS file. This function returns the detected encoding (or
+ ``None`` if it hasn't got enough data), and a flag that indicates whether
+ that encoding has been detected explicitely or implicitely. To detect the
+ encoding the first few bytes are used (or if ``input`` is ASCII compatible
+ and starts with a charset rule the encoding name from the rule). "Explicit"
+ detection means that the bytes start with a BOM or a charset rule.
+
+ If the encoding can't be detected yet, ``None`` is returned as the encoding.
+ ``final`` specifies whether more data will be available in later calls or
+ not. If ``final`` is true, ``detectencoding_str()`` will never return
+ ``None`` as the encoding.
+ """
+
+ # A bit for every candidate
+ CANDIDATE_UTF_8_SIG = 1
+ CANDIDATE_UTF_16_AS_LE = 2
+ CANDIDATE_UTF_16_AS_BE = 4
+ CANDIDATE_UTF_16_LE = 8
+ CANDIDATE_UTF_16_BE = 16
+ CANDIDATE_UTF_32_AS_LE = 32
+ CANDIDATE_UTF_32_AS_BE = 64
+ CANDIDATE_UTF_32_LE = 128
+ CANDIDATE_UTF_32_BE = 256
+ CANDIDATE_CHARSET = 512
+
+ candidates = 1023 # all candidates
+
+ #input = chars(input)
+ li = len(input)
+ if li>=1:
+ # Check first byte
+ c = input[0]
+ if c != b"\xef"[0]:
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != b"\xff"[0]:
+ candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE)
+ if c != b"\xfe"[0]:
+ candidates &= ~CANDIDATE_UTF_16_AS_BE
+ if c != b"@"[0]:
+ candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET)
+ if c != b"\x00"[0]:
+ candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE)
+ if li>=2:
+ # Check second byte
+ c = input[1]
+ if c != b"\xbb"[0]:
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != b"\xfe"[0]:
+ candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE)
+ if c != b"\xff"[0]:
+ candidates &= ~CANDIDATE_UTF_16_AS_BE
+ if c != b"\x00"[0]:
+ candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE)
+ if c != b"@"[0]:
+ candidates &= ~CANDIDATE_UTF_16_BE
+ if c != b"c"[0]:
+ candidates &= ~CANDIDATE_CHARSET
+ if li>=3:
+ # Check third byte
+ c = input[2]
+ if c != b"\xbf"[0]:
+ candidates &= ~CANDIDATE_UTF_8_SIG
+ if c != b"c"[0]:
+ candidates &= ~CANDIDATE_UTF_16_LE
+ if c != b"\x00"[0]:
+ candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE)
+ if c != b"\xfe"[0]:
+ candidates &= ~CANDIDATE_UTF_32_AS_BE
+ if c != b"h"[0]:
+ candidates &= ~CANDIDATE_CHARSET
+ if li>=4:
+ # Check fourth byte
+ c = input[3]
+ if input[2:4] == b"\x00\x00"[0:2]:
+ candidates &= ~CANDIDATE_UTF_16_AS_LE
+ if c != b"\x00"[0]:
+ candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE)
+ if c != b"\xff"[0]:
+ candidates &= ~CANDIDATE_UTF_32_AS_BE
+ if c != b"@"[0]:
+ candidates &= ~CANDIDATE_UTF_32_BE
+ if c != b"a"[0]:
+ candidates &= ~CANDIDATE_CHARSET
+ if candidates == 0:
+ return ("utf-8", False)
+ if not (candidates & (candidates-1)): # only one candidate remaining
+ if candidates == CANDIDATE_UTF_8_SIG and li >= 3:
+ return ("utf-8-sig", True)
+ elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2:
+ return ("utf-16", True)
+ elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2:
+ return ("utf-16", True)
+ elif candidates == CANDIDATE_UTF_16_LE and li >= 4:
+ return ("utf-16-le", False)
+ elif candidates == CANDIDATE_UTF_16_BE and li >= 2:
+ return ("utf-16-be", False)
+ elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4:
+ return ("utf-32", True)
+ elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4:
+ return ("utf-32", True)
+ elif candidates == CANDIDATE_UTF_32_LE and li >= 4:
+ return ("utf-32-le", False)
+ elif candidates == CANDIDATE_UTF_32_BE and li >= 4:
+ return ("utf-32-be", False)
+ elif candidates == CANDIDATE_CHARSET and li >= 4:
+ prefix = '@charset "'
+ charsinput = chars(input)
+ if charsinput[:len(prefix)] == prefix:
+ pos = charsinput.find('"', len(prefix))
+ if pos >= 0:
+ # TODO: return str and not bytes!
+ return (charsinput[len(prefix):pos], True)
+ # if this is the last call, and we haven't determined an encoding yet,
+ # we default to UTF-8
+ if final:
+ return ("utf-8", False)
+ return (None, False) # dont' know yet
+
+
+def detectencoding_unicode(input, final=False):
+ """
+ Detect the encoding of the unicode string ``input``, which contains the
+ beginning of a CSS file. The encoding is detected from the charset rule
+ at the beginning of ``input``. If there is no charset rule, ``"utf-8"``
+ will be returned.
+
+ If the encoding can't be detected yet, ``None`` is returned. ``final``
+ specifies whether more data will be available in later calls or not. If
+ ``final`` is true, ``detectencoding_unicode()`` will never return ``None``.
+ """
+ prefix = '@charset "'
+ if input.startswith(prefix):
+ pos = input.find('"', len(prefix))
+ if pos >= 0:
+ return (input[len(prefix):pos], True)
+ elif final or not prefix.startswith(input):
+ # if this is the last call, and we haven't determined an encoding yet,
+ # (or the string definitely doesn't start with prefix) we default to UTF-8
+ return ("utf-8", False)
+ return (None, False) # don't know yet
+
+
+def _fixencoding(input, encoding, final=False):
+ """
+ Replace the name of the encoding in the charset rule at the beginning of
+ ``input`` with ``encoding``. If ``input`` doesn't starts with a charset
+ rule, ``input`` will be returned unmodified.
+
+ If the encoding can't be found yet, ``None`` is returned. ``final``
+ specifies whether more data will be available in later calls or not.
+ If ``final`` is true, ``_fixencoding()`` will never return ``None``.
+ """
+ prefix = '@charset "'
+ if len(input) > len(prefix):
+ if input.startswith(prefix):
+ pos = input.find('"', len(prefix))
+ if pos >= 0:
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ return prefix + encoding + input[pos:]
+ # we haven't seen the end of the encoding name yet => fall through
+ else:
+ return input # doesn't start with prefix, so nothing to fix
+ elif not prefix.startswith(input) or final:
+ # can't turn out to be a @charset rule later (or there is no "later")
+ return input
+ if final:
+ return input
+ return None # don't know yet
+
+
+def decode(input, errors="strict", encoding=None, force=True):
+ try:
+ # py 3 only, memory?! object to bytes
+ input = input.tobytes()
+ except AttributeError as e:
+ pass
+
+ if encoding is None or not force:
+ (_encoding, explicit) = detectencoding_str(input, True)
+ if _encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not force) or encoding is None: # Take the encoding from the input
+ encoding = _encoding
+
+ # NEEDS: change in parse.py (str to bytes!)
+ (input, consumed) = codecs.getdecoder(encoding)(input, errors)
+ return (_fixencoding(input, str(encoding), True), consumed)
+
+
+def encode(input, errors="strict", encoding=None):
+ consumed = len(input)
+ if encoding is None:
+ encoding = detectencoding_unicode(input, True)[0]
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, "utf-8", True)
+ else:
+ input = _fixencoding(input, str(encoding), True)
+ if encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ encoder = codecs.getencoder(encoding)
+ return (encoder(input, errors)[0], consumed)
+
+
+def _bytes2int(bytes):
+ # Helper: convert an 8 bit string into an ``int``.
+ i = 0
+ for byte in bytes:
+ i = (i<<8) + ord(byte)
+ return i
+
+
+def _int2bytes(i):
+ # Helper: convert an ``int`` into an 8-bit string.
+ v = []
+ while i:
+ v.insert(0, chr(i&0xff))
+ i >>= 8
+ return "".join(v)
+
+
+if hasattr(codecs, "IncrementalDecoder"):
+ class IncrementalDecoder(codecs.IncrementalDecoder):
+ def __init__(self, errors="strict", encoding=None, force=True):
+ self.decoder = None
+ self.encoding = encoding
+ self.force = force
+ codecs.IncrementalDecoder.__init__(self, errors)
+ # Store ``errors`` somewhere else,
+ # because we have to hide it in a property
+ self._errors = errors
+ self.buffer = b""
+ self.headerfixed = False
+
+ def iterdecode(self, input):
+ for part in input:
+ result = self.decode(part, False)
+ if result:
+ yield result
+ result = self.decode("", True)
+ if result:
+ yield result
+
+ def decode(self, input, final=False):
+ # We're doing basically the same as a ``BufferedIncrementalDecoder``,
+ # but since the buffer is only relevant until the encoding has been
+ # detected (in which case the buffer of the underlying codec might
+ # kick in), we're implementing buffering ourselves to avoid some
+ # overhead.
+ if self.decoder is None:
+ input = self.buffer + input
+ # Do we have to detect the encoding from the input?
+ if self.encoding is None or not self.force:
+ (encoding, explicit) = detectencoding_str(input, final)
+ if encoding is None: # no encoding determined yet
+ self.buffer = input # retry the complete input on the next call
+ return "" # no encoding determined yet, so no output
+ elif encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input
+ self.encoding = encoding
+ self.buffer = "" # drop buffer, as the decoder might keep its own
+ decoder = codecs.getincrementaldecoder(self.encoding)
+ self.decoder = decoder(self._errors)
+ if self.headerfixed:
+ return self.decoder.decode(input, final)
+ # If we haven't fixed the header yet,
+ # the content of ``self.buffer`` is a ``unicode`` object
+ output = self.buffer + self.decoder.decode(input, final)
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newoutput = _fixencoding(output, str(encoding), final)
+ if newoutput is None:
+ # retry fixing the @charset rule (but keep the decoded stuff)
+ self.buffer = output
+ return ""
+ self.headerfixed = True
+ return newoutput
+
+ def reset(self):
+ codecs.IncrementalDecoder.reset(self)
+ self.decoder = None
+ self.buffer = b""
+ self.headerfixed = False
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the real decoder too
+ if self.decoder is not None:
+ self.decoder.errors = errors
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+ def getstate(self):
+ if self.decoder is not None:
+ state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate())
+ else:
+ state = (self.encoding, self.buffer, self.headerfixed, False, None)
+ return ("", _bytes2int(marshal.dumps(state)))
+
+ def setstate(self, state):
+ state = _int2bytes(marshal.loads(state[1])) # ignore buffered input
+ self.encoding = state[0]
+ self.buffer = state[1]
+ self.headerfixed = state[2]
+ if state[3] is not None:
+ self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors)
+ self.decoder.setstate(state[4])
+ else:
+ self.decoder = None
+
+
+if hasattr(codecs, "IncrementalEncoder"):
+ class IncrementalEncoder(codecs.IncrementalEncoder):
+ def __init__(self, errors="strict", encoding=None):
+ self.encoder = None
+ self.encoding = encoding
+ codecs.IncrementalEncoder.__init__(self, errors)
+ # Store ``errors`` somewhere else,
+ # because we have to hide it in a property
+ self._errors = errors
+ self.buffer = ""
+
+ def iterencode(self, input):
+ for part in input:
+ result = self.encode(part, False)
+ if result:
+ yield result
+ result = self.encode("", True)
+ if result:
+ yield result
+
+ def encode(self, input, final=False):
+ if self.encoder is None:
+ input = self.buffer + input
+ if self.encoding is not None:
+ # Replace encoding in the @charset rule with the specified one
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newinput = _fixencoding(input, str(encoding), final)
+ if newinput is None: # @charset rule incomplete => Retry next time
+ self.buffer = input
+ return ""
+ input = newinput
+ else:
+ # Use encoding from the @charset declaration
+ self.encoding = detectencoding_unicode(input, final)[0]
+ if self.encoding is not None:
+ if self.encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ info = codecs.lookup(self.encoding)
+ encoding = self.encoding
+ if self.encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, "utf-8", True)
+ self.encoder = info.incrementalencoder(self._errors)
+ self.buffer = ""
+ else:
+ self.buffer = input
+ return ""
+ return self.encoder.encode(input, final)
+
+ def reset(self):
+ codecs.IncrementalEncoder.reset(self)
+ self.encoder = None
+ self.buffer = ""
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors ``must be done on the real encoder too
+ if self.encoder is not None:
+ self.encoder.errors = errors
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+ def getstate(self):
+ if self.encoder is not None:
+ state = (self.encoding, self.buffer, True, self.encoder.getstate())
+ else:
+ state = (self.encoding, self.buffer, False, None)
+ return _bytes2int(marshal.dumps(state))
+
+ def setstate(self, state):
+ state = _int2bytes(marshal.loads(state))
+ self.encoding = state[0]
+ self.buffer = state[1]
+ if state[2] is not None:
+ self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors)
+ self.encoder.setstate(state[4])
+ else:
+ self.encoder = None
+
+
+class StreamWriter(codecs.StreamWriter):
+ def __init__(self, stream, errors="strict", encoding=None, header=False):
+ codecs.StreamWriter.__init__(self, stream, errors)
+ self.streamwriter = None
+ self.encoding = encoding
+ self._errors = errors
+ self.buffer = ""
+
+ def encode(self, input, errors='strict'):
+ li = len(input)
+ if self.streamwriter is None:
+ input = self.buffer + input
+ li = len(input)
+ if self.encoding is not None:
+ # Replace encoding in the @charset rule with the specified one
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newinput = _fixencoding(input, str(encoding), False)
+ if newinput is None: # @charset rule incomplete => Retry next time
+ self.buffer = input
+ return ("", 0)
+ input = newinput
+ else:
+ # Use encoding from the @charset declaration
+ self.encoding = detectencoding_unicode(input, False)[0]
+ if self.encoding is not None:
+ if self.encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors)
+ encoding = self.encoding
+ if self.encoding.replace("_", "-").lower() == "utf-8-sig":
+ input = _fixencoding(input, "utf-8", True)
+ self.buffer = ""
+ else:
+ self.buffer = input
+ return ("", 0)
+ return (self.streamwriter.encode(input, errors)[0], li)
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the streamwriter too
+ try:
+ if self.streamwriter is not None:
+ self.streamwriter.errors = errors
+ except AttributeError as e:
+ # TODO: py3 only exception?
+ pass
+
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+
+class StreamReader(codecs.StreamReader):
+ def __init__(self, stream, errors="strict", encoding=None, force=True):
+ codecs.StreamReader.__init__(self, stream, errors)
+ self.streamreader = None
+ self.encoding = encoding
+ self.force = force
+ self._errors = errors
+
+ def decode(self, input, errors='strict'):
+ if self.streamreader is None:
+ if self.encoding is None or not self.force:
+ (encoding, explicit) = detectencoding_str(input, False)
+ if encoding is None: # no encoding determined yet
+ return ("", 0) # no encoding determined yet, so no output
+ elif encoding == "css":
+ raise ValueError("css not allowed as encoding name")
+ if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input
+ self.encoding = encoding
+ streamreader = codecs.getreader(self.encoding)
+ streamreader = streamreader(self.stream, self._errors)
+ (output, consumed) = streamreader.decode(input, errors)
+ encoding = self.encoding
+ if encoding.replace("_", "-").lower() == "utf-8-sig":
+ encoding = "utf-8"
+ newoutput = _fixencoding(output, str(encoding), False)
+ if newoutput is not None:
+ self.streamreader = streamreader
+ return (newoutput, consumed)
+ return ("", 0) # we will create a new streamreader on the next call
+ return self.streamreader.decode(input, errors)
+
+ def _geterrors(self):
+ return self._errors
+
+ def _seterrors(self, errors):
+ # Setting ``errors`` must be done on the streamreader too
+ try:
+ if self.streamreader is not None:
+ self.streamreader.errors = errors
+ except AttributeError as e:
+ # TODO: py3 only exception?
+ pass
+
+ self._errors = errors
+ errors = property(_geterrors, _seterrors)
+
+
+if hasattr(codecs, "CodecInfo"):
+ # We're running on Python 2.5 or better
+ def search_function(name):
+ if name == "css":
+ return codecs.CodecInfo(
+ name="css",
+ encode=encode,
+ decode=decode,
+ incrementalencoder=IncrementalEncoder,
+ incrementaldecoder=IncrementalDecoder,
+ streamwriter=StreamWriter,
+ streamreader=StreamReader,
+ )
+else:
+ # If we're running on Python 2.4, define the utf-8-sig codec here
+ def utf8sig_encode(input, errors='strict'):
+ return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input))
+
+ def utf8sig_decode(input, errors='strict'):
+ prefix = 0
+ if input[:3] == codecs.BOM_UTF8:
+ input = input[3:]
+ prefix = 3
+ (output, consumed) = codecs.utf_8_decode(input, errors, True)
+ return (output, consumed+prefix)
+
+ class UTF8SigStreamWriter(codecs.StreamWriter):
+ def reset(self):
+ codecs.StreamWriter.reset(self)
+ try:
+ del self.encode
+ except AttributeError:
+ pass
+
+ def encode(self, input, errors='strict'):
+ self.encode = codecs.utf_8_encode
+ return utf8sig_encode(input, errors)
+
+ class UTF8SigStreamReader(codecs.StreamReader):
+ def reset(self):
+ codecs.StreamReader.reset(self)
+ try:
+ del self.decode
+ except AttributeError:
+ pass
+
+ def decode(self, input, errors='strict'):
+ if len(input) < 3 and codecs.BOM_UTF8.startswith(input):
+ # not enough data to decide if this is a BOM
+ # => try again on the next call
+ return ("", 0)
+ self.decode = codecs.utf_8_decode
+ return utf8sig_decode(input, errors)
+
+ def search_function(name):
+ import encodings
+ name = encodings.normalize_encoding(name)
+ if name == "css":
+ return (encode, decode, StreamReader, StreamWriter)
+ elif name == "utf_8_sig":
+ return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter)
+
+
+codecs.register(search_function)
+
+
+# Error handler for CSS escaping
+
+def cssescape(exc):
+ if not isinstance(exc, UnicodeEncodeError):
+ raise TypeError("don't know how to handle %r" % exc)
+ return ("".join("\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end)
+
+codecs.register_error("cssescape", cssescape)
diff --git a/libs/cssutils/_fetch.py b/libs/cssutils/_fetch.py
new file mode 100755
index 00000000..a9138fe3
--- /dev/null
+++ b/libs/cssutils/_fetch.py
@@ -0,0 +1,44 @@
+"""Default URL reading functions"""
+__all__ = ['_defaultFetcher']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $'
+
+import cssutils
+from cssutils import VERSION
+import encutils
+import errorhandler
+import urllib2
+
+log = errorhandler.ErrorHandler()
+
+def _defaultFetcher(url):
+ """Retrieve data from ``url``. cssutils default implementation of fetch
+ URL function.
+
+ Returns ``(encoding, string)`` or ``None``
+ """
+ try:
+ request = urllib2.Request(url)
+ request.add_header('User-agent',
+ 'cssutils %s (http://www.cthedot.de/cssutils/)' % VERSION)
+ res = urllib2.urlopen(request)
+ except OSError, e:
+ # e.g if file URL and not found
+ log.warn(e, error=OSError)
+ except (OSError, ValueError), e:
+ # invalid url, e.g. "1"
+ log.warn(u'ValueError, %s' % e.args[0], error=ValueError)
+ except urllib2.HTTPError, e:
+ # http error, e.g. 404, e can be raised
+ log.warn(u'HTTPError opening url=%s: %s %s' %
+ (url, e.code, e.msg), error=e)
+ except urllib2.URLError, e:
+ # URLError like mailto: or other IO errors, e can be raised
+ log.warn(u'URLError, %s' % e.reason, error=e)
+ else:
+ if res:
+ mimeType, encoding = encutils.getHTTPInfo(res)
+ if mimeType != u'text/css':
+ log.error(u'Expected "text/css" mime type for url=%r but found: %r' %
+ (url, mimeType), error=ValueError)
+ return encoding, res.read()
diff --git a/libs/cssutils/_fetchgae.py b/libs/cssutils/_fetchgae.py
new file mode 100755
index 00000000..ef91d412
--- /dev/null
+++ b/libs/cssutils/_fetchgae.py
@@ -0,0 +1,68 @@
+"""GAE specific URL reading functions"""
+__all__ = ['_defaultFetcher']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $'
+
+# raises ImportError of not on GAE
+from google.appengine.api import urlfetch
+import cgi
+import errorhandler
+import util
+
+log = errorhandler.ErrorHandler()
+
+def _defaultFetcher(url):
+ """
+ uses GoogleAppEngine (GAE)
+ fetch(url, payload=None, method=GET, headers={}, allow_truncated=False)
+
+ Response
+ content
+ The body content of the response.
+ content_was_truncated
+ True if the allow_truncated parameter to fetch() was True and
+ the response exceeded the maximum response size. In this case,
+ the content attribute contains the truncated response.
+ status_code
+ The HTTP status code.
+ headers
+ The HTTP response headers, as a mapping of names to values.
+
+ Exceptions
+ exception InvalidURLError()
+ The URL of the request was not a valid URL, or it used an
+ unsupported method. Only http and https URLs are supported.
+ exception DownloadError()
+ There was an error retrieving the data.
+
+ This exception is not raised if the server returns an HTTP
+ error code: In that case, the response data comes back intact,
+ including the error code.
+
+ exception ResponseTooLargeError()
+ The response data exceeded the maximum allowed size, and the
+ allow_truncated parameter passed to fetch() was False.
+ """
+ #from google.appengine.api import urlfetch
+ try:
+ r = urlfetch.fetch(url, method=urlfetch.GET)
+ except urlfetch.Error, e:
+ log.warn(u'Error opening url=%r: %s' % (url, e),
+ error=IOError)
+ else:
+ if r.status_code == 200:
+ # find mimetype and encoding
+ mimetype = 'application/octet-stream'
+ try:
+ mimetype, params = cgi.parse_header(r.headers['content-type'])
+ encoding = params['charset']
+ except KeyError:
+ encoding = None
+ if mimetype != u'text/css':
+ log.error(u'Expected "text/css" mime type for url %r but found: %r' %
+ (url, mimetype), error=ValueError)
+ return encoding, r.content
+ else:
+ # TODO: 301 etc
+ log.warn(u'Error opening url=%r: HTTP status %s' %
+ (url, r.status_code), error=IOError)
diff --git a/libs/cssutils/codec.py b/libs/cssutils/codec.py
new file mode 100755
index 00000000..c694e1fc
--- /dev/null
+++ b/libs/cssutils/codec.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+"""Python codec for CSS."""
+__docformat__ = 'restructuredtext'
+__author__ = 'Walter Doerwald'
+__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $'
+
+import sys
+
+if sys.version_info < (3,):
+ from _codec2 import *
+ # for tests
+ from _codec2 import _fixencoding
+else:
+ from _codec3 import *
+ # for tests
+ from _codec3 import _fixencoding
diff --git a/libs/cssutils/css/__init__.py b/libs/cssutils/css/__init__.py
new file mode 100755
index 00000000..4cc4a904
--- /dev/null
+++ b/libs/cssutils/css/__init__.py
@@ -0,0 +1,80 @@
+"""Implements Document Object Model Level 2 CSS
+http://www.w3.org/TR/2000/PR-DOM-Level-2-Style-20000927/css.html
+
+currently implemented
+ - CSSStyleSheet
+ - CSSRuleList
+ - CSSRule
+ - CSSComment (cssutils addon)
+ - CSSCharsetRule
+ - CSSFontFaceRule
+ - CSSImportRule
+ - CSSMediaRule
+ - CSSNamespaceRule (WD)
+ - CSSPageRule
+ - CSSStyleRule
+ - CSSUnkownRule
+ - Selector and SelectorList
+ - CSSStyleDeclaration
+ - CSS2Properties
+ - CSSValue
+ - CSSPrimitiveValue
+ - CSSValueList
+ - CSSVariablesRule
+ - CSSVariablesDeclaration
+
+todo
+ - RGBColor, Rect, Counter
+"""
+__all__ = [
+ 'CSSStyleSheet',
+ 'CSSRuleList',
+ 'CSSRule',
+ 'CSSComment',
+ 'CSSCharsetRule',
+ 'CSSFontFaceRule'
+ 'CSSImportRule',
+ 'CSSMediaRule',
+ 'CSSNamespaceRule',
+ 'CSSPageRule',
+ 'MarginRule',
+ 'CSSStyleRule',
+ 'CSSUnknownRule',
+ 'CSSVariablesRule',
+ 'CSSVariablesDeclaration',
+ 'Selector', 'SelectorList',
+ 'CSSStyleDeclaration', 'Property',
+ #'CSSValue', 'CSSPrimitiveValue', 'CSSValueList'
+ 'PropertyValue',
+ 'Value',
+ 'ColorValue',
+ 'DimensionValue',
+ 'URIValue',
+ 'CSSFunction',
+ 'CSSVariable',
+ 'MSValue'
+ ]
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssstylesheet import *
+from cssrulelist import *
+from cssrule import *
+from csscomment import *
+from csscharsetrule import *
+from cssfontfacerule import *
+from cssimportrule import *
+from cssmediarule import *
+from cssnamespacerule import *
+from csspagerule import *
+from marginrule import *
+from cssstylerule import *
+from cssvariablesrule import *
+from cssunknownrule import *
+from selector import *
+from selectorlist import *
+from cssstyledeclaration import *
+from cssvariablesdeclaration import *
+from property import *
+#from cssvalue import *
+from value import *
diff --git a/libs/cssutils/css/colors.py b/libs/cssutils/css/colors.py
new file mode 100755
index 00000000..5f8dd899
--- /dev/null
+++ b/libs/cssutils/css/colors.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+"""
+Built from something like this:
+
+ print [
+ (
+ row[2].text_content().strip(),
+ eval(row[4].text_content().strip())
+ )
+ for row in lxml.html.parse('http://www.w3.org/TR/css3-color/')
+ .xpath("//*[@class='colortable']//tr[position()>1]")
+ ]
+
+by Simon Sapin
+"""
+
+COLORS = {
+ 'transparent': (0, 0, 0, 0.0),
+
+ 'black': (0, 0, 0, 1.0),
+ 'silver': (192, 192, 192, 1.0),
+ 'gray': (128, 128, 128, 1.0),
+ 'white': (255, 255, 255, 1.0),
+ 'maroon': (128, 0, 0, 1.0),
+ 'red': (255, 0, 0, 1.0),
+ 'purple': (128, 0, 128, 1.0),
+ 'fuchsia': (255, 0, 255, 1.0),
+ 'green': (0, 128, 0, 1.0),
+ 'lime': (0, 255, 0, 1.0),
+ 'olive': (128, 128, 0, 1.0),
+ 'yellow': (255, 255, 0, 1.0),
+ 'navy': (0, 0, 128, 1.0),
+ 'blue': (0, 0, 255, 1.0),
+ 'teal': (0, 128, 128, 1.0),
+
+ 'aqua': (0, 255, 255, 1.0),
+ 'aliceblue': (240, 248, 255, 1.0),
+ 'antiquewhite': (250, 235, 215, 1.0),
+ 'aqua': (0, 255, 255, 1.0),
+ 'aquamarine': (127, 255, 212, 1.0),
+ 'azure': (240, 255, 255, 1.0),
+ 'beige': (245, 245, 220, 1.0),
+ 'bisque': (255, 228, 196, 1.0),
+ 'black': (0, 0, 0, 1.0),
+ 'blanchedalmond': (255, 235, 205, 1.0),
+ 'blue': (0, 0, 255, 1.0),
+ 'blueviolet': (138, 43, 226, 1.0),
+ 'brown': (165, 42, 42, 1.0),
+ 'burlywood': (222, 184, 135, 1.0),
+ 'cadetblue': (95, 158, 160, 1.0),
+ 'chartreuse': (127, 255, 0, 1.0),
+ 'chocolate': (210, 105, 30, 1.0),
+ 'coral': (255, 127, 80, 1.0),
+ 'cornflowerblue': (100, 149, 237, 1.0),
+ 'cornsilk': (255, 248, 220, 1.0),
+ 'crimson': (220, 20, 60, 1.0),
+ 'cyan': (0, 255, 255, 1.0),
+ 'darkblue': (0, 0, 139, 1.0),
+ 'darkcyan': (0, 139, 139, 1.0),
+ 'darkgoldenrod': (184, 134, 11, 1.0),
+ 'darkgray': (169, 169, 169, 1.0),
+ 'darkgreen': (0, 100, 0, 1.0),
+ 'darkgrey': (169, 169, 169, 1.0),
+ 'darkkhaki': (189, 183, 107, 1.0),
+ 'darkmagenta': (139, 0, 139, 1.0),
+ 'darkolivegreen': (85, 107, 47, 1.0),
+ 'darkorange': (255, 140, 0, 1.0),
+ 'darkorchid': (153, 50, 204, 1.0),
+ 'darkred': (139, 0, 0, 1.0),
+ 'darksalmon': (233, 150, 122, 1.0),
+ 'darkseagreen': (143, 188, 143, 1.0),
+ 'darkslateblue': (72, 61, 139, 1.0),
+ 'darkslategray': (47, 79, 79, 1.0),
+ 'darkslategrey': (47, 79, 79, 1.0),
+ 'darkturquoise': (0, 206, 209, 1.0),
+ 'darkviolet': (148, 0, 211, 1.0),
+ 'deeppink': (255, 20, 147, 1.0),
+ 'deepskyblue': (0, 191, 255, 1.0),
+ 'dimgray': (105, 105, 105, 1.0),
+ 'dimgrey': (105, 105, 105, 1.0),
+ 'dodgerblue': (30, 144, 255, 1.0),
+ 'firebrick': (178, 34, 34, 1.0),
+ 'floralwhite': (255, 250, 240, 1.0),
+ 'forestgreen': (34, 139, 34, 1.0),
+ 'fuchsia': (255, 0, 255, 1.0),
+ 'gainsboro': (220, 220, 220, 1.0),
+ 'ghostwhite': (248, 248, 255, 1.0),
+ 'gold': (255, 215, 0, 1.0),
+ 'goldenrod': (218, 165, 32, 1.0),
+ 'gray': (128, 128, 128, 1.0),
+ 'green': (0, 128, 0, 1.0),
+ 'greenyellow': (173, 255, 47, 1.0),
+ 'grey': (128, 128, 128, 1.0),
+ 'honeydew': (240, 255, 240, 1.0),
+ 'hotpink': (255, 105, 180, 1.0),
+ 'indianred': (205, 92, 92, 1.0),
+ 'indigo': (75, 0, 130, 1.0),
+ 'ivory': (255, 255, 240, 1.0),
+ 'khaki': (240, 230, 140, 1.0),
+ 'lavender': (230, 230, 250, 1.0),
+ 'lavenderblush': (255, 240, 245, 1.0),
+ 'lawngreen': (124, 252, 0, 1.0),
+ 'lemonchiffon': (255, 250, 205, 1.0),
+ 'lightblue': (173, 216, 230, 1.0),
+ 'lightcoral': (240, 128, 128, 1.0),
+ 'lightcyan': (224, 255, 255, 1.0),
+ 'lightgoldenrodyellow': (250, 250, 210, 1.0),
+ 'lightgray': (211, 211, 211, 1.0),
+ 'lightgreen': (144, 238, 144, 1.0),
+ 'lightgrey': (211, 211, 211, 1.0),
+ 'lightpink': (255, 182, 193, 1.0),
+ 'lightsalmon': (255, 160, 122, 1.0),
+ 'lightseagreen': (32, 178, 170, 1.0),
+ 'lightskyblue': (135, 206, 250, 1.0),
+ 'lightslategray': (119, 136, 153, 1.0),
+ 'lightslategrey': (119, 136, 153, 1.0),
+ 'lightsteelblue': (176, 196, 222, 1.0),
+ 'lightyellow': (255, 255, 224, 1.0),
+ 'lime': (0, 255, 0, 1.0),
+ 'limegreen': (50, 205, 50, 1.0),
+ 'linen': (250, 240, 230, 1.0),
+ 'magenta': (255, 0, 255, 1.0),
+ 'maroon': (128, 0, 0, 1.0),
+ 'mediumaquamarine': (102, 205, 170, 1.0),
+ 'mediumblue': (0, 0, 205, 1.0),
+ 'mediumorchid': (186, 85, 211, 1.0),
+ 'mediumpurple': (147, 112, 219, 1.0),
+ 'mediumseagreen': (60, 179, 113, 1.0),
+ 'mediumslateblue': (123, 104, 238, 1.0),
+ 'mediumspringgreen': (0, 250, 154, 1.0),
+ 'mediumturquoise': (72, 209, 204, 1.0),
+ 'mediumvioletred': (199, 21, 133, 1.0),
+ 'midnightblue': (25, 25, 112, 1.0),
+ 'mintcream': (245, 255, 250, 1.0),
+ 'mistyrose': (255, 228, 225, 1.0),
+ 'moccasin': (255, 228, 181, 1.0),
+ 'navajowhite': (255, 222, 173, 1.0),
+ 'navy': (0, 0, 128, 1.0),
+ 'oldlace': (253, 245, 230, 1.0),
+ 'olive': (128, 128, 0, 1.0),
+ 'olivedrab': (107, 142, 35, 1.0),
+ 'orange': (255, 165, 0, 1.0),
+ 'orangered': (255, 69, 0, 1.0),
+ 'orchid': (218, 112, 214, 1.0),
+ 'palegoldenrod': (238, 232, 170, 1.0),
+ 'palegreen': (152, 251, 152, 1.0),
+ 'paleturquoise': (175, 238, 238, 1.0),
+ 'palevioletred': (219, 112, 147, 1.0),
+ 'papayawhip': (255, 239, 213, 1.0),
+ 'peachpuff': (255, 218, 185, 1.0),
+ 'peru': (205, 133, 63, 1.0),
+ 'pink': (255, 192, 203, 1.0),
+ 'plum': (221, 160, 221, 1.0),
+ 'powderblue': (176, 224, 230, 1.0),
+ 'purple': (128, 0, 128, 1.0),
+ 'red': (255, 0, 0, 1.0),
+ 'rosybrown': (188, 143, 143, 1.0),
+ 'royalblue': (65, 105, 225, 1.0),
+ 'saddlebrown': (139, 69, 19, 1.0),
+ 'salmon': (250, 128, 114, 1.0),
+ 'sandybrown': (244, 164, 96, 1.0),
+ 'seagreen': (46, 139, 87, 1.0),
+ 'seashell': (255, 245, 238, 1.0),
+ 'sienna': (160, 82, 45, 1.0),
+ 'silver': (192, 192, 192, 1.0),
+ 'skyblue': (135, 206, 235, 1.0),
+ 'slateblue': (106, 90, 205, 1.0),
+ 'slategray': (112, 128, 144, 1.0),
+ 'slategrey': (112, 128, 144, 1.0),
+ 'snow': (255, 250, 250, 1.0),
+ 'springgreen': (0, 255, 127, 1.0),
+ 'steelblue': (70, 130, 180, 1.0),
+ 'tan': (210, 180, 140, 1.0),
+ 'teal': (0, 128, 128, 1.0),
+ 'thistle': (216, 191, 216, 1.0),
+ 'tomato': (255, 99, 71, 1.0),
+ 'turquoise': (64, 224, 208, 1.0),
+ 'violet': (238, 130, 238, 1.0),
+ 'wheat': (245, 222, 179, 1.0),
+ 'white': (255, 255, 255, 1.0),
+ 'whitesmoke': (245, 245, 245, 1.0),
+ 'yellow': (255, 255, 0, 1.0),
+ 'yellowgreen': (154, 205, 50, 1.0),
+}
diff --git a/libs/cssutils/css/csscharsetrule.py b/libs/cssutils/css/csscharsetrule.py
new file mode 100755
index 00000000..fa6b7c4d
--- /dev/null
+++ b/libs/cssutils/css/csscharsetrule.py
@@ -0,0 +1,159 @@
+"""CSSCharsetRule implements DOM Level 2 CSS CSSCharsetRule."""
+__all__ = ['CSSCharsetRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import codecs
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSCharsetRule(cssrule.CSSRule):
+ """
+ The CSSCharsetRule interface represents an @charset rule in a CSS style
+ sheet. The value of the encoding attribute does not affect the encoding
+ of text data in the DOM objects; this encoding is always UTF-16
+ (also in Python?). After a stylesheet is loaded, the value of the
+ encoding attribute is the value found in the @charset rule. If there
+ was no @charset in the original document, then no CSSCharsetRule is
+ created. The value of the encoding attribute may also be used as a hint
+ for the encoding used on serialization of the style sheet.
+
+ The value of the @charset rule (and therefore of the CSSCharsetRule)
+ may not correspond to the encoding the document actually came in;
+ character encoding information e.g. in an HTTP header, has priority
+ (see CSS document representation) but this is not reflected in the
+ CSSCharsetRule.
+
+ This rule is not really needed anymore as setting
+ :attr:`CSSStyleSheet.encoding` is much easier.
+
+ Format::
+
+ charsetrule:
+ CHARSET_SYM S* STRING S* ';'
+
+ BUT: Only valid format is (single space, double quotes!)::
+
+ @charset "ENCODING";
+ """
+ def __init__(self, encoding=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ :param encoding:
+ a valid character encoding
+ :param readonly:
+ defaults to False, not used yet
+ """
+ super(CSSCharsetRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = '@charset'
+
+ if encoding:
+ self.encoding = encoding
+ else:
+ self._encoding = None
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(encoding=%r)" % (
+ self.__class__.__name__,
+ self.encoding)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.encoding,
+ id(self))
+
+ def _getCssText(self):
+ """The parsable textual representation."""
+ return cssutils.ser.do_CSSCharsetRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText:
+ A parsable DOMString.
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSCharsetRule, self)._setCssText(cssText)
+
+ wellformed = True
+ tokenizer = self._tokenize2(cssText)
+
+ if self._type(self._nexttoken(tokenizer)) != self._prods.CHARSET_SYM:
+ wellformed = False
+ self._log.error(u'CSSCharsetRule must start with "@charset "',
+ error=xml.dom.InvalidModificationErr)
+
+ encodingtoken = self._nexttoken(tokenizer)
+ encodingtype = self._type(encodingtoken)
+ encoding = self._stringtokenvalue(encodingtoken)
+ if self._prods.STRING != encodingtype or not encoding:
+ wellformed = False
+ self._log.error(u'CSSCharsetRule: no encoding found; %r.' %
+ self._valuestr(cssText))
+
+ semicolon = self._tokenvalue(self._nexttoken(tokenizer))
+ EOFtype = self._type(self._nexttoken(tokenizer))
+ if u';' != semicolon or EOFtype not in ('EOF', None):
+ wellformed = False
+ self._log.error(u'CSSCharsetRule: Syntax Error: %r.' %
+ self._valuestr(cssText))
+
+ if wellformed:
+ self.encoding = encoding
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc=u"(DOM) The parsable textual representation.")
+
+ def _setEncoding(self, encoding):
+ """
+ :param encoding:
+ a valid encoding to be used. Currently only valid Python encodings
+ are allowed.
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this encoding rule is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified encoding value has a syntax error and
+ is unparsable.
+ """
+ self._checkReadonly()
+ tokenizer = self._tokenize2(encoding)
+ encodingtoken = self._nexttoken(tokenizer)
+ unexpected = self._nexttoken(tokenizer)
+
+ if not encodingtoken or unexpected or\
+ self._prods.IDENT != self._type(encodingtoken):
+ self._log.error(u'CSSCharsetRule: Syntax Error in encoding value '
+ u'%r.' % encoding)
+ else:
+ try:
+ codecs.lookup(encoding)
+ except LookupError:
+ self._log.error(u'CSSCharsetRule: Unknown (Python) encoding %r.'
+ % encoding)
+ else:
+ self._encoding = encoding.lower()
+
+ encoding = property(lambda self: self._encoding, _setEncoding,
+ doc=u"(DOM)The encoding information used in this @charset rule.")
+
+ type = property(lambda self: self.CHARSET_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ wellformed = property(lambda self: bool(self.encoding))
diff --git a/libs/cssutils/css/csscomment.py b/libs/cssutils/css/csscomment.py
new file mode 100755
index 00000000..311df0fd
--- /dev/null
+++ b/libs/cssutils/css/csscomment.py
@@ -0,0 +1,87 @@
+"""CSSComment is not defined in DOM Level 2 at all but a cssutils defined
+class only.
+
+Implements CSSRule which is also extended for a CSSComment rule type.
+"""
+__all__ = ['CSSComment']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSComment(cssrule.CSSRule):
+ """
+ Represents a CSS comment (cssutils only).
+
+ Format::
+
+ /*...*/
+ """
+ def __init__(self, cssText=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ super(CSSComment, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+
+ self._cssText = None
+ if cssText:
+ self._setCssText(cssText)
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(cssText=%r)" % (
+ self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.cssText,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSComment(self)
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText:
+ textual text to set or tokenlist which is not tokenized
+ anymore. May also be a single token for this rule
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSComment, self)._setCssText(cssText)
+ tokenizer = self._tokenize2(cssText)
+
+ commenttoken = self._nexttoken(tokenizer)
+ unexpected = self._nexttoken(tokenizer)
+
+ if not commenttoken or\
+ self._type(commenttoken) != self._prods.COMMENT or\
+ unexpected:
+ self._log.error(u'CSSComment: Not a CSSComment: %r' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ self._cssText = self._tokenvalue(commenttoken)
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"The parsable textual representation of this rule.")
+
+ type = property(lambda self: self.COMMENT,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ # constant but needed:
+ wellformed = property(lambda self: True)
diff --git a/libs/cssutils/css/cssfontfacerule.py b/libs/cssutils/css/cssfontfacerule.py
new file mode 100755
index 00000000..1e936066
--- /dev/null
+++ b/libs/cssutils/css/cssfontfacerule.py
@@ -0,0 +1,184 @@
+"""CSSFontFaceRule implements DOM Level 2 CSS CSSFontFaceRule.
+
+From cssutils 0.9.6 additions from CSS Fonts Module Level 3 are
+added http://www.w3.org/TR/css3-fonts/.
+"""
+__all__ = ['CSSFontFaceRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssstyledeclaration import CSSStyleDeclaration
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSFontFaceRule(cssrule.CSSRule):
+ """
+ The CSSFontFaceRule interface represents a @font-face rule in a CSS
+ style sheet. The @font-face rule is used to hold a set of font
+ descriptions.
+
+ Format::
+
+ font_face
+ : FONT_FACE_SYM S*
+ '{' S* declaration [ ';' S* declaration ]* '}' S*
+ ;
+
+ cssutils uses a :class:`~cssutils.css.CSSStyleDeclaration` to
+ represent the font descriptions. For validation a specific profile
+ is used though were some properties have other valid values than
+ when used in e.g. a :class:`~cssutils.css.CSSStyleRule`.
+ """
+ def __init__(self, style=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ If readonly allows setting of properties in constructor only.
+
+ :param style:
+ CSSStyleDeclaration used to hold any font descriptions
+ for this CSSFontFaceRule
+ """
+ super(CSSFontFaceRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@font-face'
+
+ if style:
+ self.style = style
+ else:
+ self.style = CSSStyleDeclaration()
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(style=%r)" % (
+ self.__class__.__name__,
+ self.style.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.style.cssText,
+ self.valid,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSFontFaceRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSFontFaceRule, self)._setCssText(cssText)
+
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if self._type(attoken) != self._prods.FONT_FACE_SYM:
+ self._log.error(u'CSSFontFaceRule: No CSSFontFaceRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ newStyle = CSSStyleDeclaration(parentRule=self)
+ ok = True
+
+ beforetokens, brace = self._tokensupto2(tokenizer,
+ blockstartonly=True,
+ separateEnd=True)
+ if self._tokenvalue(brace) != u'{':
+ ok = False
+ self._log.error(u'CSSFontFaceRule: No start { of style '
+ u'declaration found: %r'
+ % self._valuestr(cssText), brace)
+
+ # parse stuff before { which should be comments and S only
+ new = {'wellformed': True}
+ newseq = self._tempSeq()
+
+ beforewellformed, expected = self._parse(expected=':',
+ seq=newseq, tokenizer=self._tokenize2(beforetokens),
+ productions={})
+ ok = ok and beforewellformed and new['wellformed']
+
+ styletokens, braceorEOFtoken = self._tokensupto2(tokenizer,
+ blockendonly=True,
+ separateEnd=True)
+
+ val, type_ = self._tokenvalue(braceorEOFtoken),\
+ self._type(braceorEOFtoken)
+ if val != u'}' and type_ != 'EOF':
+ ok = False
+ self._log.error(u'CSSFontFaceRule: No "}" after style '
+ u'declaration found: %r'
+ % self._valuestr(cssText))
+
+ nonetoken = self._nexttoken(tokenizer)
+ if nonetoken:
+ ok = False
+ self._log.error(u'CSSFontFaceRule: Trailing content found.',
+ token=nonetoken)
+
+ if 'EOF' == type_:
+ # add again as style needs it
+ styletokens.append(braceorEOFtoken)
+
+ # SET, may raise:
+ newStyle.cssText = styletokens
+
+ if ok:
+ # contains probably comments only (upto ``{``)
+ self._setSeq(newseq)
+ self.style = newStyle
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) The parsable textual representation of this "
+ u"rule.")
+
+ def _setStyle(self, style):
+ """
+ :param style:
+ a CSSStyleDeclaration or string
+ """
+ self._checkReadonly()
+ if isinstance(style, basestring):
+ self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
+ else:
+ style._parentRule = self
+ self._style = style
+
+ style = property(lambda self: self._style, _setStyle,
+ doc=u"(DOM) The declaration-block of this rule set, "
+ u"a :class:`~cssutils.css.CSSStyleDeclaration`.")
+
+ type = property(lambda self: self.FONT_FACE_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ def _getValid(self):
+ needed = ['font-family', 'src']
+ for p in self.style.getProperties(all=True):
+ if not p.valid:
+ return False
+ try:
+ needed.remove(p.name)
+ except ValueError:
+ pass
+ return not bool(needed)
+
+ valid = property(_getValid,
+ doc=u"CSSFontFace is valid if properties `font-family` "
+ u"and `src` are set and all properties are valid.")
+
+ # constant but needed:
+ wellformed = property(lambda self: True)
diff --git a/libs/cssutils/css/cssimportrule.py b/libs/cssutils/css/cssimportrule.py
new file mode 100755
index 00000000..59fe2564
--- /dev/null
+++ b/libs/cssutils/css/cssimportrule.py
@@ -0,0 +1,396 @@
+"""CSSImportRule implements DOM Level 2 CSS CSSImportRule plus the
+``name`` property from http://www.w3.org/TR/css3-cascade/#cascading."""
+__all__ = ['CSSImportRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssrule
+import cssutils
+import os
+import urlparse
+import xml.dom
+
+class CSSImportRule(cssrule.CSSRule):
+ """
+ Represents an @import rule within a CSS style sheet. The @import rule
+ is used to import style rules from other style sheets.
+
+ Format::
+
+ import
+ : IMPORT_SYM S*
+ [STRING|URI] S* [ medium [ COMMA S* medium]* ]? S* STRING? S* ';' S*
+ ;
+ """
+ def __init__(self, href=None, mediaText=None, name=None,
+ parentRule=None, parentStyleSheet=None, readonly=False):
+ """
+ If readonly allows setting of properties in constructor only
+
+ :param href:
+ location of the style sheet to be imported.
+ :param mediaText:
+ A list of media types for which this style sheet may be used
+ as a string
+ :param name:
+ Additional name of imported style sheet
+ """
+ super(CSSImportRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@import'
+ self._styleSheet = None
+
+ # string or uri used for reserialization
+ self.hreftype = None
+
+ # prepare seq
+ seq = self._tempSeq()
+ seq.append(None, 'href')
+ #seq.append(None, 'media')
+ seq.append(None, 'name')
+ self._setSeq(seq)
+
+ # 1. media
+ if mediaText:
+ self.media = mediaText
+ else:
+ # must be all for @import
+ self.media = cssutils.stylesheets.MediaList(mediaText=u'all')
+ # 2. name
+ self.name = name
+ # 3. href and styleSheet
+ self.href = href
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ if self._usemedia:
+ mediaText = self.media.mediaText
+ else:
+ mediaText = None
+ return u"cssutils.css.%s(href=%r, mediaText=%r, name=%r)" % (
+ self.__class__.__name__,
+ self.href,
+ self.media.mediaText,
+ self.name)
+
+ def __str__(self):
+ if self._usemedia:
+ mediaText = self.media.mediaText
+ else:
+ mediaText = None
+ return u""\
+ % (self.__class__.__name__,
+ self.href,
+ mediaText,
+ self.name,
+ id(self))
+
+ _usemedia = property(lambda self: self.media.mediaText not in (u'', u'all'),
+ doc="if self.media is used (or simply empty)")
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSImportRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ """
+ super(CSSImportRule, self)._setCssText(cssText)
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if self._type(attoken) != self._prods.IMPORT_SYM:
+ self._log.error(u'CSSImportRule: No CSSImportRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ # for closures: must be a mutable
+ new = {'keyword': self._tokenvalue(attoken),
+ 'href': None,
+ 'hreftype': None,
+ 'media': None,
+ 'name': None,
+ 'wellformed': True
+ }
+
+ def __doname(seq, token):
+ # called by _string or _ident
+ new['name'] = self._stringtokenvalue(token)
+ seq.append(new['name'], 'name')
+ return ';'
+
+ def _string(expected, seq, token, tokenizer=None):
+ if 'href' == expected:
+ # href
+ new['href'] = self._stringtokenvalue(token)
+ new['hreftype'] = 'string'
+ seq.append(new['href'], 'href')
+ return 'media name ;'
+ elif 'name' in expected:
+ # name
+ return __doname(seq, token)
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSImportRule: Unexpected string.', token)
+ return expected
+
+ def _uri(expected, seq, token, tokenizer=None):
+ # href
+ if 'href' == expected:
+ uri = self._uritokenvalue(token)
+ new['hreftype'] = 'uri'
+ new['href'] = uri
+ seq.append(new['href'], 'href')
+ return 'media name ;'
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSImportRule: Unexpected URI.', token)
+ return expected
+
+ def _ident(expected, seq, token, tokenizer=None):
+ # medialist ending with ; which is checked upon too
+ if expected.startswith('media'):
+ mediatokens = self._tokensupto2(
+ tokenizer, importmediaqueryendonly=True)
+ mediatokens.insert(0, token) # push found token
+
+ last = mediatokens.pop() # retrieve ;
+ lastval, lasttyp = self._tokenvalue(last), self._type(last)
+ if lastval != u';' and lasttyp not in ('EOF',
+ self._prods.STRING):
+ new['wellformed'] = False
+ self._log.error(u'CSSImportRule: No ";" found: %s' %
+ self._valuestr(cssText), token=token)
+
+ newMedia = cssutils.stylesheets.MediaList(parentRule=self)
+ newMedia.mediaText = mediatokens
+ if newMedia.wellformed:
+ new['media'] = newMedia
+ seq.append(newMedia, 'media')
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSImportRule: Invalid MediaList: %s' %
+ self._valuestr(cssText), token=token)
+
+ if lasttyp == self._prods.STRING:
+ # name
+ return __doname(seq, last)
+ else:
+ return 'EOF' # ';' is token "last"
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSImportRule: Unexpected ident.', token)
+ return expected
+
+ def _char(expected, seq, token, tokenizer=None):
+ # final ;
+ val = self._tokenvalue(token)
+ if expected.endswith(';') and u';' == val:
+ return 'EOF'
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSImportRule: Unexpected char.', token)
+ return expected
+
+ # import : IMPORT_SYM S* [STRING|URI]
+ # S* [ medium [ ',' S* medium]* ]? ';' S*
+ # STRING? # see http://www.w3.org/TR/css3-cascade/#cascading
+ # ;
+ newseq = self._tempSeq()
+ wellformed, expected = self._parse(expected='href',
+ seq=newseq, tokenizer=tokenizer,
+ productions={'STRING': _string,
+ 'URI': _uri,
+ 'IDENT': _ident,
+ 'CHAR': _char},
+ new=new)
+
+ # wellformed set by parse
+ ok = wellformed and new['wellformed']
+
+ # post conditions
+ if not new['href']:
+ ok = False
+ self._log.error(u'CSSImportRule: No href found: %s' %
+ self._valuestr(cssText))
+
+ if expected != 'EOF':
+ ok = False
+ self._log.error(u'CSSImportRule: No ";" found: %s' %
+ self._valuestr(cssText))
+
+ # set all
+ if ok:
+ self._setSeq(newseq)
+
+ self.atkeyword = new['keyword']
+ self.hreftype = new['hreftype']
+ self.name = new['name']
+
+ if new['media']:
+ self.media = new['media']
+ else:
+ # must be all for @import
+ self.media = cssutils.stylesheets.MediaList(mediaText=u'all')
+
+ # needs new self.media
+ self.href = new['href']
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc="(DOM) The parsable textual representation of this rule.")
+
+ def _setHref(self, href):
+ # set new href
+ self._href = href
+ # update seq
+ for i, item in enumerate(self.seq):
+ val, type_ = item.value, item.type
+ if 'href' == type_:
+ self._seq[i] = (href, type_, item.line, item.col)
+ break
+
+ importedSheet = cssutils.css.CSSStyleSheet(media=self.media,
+ ownerRule=self,
+ title=self.name)
+ self.hrefFound = False
+ # set styleSheet
+ if href and self.parentStyleSheet:
+ # loading errors are all catched!
+
+ # relative href
+ parentHref = self.parentStyleSheet.href
+ if parentHref is None:
+ # use cwd instead
+ parentHref = cssutils.helper.path2url(os.getcwd()) + '/'
+
+ fullhref = urlparse.urljoin(parentHref, self.href)
+
+ # all possible exceptions are ignored
+ try:
+ usedEncoding, enctype, cssText = \
+ self.parentStyleSheet._resolveImport(fullhref)
+
+ if cssText is None:
+ # catched in next except below!
+ raise IOError('Cannot read Stylesheet.')
+
+ # contentEncoding with parentStyleSheet.overrideEncoding,
+ # HTTP or parent
+ encodingOverride, encoding = None, None
+
+ if enctype == 0:
+ encodingOverride = usedEncoding
+ elif 0 < enctype < 5:
+ encoding = usedEncoding
+
+ # inherit fetcher for @imports in styleSheet
+ importedSheet._href = fullhref
+ importedSheet._setFetcher(self.parentStyleSheet._fetcher)
+ importedSheet._setCssTextWithEncodingOverride(
+ cssText,
+ encodingOverride=encodingOverride,
+ encoding=encoding)
+
+ except (OSError, IOError, ValueError), e:
+ self._log.warn(u'CSSImportRule: While processing imported '
+ u'style sheet href=%s: %r'
+ % (self.href, e), neverraise=True)
+
+ else:
+ # used by resolveImports if to keep unprocessed href
+ self.hrefFound = True
+
+ self._styleSheet = importedSheet
+
+ _href = None # needs to be set
+ href = property(lambda self: self._href, _setHref,
+ doc=u"Location of the style sheet to be imported.")
+
+ def _setMedia(self, media):
+ """
+ :param media:
+ a :class:`~cssutils.stylesheets.MediaList` or string
+ """
+ self._checkReadonly()
+ if isinstance(media, basestring):
+ self._media = cssutils.stylesheets.MediaList(mediaText=media,
+ parentRule=self)
+ else:
+ media._parentRule = self
+ self._media = media
+
+ # update seq
+ ihref = 0
+ for i, item in enumerate(self.seq):
+ if item.type == 'href':
+ ihref = i
+ elif item.type == 'media':
+ self.seq[i] = (self._media, 'media', None, None)
+ break
+ else:
+ # if no media until now add after href
+ self.seq.insert(ihref+1,
+ self._media, 'media', None, None)
+
+ media = property(lambda self: self._media, _setMedia,
+ doc=u"(DOM) A list of media types for this rule "
+ u"of type :class:`~cssutils.stylesheets.MediaList`.")
+
+ def _setName(self, name=u''):
+ """Raises xml.dom.SyntaxErr if name is not a string."""
+ if name is None or isinstance(name, basestring):
+ # "" or '' handled as None
+ if not name:
+ name = None
+
+ # save name
+ self._name = name
+
+ # update seq
+ for i, item in enumerate(self.seq):
+ val, typ = item.value, item.type
+ if 'name' == typ:
+ self._seq[i] = (name, typ, item.line, item.col)
+ break
+
+ # set title of imported sheet
+ if self.styleSheet:
+ self.styleSheet.title = name
+
+ else:
+ self._log.error(u'CSSImportRule: Not a valid name: %s' % name)
+
+ name = property(lambda self: self._name, _setName,
+ doc=u"An optional name for the imported sheet.")
+
+ styleSheet = property(lambda self: self._styleSheet,
+ doc=u"(readonly) The style sheet referred to by this "
+ u"rule.")
+
+ type = property(lambda self: self.IMPORT_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ def _getWellformed(self):
+ "Depending on if media is used at all."
+ if self._usemedia:
+ return bool(self.href and self.media.wellformed)
+ else:
+ return bool(self.href)
+
+ wellformed = property(_getWellformed)
diff --git a/libs/cssutils/css/cssmediarule.py b/libs/cssutils/css/cssmediarule.py
new file mode 100755
index 00000000..a7ebf463
--- /dev/null
+++ b/libs/cssutils/css/cssmediarule.py
@@ -0,0 +1,302 @@
+"""CSSMediaRule implements DOM Level 2 CSS CSSMediaRule."""
+__all__ = ['CSSMediaRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSMediaRule(cssrule.CSSRuleRules):
+ """
+ Objects implementing the CSSMediaRule interface can be identified by the
+ MEDIA_RULE constant. On these objects the type attribute must return the
+ value of that constant.
+
+ Format::
+
+ : MEDIA_SYM S* medium [ COMMA S* medium ]*
+
+ STRING? # the name
+
+ LBRACE S* ruleset* '}' S*;
+
+ ``cssRules``
+ All Rules in this media rule, a :class:`~cssutils.css.CSSRuleList`.
+ """
+ def __init__(self, mediaText='all', name=None,
+ parentRule=None, parentStyleSheet=None, readonly=False):
+ """constructor"""
+ super(CSSMediaRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@media'
+
+ # 1. media
+ if mediaText:
+ self.media = mediaText
+ else:
+ self.media = cssutils.stylesheets.MediaList()
+
+ self.name = name
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(mediaText=%r)" % (
+ self.__class__.__name__,
+ self.media.mediaText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.media.mediaText,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSMediaRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText:
+ a parseable string or a tuple of (cssText, dict-of-namespaces)
+ :Exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if a specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ # media "name"? { cssRules }
+ super(CSSMediaRule, self)._setCssText(cssText)
+
+ # might be (cssText, namespaces)
+ cssText, namespaces = self._splitNamespacesOff(cssText)
+
+ try:
+ # use parent style sheet ones if available
+ namespaces = self.parentStyleSheet.namespaces
+ except AttributeError:
+ pass
+
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if self._type(attoken) != self._prods.MEDIA_SYM:
+ self._log.error(u'CSSMediaRule: No CSSMediaRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+
+ else:
+ # save if parse goes wrong
+ oldMedia = self._media
+ oldName = self._name
+ oldCssRules = self._cssRules
+
+ ok = True
+
+ # media
+ mediatokens, end = self._tokensupto2(tokenizer,
+ mediaqueryendonly=True,
+ separateEnd=True)
+ if u'{' == self._tokenvalue(end)\
+ or self._prods.STRING == self._type(end):
+ self.media = cssutils.stylesheets.MediaList(parentRule=self)
+ # TODO: remove special case
+ self.media.mediaText = mediatokens
+ ok = ok and self.media.wellformed
+ else:
+ ok = False
+
+ # name (optional)
+ name = None
+ nameseq = self._tempSeq()
+ if self._prods.STRING == self._type(end):
+ name = self._stringtokenvalue(end)
+ # TODO: for now comments are lost after name
+ nametokens, end = self._tokensupto2(tokenizer,
+ blockstartonly=True,
+ separateEnd=True)
+ wellformed, expected = self._parse(None,
+ nameseq,
+ nametokens,
+ {})
+ if not wellformed:
+ ok = False
+ self._log.error(u'CSSMediaRule: Syntax Error: %s' %
+ self._valuestr(cssText))
+
+
+ # check for {
+ if u'{' != self._tokenvalue(end):
+ self._log.error(u'CSSMediaRule: No "{" found: %s' %
+ self._valuestr(cssText))
+ return
+
+ # cssRules
+ cssrulestokens, braceOrEOF = self._tokensupto2(tokenizer,
+ mediaendonly=True,
+ separateEnd=True)
+ nonetoken = self._nexttoken(tokenizer, None)
+ if 'EOF' == self._type(braceOrEOF):
+ # HACK!!!
+ # TODO: Not complete, add EOF to rule and } to @media
+ cssrulestokens.append(braceOrEOF)
+ braceOrEOF = ('CHAR', '}', 0, 0)
+ self._log.debug(u'CSSMediaRule: Incomplete, adding "}".',
+ token=braceOrEOF, neverraise=True)
+
+ if u'}' != self._tokenvalue(braceOrEOF):
+ self._log.error(u'CSSMediaRule: No "}" found.',
+ token=braceOrEOF)
+ elif nonetoken:
+ self._log.error(u'CSSMediaRule: Trailing content found.',
+ token=nonetoken)
+ else:
+ # for closures: must be a mutable
+ new = {'wellformed': True }
+
+ def COMMENT(expected, seq, token, tokenizer=None):
+ self.insertRule(cssutils.css.CSSComment([token],
+ parentRule=self,
+ parentStyleSheet=self.parentStyleSheet))
+ return expected
+
+ def ruleset(expected, seq, token, tokenizer):
+ rule = cssutils.css.CSSStyleRule(parentRule=self,
+ parentStyleSheet=self.parentStyleSheet)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return expected
+
+ def atrule(expected, seq, token, tokenizer):
+ # TODO: get complete rule!
+ tokens = self._tokensupto2(tokenizer, token)
+ atval = self._tokenvalue(token)
+ if atval in ('@charset ', '@font-face', '@import',
+ '@namespace', '@page', '@media', '@variables'):
+ self._log.error(u'CSSMediaRule: This rule is not '
+ u'allowed in CSSMediaRule - ignored: '
+ u'%s.' % self._valuestr(tokens),
+ token = token,
+ error=xml.dom.HierarchyRequestErr)
+ else:
+ rule = cssutils.css.CSSUnknownRule(tokens,
+ parentRule=self,
+ parentStyleSheet=self.parentStyleSheet)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return expected
+
+ # save for possible reset
+ oldCssRules = self.cssRules
+
+ self.cssRules = cssutils.css.CSSRuleList()
+ seq = [] # not used really
+
+ tokenizer = iter(cssrulestokens)
+ wellformed, expected = self._parse(braceOrEOF,
+ seq,
+ tokenizer, {
+ 'COMMENT': COMMENT,
+ 'CHARSET_SYM': atrule,
+ 'FONT_FACE_SYM': atrule,
+ 'IMPORT_SYM': atrule,
+ 'NAMESPACE_SYM': atrule,
+ 'PAGE_SYM': atrule,
+ 'MEDIA_SYM': atrule,
+ 'ATKEYWORD': atrule
+ },
+ default=ruleset,
+ new=new)
+ ok = ok and wellformed
+
+ if ok:
+ self.name = name
+ self._setSeq(nameseq)
+ else:
+ self._media = oldMedia
+ self._cssRules = oldCssRules
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) The parsable textual representation of this "
+ u"rule.")
+
+ def _setName(self, name):
+ if isinstance(name, basestring) or name is None:
+ # "" or ''
+ if not name:
+ name = None
+
+ self._name = name
+ else:
+ self._log.error(u'CSSImportRule: Not a valid name: %s' % name)
+
+ name = property(lambda self: self._name, _setName,
+ doc=u"An optional name for this media rule.")
+
+ def _setMedia(self, media):
+ """
+ :param media:
+ a :class:`~cssutils.stylesheets.MediaList` or string
+ """
+ self._checkReadonly()
+ if isinstance(media, basestring):
+ self._media = cssutils.stylesheets.MediaList(mediaText=media,
+ parentRule=self)
+ else:
+ media._parentRule = self
+ self._media = media
+
+ # NOT IN @media seq at all?!
+# # update seq
+# for i, item in enumerate(self.seq):
+# if item.type == 'media':
+# self._seq[i] = (self._media, 'media', None, None)
+# break
+# else:
+# # insert after @media if not in seq at all
+# self.seq.insert(0,
+# self._media, 'media', None, None)
+
+ media = property(lambda self: self._media, _setMedia,
+ doc=u"(DOM) A list of media types for this rule "
+ u"of type :class:`~cssutils.stylesheets.MediaList`.")
+
+
+ def insertRule(self, rule, index=None):
+ """Implements base ``insertRule``."""
+ rule, index = self._prepareInsertRule(rule, index)
+
+ if rule is False or rule is True:
+ # done or error
+ return
+
+ # check hierarchy
+ if isinstance(rule, cssutils.css.CSSCharsetRule) or \
+ isinstance(rule, cssutils.css.CSSFontFaceRule) or \
+ isinstance(rule, cssutils.css.CSSImportRule) or \
+ isinstance(rule, cssutils.css.CSSNamespaceRule) or \
+ isinstance(rule, cssutils.css.CSSPageRule) or \
+ isinstance(rule, cssutils.css.MarginRule) or \
+ isinstance(rule, CSSMediaRule):
+ self._log.error(u'%s: This type of rule is not allowed here: %s'
+ % (self.__class__.__name__, rule.cssText),
+ error=xml.dom.HierarchyRequestErr)
+ return
+
+ return self._finishInsertRule(rule, index)
+
+ type = property(lambda self: self.MEDIA_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ wellformed = property(lambda self: self.media.wellformed)
diff --git a/libs/cssutils/css/cssnamespacerule.py b/libs/cssutils/css/cssnamespacerule.py
new file mode 100755
index 00000000..a2610fc2
--- /dev/null
+++ b/libs/cssutils/css/cssnamespacerule.py
@@ -0,0 +1,295 @@
+"""CSSNamespaceRule currently implements http://dev.w3.org/csswg/css3-namespace/
+"""
+__all__ = ['CSSNamespaceRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSNamespaceRule(cssrule.CSSRule):
+ """
+ Represents an @namespace rule within a CSS style sheet.
+
+ The @namespace at-rule declares a namespace prefix and associates
+ it with a given namespace (a string). This namespace prefix can then be
+ used in namespace-qualified names such as those described in the
+ Selectors Module [SELECT] or the Values and Units module [CSS3VAL].
+
+ Dealing with these rules directly is not needed anymore, easier is
+ the use of :attr:`cssutils.css.CSSStyleSheet.namespaces`.
+
+ Format::
+
+ namespace
+ : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
+ ;
+ namespace_prefix
+ : IDENT
+ ;
+ """
+ def __init__(self, namespaceURI=None, prefix=None, cssText=None,
+ parentRule=None, parentStyleSheet=None, readonly=False):
+ """
+ :Parameters:
+ namespaceURI
+ The namespace URI (a simple string!) which is bound to the
+ given prefix. If no prefix is set
+ (``CSSNamespaceRule.prefix==''``) the namespace defined by
+ namespaceURI is set as the default namespace
+ prefix
+ The prefix used in the stylesheet for the given
+ ``CSSNamespaceRule.uri``.
+ cssText
+ if no namespaceURI is given cssText must be given to set
+ a namespaceURI as this is readonly later on
+ parentStyleSheet
+ sheet where this rule belongs to
+
+ Do not use as positional but as keyword parameters only!
+
+ If readonly allows setting of properties in constructor only
+
+ format namespace::
+
+ namespace
+ : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
+ ;
+ namespace_prefix
+ : IDENT
+ ;
+ """
+ super(CSSNamespaceRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@namespace'
+ self._prefix = u''
+ self._namespaceURI = None
+
+ if namespaceURI:
+ self.namespaceURI = namespaceURI
+ self.prefix = prefix
+ tempseq = self._tempSeq()
+ tempseq.append(self.prefix, 'prefix')
+ tempseq.append(self.namespaceURI, 'namespaceURI')
+ self._setSeq(tempseq)
+
+ elif cssText is not None:
+ self.cssText = cssText
+
+ if parentStyleSheet:
+ self._parentStyleSheet = parentStyleSheet
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(namespaceURI=%r, prefix=%r)" % (
+ self.__class__.__name__,
+ self.namespaceURI,
+ self.prefix)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.namespaceURI,
+ self.prefix,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText"""
+ return cssutils.ser.do_CSSNamespaceRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText: initial value for this rules cssText which is parsed
+ :exceptions:
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ """
+ super(CSSNamespaceRule, self)._setCssText(cssText)
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if self._type(attoken) != self._prods.NAMESPACE_SYM:
+ self._log.error(u'CSSNamespaceRule: No CSSNamespaceRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ # for closures: must be a mutable
+ new = {'keyword': self._tokenvalue(attoken),
+ 'prefix': u'',
+ 'uri': None,
+ 'wellformed': True
+ }
+
+ def _ident(expected, seq, token, tokenizer=None):
+ # the namespace prefix, optional
+ if 'prefix or uri' == expected:
+ new['prefix'] = self._tokenvalue(token)
+ seq.append(new['prefix'], 'prefix')
+ return 'uri'
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSNamespaceRule: Unexpected ident.', token)
+ return expected
+
+ def _string(expected, seq, token, tokenizer=None):
+ # the namespace URI as a STRING
+ if expected.endswith('uri'):
+ new['uri'] = self._stringtokenvalue(token)
+ seq.append(new['uri'], 'namespaceURI')
+ return ';'
+
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSNamespaceRule: Unexpected string.', token)
+ return expected
+
+ def _uri(expected, seq, token, tokenizer=None):
+ # the namespace URI as URI which is DEPRECATED
+ if expected.endswith('uri'):
+ uri = self._uritokenvalue(token)
+ new['uri'] = uri
+ seq.append(new['uri'], 'namespaceURI')
+ return ';'
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSNamespaceRule: Unexpected URI.', token)
+ return expected
+
+ def _char(expected, seq, token, tokenizer=None):
+ # final ;
+ val = self._tokenvalue(token)
+ if ';' == expected and u';' == val:
+ return 'EOF'
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'CSSNamespaceRule: Unexpected char.', token)
+ return expected
+
+ # "NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*"
+ newseq = self._tempSeq()
+ wellformed, expected = self._parse(expected='prefix or uri',
+ seq=newseq, tokenizer=tokenizer,
+ productions={'IDENT': _ident,
+ 'STRING': _string,
+ 'URI': _uri,
+ 'CHAR': _char},
+ new=new)
+
+ # wellformed set by parse
+ wellformed = wellformed and new['wellformed']
+
+ # post conditions
+ if new['uri'] is None:
+ wellformed = False
+ self._log.error(u'CSSNamespaceRule: No namespace URI found: %s'
+ % self._valuestr(cssText))
+
+ if expected != 'EOF':
+ wellformed = False
+ self._log.error(u'CSSNamespaceRule: No ";" found: %s' %
+ self._valuestr(cssText))
+
+ # set all
+ if wellformed:
+ self.atkeyword = new['keyword']
+ self._prefix = new['prefix']
+ self.namespaceURI = new['uri']
+ self._setSeq(newseq)
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc=u"(DOM) The parsable textual representation of this "
+ u"rule.")
+
+ def _setNamespaceURI(self, namespaceURI):
+ """
+ :param namespaceURI: the initial value for this rules namespaceURI
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ (CSSRule) Raised if this rule is readonly or a namespaceURI is
+ already set in this rule.
+ """
+ self._checkReadonly()
+ if not self._namespaceURI:
+ # initial setting
+ self._namespaceURI = namespaceURI
+ tempseq = self._tempSeq()
+ tempseq.append(namespaceURI, 'namespaceURI')
+ self._setSeq(tempseq) # makes seq readonly!
+ elif self._namespaceURI != namespaceURI:
+ self._log.error(u'CSSNamespaceRule: namespaceURI is readonly.',
+ error=xml.dom.NoModificationAllowedErr)
+
+ namespaceURI = property(lambda self: self._namespaceURI, _setNamespaceURI,
+ doc="URI (handled as simple string) of the defined namespace.")
+
+ def _replaceNamespaceURI(self, namespaceURI):
+ """Used during parse of new sheet only!
+
+ :param namespaceURI: the new value for this rules namespaceURI
+ """
+ self._namespaceURI = namespaceURI
+ for i, x in enumerate(self._seq):
+ if 'namespaceURI' == x.type:
+ self._seq._readonly = False
+ self._seq.replace(i, namespaceURI, 'namespaceURI')
+ self._seq._readonly = True
+ break
+
+ def _setPrefix(self, prefix=None):
+ """
+ :param prefix: the new prefix
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+ if not prefix:
+ prefix = u''
+ else:
+ tokenizer = self._tokenize2(prefix)
+ prefixtoken = self._nexttoken(tokenizer, None)
+ if not prefixtoken or self._type(prefixtoken) != self._prods.IDENT:
+ self._log.error(u'CSSNamespaceRule: No valid prefix "%s".' %
+ self._valuestr(prefix),
+ error=xml.dom.SyntaxErr)
+ return
+ else:
+ prefix = self._tokenvalue(prefixtoken)
+ # update seq
+ for i, x in enumerate(self._seq):
+ if x == self._prefix:
+ self._seq[i] = (prefix, 'prefix', None, None)
+ break
+ else:
+ # put prefix at the beginning!
+ self._seq[0] = (prefix, 'prefix', None, None)
+
+ # set new prefix
+ self._prefix = prefix
+
+ prefix = property(lambda self: self._prefix, _setPrefix,
+ doc=u"Prefix used for the defined namespace.")
+
+ type = property(lambda self: self.NAMESPACE_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ wellformed = property(lambda self: self.namespaceURI is not None)
+
\ No newline at end of file
diff --git a/libs/cssutils/css/csspagerule.py b/libs/cssutils/css/csspagerule.py
new file mode 100755
index 00000000..cc2f4516
--- /dev/null
+++ b/libs/cssutils/css/csspagerule.py
@@ -0,0 +1,436 @@
+"""CSSPageRule implements DOM Level 2 CSS CSSPageRule."""
+__all__ = ['CSSPageRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from itertools import chain
+from cssstyledeclaration import CSSStyleDeclaration
+from marginrule import MarginRule
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSPageRule(cssrule.CSSRuleRules):
+ """
+ The CSSPageRule interface represents a @page rule within a CSS style
+ sheet. The @page rule is used to specify the dimensions, orientation,
+ margins, etc. of a page box for paged media.
+
+ Format::
+
+ page :
+ PAGE_SYM S* IDENT? pseudo_page? S*
+ '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
+ ;
+
+ pseudo_page :
+ ':' [ "left" | "right" | "first" ]
+ ;
+
+ margin :
+ margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ ;
+
+ margin_sym :
+ TOPLEFTCORNER_SYM |
+ TOPLEFT_SYM |
+ TOPCENTER_SYM |
+ TOPRIGHT_SYM |
+ TOPRIGHTCORNER_SYM |
+ BOTTOMLEFTCORNER_SYM |
+ BOTTOMLEFT_SYM |
+ BOTTOMCENTER_SYM |
+ BOTTOMRIGHT_SYM |
+ BOTTOMRIGHTCORNER_SYM |
+ LEFTTOP_SYM |
+ LEFTMIDDLE_SYM |
+ LEFTBOTTOM_SYM |
+ RIGHTTOP_SYM |
+ RIGHTMIDDLE_SYM |
+ RIGHTBOTTOM_SYM
+ ;
+
+ `cssRules` contains a list of `MarginRule` objects.
+ """
+ def __init__(self, selectorText=None, style=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ If readonly allows setting of properties in constructor only.
+
+ :param selectorText:
+ type string
+ :param style:
+ CSSStyleDeclaration for this CSSStyleRule
+ """
+ super(CSSPageRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@page'
+ self._specificity = (0, 0, 0)
+
+ tempseq = self._tempSeq()
+
+ if selectorText:
+ self.selectorText = selectorText
+ tempseq.append(self.selectorText, 'selectorText')
+ else:
+ self._selectorText = self._tempSeq()
+
+ if style:
+ self.style = style
+ else:
+ self.style = CSSStyleDeclaration()
+
+ tempseq.append(self.style, 'style')
+
+ self._setSeq(tempseq)
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(selectorText=%r, style=%r)" % (
+ self.__class__.__name__,
+ self.selectorText,
+ self.style.cssText)
+
+ def __str__(self):
+ return (u"") % (
+ self.__class__.__name__,
+ self.selectorText,
+ self.specificity,
+ self.style.cssText,
+ len(self.cssRules),
+ id(self))
+
+ def __contains__(self, margin):
+ """Check if margin is set in the rule."""
+ return margin in self.keys()
+
+ def keys(self):
+ "Return list of all set margins (MarginRule)."
+ return list(r.margin for r in self.cssRules)
+
+ def __getitem__(self, margin):
+ """Retrieve the style (of MarginRule)
+ for `margin` (which must be normalized).
+ """
+ for r in self.cssRules:
+ if r.margin == margin:
+ return r.style
+
+ def __setitem__(self, margin, style):
+ """Set the style (of MarginRule)
+ for `margin` (which must be normalized).
+ """
+ for i, r in enumerate(self.cssRules):
+ if r.margin == margin:
+ r.style = style
+ return i
+ else:
+ return self.add(MarginRule(margin, style))
+
+ def __delitem__(self, margin):
+ """Delete the style (the MarginRule)
+ for `margin` (which must be normalized).
+ """
+ for r in self.cssRules:
+ if r.margin == margin:
+ self.deleteRule(r)
+
+ def __parseSelectorText(self, selectorText):
+ """
+ Parse `selectorText` which may also be a list of tokens
+ and returns (selectorText, seq).
+
+ see _setSelectorText for details
+ """
+ # for closures: must be a mutable
+ new = {'wellformed': True, 'last-S': False,
+ 'name': 0, 'first': 0, 'lr': 0}
+ specificity = (0, 0, 0)
+
+ def _char(expected, seq, token, tokenizer=None):
+ # pseudo_page, :left, :right or :first
+ val = self._tokenvalue(token)
+ if not new['last-S'] and expected in ['page', ': or EOF']\
+ and u':' == val:
+ try:
+ identtoken = tokenizer.next()
+ except StopIteration:
+ self._log.error(
+ u'CSSPageRule selectorText: No IDENT found.', token)
+ else:
+ ival, ityp = self._tokenvalue(identtoken),\
+ self._type(identtoken)
+ if self._prods.IDENT != ityp:
+ self._log.error(u'CSSPageRule selectorText: Expected '
+ u'IDENT but found: %r' % ival, token)
+ else:
+ if not ival in (u'first', u'left', u'right'):
+ self._log.warn(u'CSSPageRule: Unknown @page '
+ u'selector: %r'
+ % (u':'+ival,), neverraise=True)
+ if ival == u'first':
+ new['first'] = 1
+ else:
+ new['lr'] = 1
+ seq.append(val + ival, 'pseudo')
+ return 'EOF'
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSPageRule selectorText: Unexpected CHAR: %r'
+ % val, token)
+ return expected
+
+ def S(expected, seq, token, tokenizer=None):
+ "Does not raise if EOF is found."
+ if expected == ': or EOF':
+ # pseudo must directly follow IDENT if given
+ new['last-S'] = True
+ return expected
+
+ def IDENT(expected, seq, token, tokenizer=None):
+ ""
+ val = self._tokenvalue(token)
+ if 'page' == expected:
+ if self._normalize(val) == u'auto':
+ self._log.error(u'CSSPageRule selectorText: Invalid pagename.',
+ token)
+ else:
+ new['name'] = 1
+ seq.append(val, 'IDENT')
+
+ return ': or EOF'
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSPageRule selectorText: Unexpected IDENT: '
+ u'%r' % val, token)
+ return expected
+
+ def COMMENT(expected, seq, token, tokenizer=None):
+ "Does not raise if EOF is found."
+ seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
+ return expected
+
+ newseq = self._tempSeq()
+ wellformed, expected = self._parse(expected='page',
+ seq=newseq, tokenizer=self._tokenize2(selectorText),
+ productions={'CHAR': _char,
+ 'IDENT': IDENT,
+ 'COMMENT': COMMENT,
+ 'S': S},
+ new=new)
+ wellformed = wellformed and new['wellformed']
+
+ # post conditions
+ if expected == 'ident':
+ self._log.error(
+ u'CSSPageRule selectorText: No valid selector: %r' %
+ self._valuestr(selectorText))
+
+ return wellformed, newseq, (new['name'], new['first'], new['lr'])
+
+
+ def __parseMarginAndStyle(self, tokens):
+ "tokens is a list, no generator (yet)"
+ g = iter(tokens)
+ styletokens = []
+
+ # new rules until parse done
+ cssRules = []
+
+ for token in g:
+ if token[0] == 'ATKEYWORD' and \
+ self._normalize(token[1]) in MarginRule.margins:
+
+ # MarginRule
+ m = MarginRule(parentRule=self,
+ parentStyleSheet=self.parentStyleSheet)
+ m.cssText = chain([token], g)
+
+ # merge if margin set more than once
+ for r in cssRules:
+ if r.margin == m.margin:
+ for p in m.style:
+ r.style.setProperty(p, replace=False)
+ break
+ else:
+ cssRules.append(m)
+
+ continue
+
+ # TODO: Properties?
+ styletokens.append(token)
+
+ return cssRules, styletokens
+
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSPageRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSPageRule, self)._setCssText(cssText)
+
+ tokenizer = self._tokenize2(cssText)
+ if self._type(self._nexttoken(tokenizer)) != self._prods.PAGE_SYM:
+ self._log.error(u'CSSPageRule: No CSSPageRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ newStyle = CSSStyleDeclaration(parentRule=self)
+ ok = True
+
+ selectortokens, startbrace = self._tokensupto2(tokenizer,
+ blockstartonly=True,
+ separateEnd=True)
+ styletokens, braceorEOFtoken = self._tokensupto2(tokenizer,
+ blockendonly=True,
+ separateEnd=True)
+ nonetoken = self._nexttoken(tokenizer)
+ if self._tokenvalue(startbrace) != u'{':
+ ok = False
+ self._log.error(u'CSSPageRule: No start { of style declaration '
+ u'found: %r' %
+ self._valuestr(cssText), startbrace)
+ elif nonetoken:
+ ok = False
+ self._log.error(u'CSSPageRule: Trailing content found.',
+ token=nonetoken)
+
+ selok, newselseq, specificity = self.__parseSelectorText(selectortokens)
+ ok = ok and selok
+
+ val, type_ = self._tokenvalue(braceorEOFtoken),\
+ self._type(braceorEOFtoken)
+
+ if val != u'}' and type_ != 'EOF':
+ ok = False
+ self._log.error(
+ u'CSSPageRule: No "}" after style declaration found: %r' %
+ self._valuestr(cssText))
+ else:
+ if 'EOF' == type_:
+ # add again as style needs it
+ styletokens.append(braceorEOFtoken)
+
+ # filter pagemargin rules out first
+ cssRules, styletokens = self.__parseMarginAndStyle(styletokens)
+
+ # SET, may raise:
+ newStyle.cssText = styletokens
+
+ if ok:
+ self._selectorText = newselseq
+ self._specificity = specificity
+ self.style = newStyle
+ self.cssRules = cssutils.css.CSSRuleList()
+ for r in cssRules:
+ self.cssRules.append(r)
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) The parsable textual representation of this rule.")
+
+
+ def _getSelectorText(self):
+ """Wrapper for cssutils Selector object."""
+ return cssutils.ser.do_CSSPageRuleSelector(self._selectorText)
+
+ def _setSelectorText(self, selectorText):
+ """Wrapper for cssutils Selector object.
+
+ :param selectorText:
+ DOM String, in CSS 2.1 one of
+
+ - :first
+ - :left
+ - :right
+ - empty
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ and is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+
+ # may raise SYNTAX_ERR
+ wellformed, newseq, specificity = self.__parseSelectorText(selectorText)
+ if wellformed:
+ self._selectorText = newseq
+ self._specificity = specificity
+
+ selectorText = property(_getSelectorText, _setSelectorText,
+ doc=u"(DOM) The parsable textual representation of "
+ u"the page selector for the rule.")
+
+ def _setStyle(self, style):
+ """
+ :param style:
+ a CSSStyleDeclaration or string
+ """
+ self._checkReadonly()
+ if isinstance(style, basestring):
+ self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
+ else:
+ style._parentRule = self
+ self._style = style
+
+ style = property(lambda self: self._style, _setStyle,
+ doc=u"(DOM) The declaration-block of this rule set, "
+ u"a :class:`~cssutils.css.CSSStyleDeclaration`.")
+
+
+ def insertRule(self, rule, index=None):
+ """Implements base ``insertRule``."""
+ rule, index = self._prepareInsertRule(rule, index)
+
+ if rule is False or rule is True:
+ # done or error
+ return
+
+ # check hierarchy
+ if isinstance(rule, cssutils.css.CSSCharsetRule) or \
+ isinstance(rule, cssutils.css.CSSFontFaceRule) or \
+ isinstance(rule, cssutils.css.CSSImportRule) or \
+ isinstance(rule, cssutils.css.CSSNamespaceRule) or \
+ isinstance(rule, CSSPageRule) or \
+ isinstance(rule, cssutils.css.CSSMediaRule):
+ self._log.error(u'%s: This type of rule is not allowed here: %s'
+ % (self.__class__.__name__, rule.cssText),
+ error=xml.dom.HierarchyRequestErr)
+ return
+
+ return self._finishInsertRule(rule, index)
+
+ specificity = property(lambda self: self._specificity,
+ doc=u"""Specificity of this page rule (READONLY).
+Tuple of (f, g, h) where:
+
+ - if the page selector has a named page, f=1; else f=0
+ - if the page selector has a ':first' pseudo-class, g=1; else g=0
+ - if the page selector has a ':left' or ':right' pseudo-class, h=1; else h=0
+""")
+
+ type = property(lambda self: self.PAGE_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ # constant but needed:
+ wellformed = property(lambda self: True)
\ No newline at end of file
diff --git a/libs/cssutils/css/cssproperties.py b/libs/cssutils/css/cssproperties.py
new file mode 100755
index 00000000..7454daff
--- /dev/null
+++ b/libs/cssutils/css/cssproperties.py
@@ -0,0 +1,122 @@
+"""CSS2Properties (partly!) implements DOM Level 2 CSS CSS2Properties used
+by CSSStyleDeclaration
+
+TODO: CSS2Properties
+ If an implementation does implement this interface, it is expected to
+ understand the specific syntax of the shorthand properties, and apply
+ their semantics; when the margin property is set, for example, the
+ marginTop, marginRight, marginBottom and marginLeft properties are
+ actually being set by the underlying implementation.
+
+ When dealing with CSS "shorthand" properties, the shorthand properties
+ should be decomposed into their component longhand properties as
+ appropriate, and when querying for their value, the form returned
+ should be the shortest form exactly equivalent to the declarations made
+ in the ruleset. However, if there is no shorthand declaration that
+ could be added to the ruleset without changing in any way the rules
+ already declared in the ruleset (i.e., by adding longhand rules that
+ were previously not declared in the ruleset), then the empty string
+ should be returned for the shorthand property.
+
+ For example, querying for the font property should not return
+ "normal normal normal 14pt/normal Arial, sans-serif", when
+ "14pt Arial, sans-serif" suffices. (The normals are initial values, and
+ are implied by use of the longhand property.)
+
+ If the values for all the longhand properties that compose a particular
+ string are the initial values, then a string consisting of all the
+ initial values should be returned (e.g. a border-width value of
+ "medium" should be returned as such, not as "").
+
+ For some shorthand properties that take missing values from other
+ sides, such as the margin, padding, and border-[width|style|color]
+ properties, the minimum number of sides possible should be used; i.e.,
+ "0px 10px" will be returned instead of "0px 10px 0px 10px".
+
+ If the value of a shorthand property can not be decomposed into its
+ component longhand properties, as is the case for the font property
+ with a value of "menu", querying for the values of the component
+ longhand properties should return the empty string.
+
+TODO: CSS2Properties DOMImplementation
+ The interface found within this section are not mandatory. A DOM
+ application can use the hasFeature method of the DOMImplementation
+ interface to determine whether it is supported or not. The feature
+ string for this extended interface listed in this section is "CSS2"
+ and the version is "2.0".
+
+"""
+__all__ = ['CSS2Properties']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssutils.profiles
+import re
+
+class CSS2Properties(object):
+ """The CSS2Properties interface represents a convenience mechanism
+ for retrieving and setting properties within a CSSStyleDeclaration.
+ The attributes of this interface correspond to all the properties
+ specified in CSS2. Getting an attribute of this interface is
+ equivalent to calling the getPropertyValue method of the
+ CSSStyleDeclaration interface. Setting an attribute of this
+ interface is equivalent to calling the setProperty method of the
+ CSSStyleDeclaration interface.
+
+ cssutils actually also allows usage of ``del`` to remove a CSS property
+ from a CSSStyleDeclaration.
+
+ This is an abstract class, the following functions need to be present
+ in inheriting class:
+
+ - ``_getP``
+ - ``_setP``
+ - ``_delP``
+ """
+ # actual properties are set after the class definition!
+ def _getP(self, CSSname): pass
+ def _setP(self, CSSname, value): pass
+ def _delP(self, CSSname): pass
+
+
+_reCSStoDOMname = re.compile('-[a-z]', re.I)
+def _toDOMname(CSSname):
+ """Returns DOMname for given CSSname e.g. for CSSname 'font-style' returns
+ 'fontStyle'.
+ """
+ def _doCSStoDOMname2(m): return m.group(0)[1].capitalize()
+ return _reCSStoDOMname.sub(_doCSStoDOMname2, CSSname)
+
+_reDOMtoCSSname = re.compile('([A-Z])[a-z]+')
+def _toCSSname(DOMname):
+ """Return CSSname for given DOMname e.g. for DOMname 'fontStyle' returns
+ 'font-style'.
+ """
+ def _doDOMtoCSSname2(m): return '-' + m.group(0).lower()
+ return _reDOMtoCSSname.sub(_doDOMtoCSSname2, DOMname)
+
+# add list of DOMname properties to CSS2Properties
+# used for CSSStyleDeclaration to check if allowed properties
+# but somehow doubled, any better way?
+CSS2Properties._properties = []
+for group in cssutils.profiles.properties:
+ for name in cssutils.profiles.properties[group]:
+ CSS2Properties._properties.append(_toDOMname(name))
+
+
+# add CSS2Properties to CSSStyleDeclaration:
+def __named_property_def(DOMname):
+ """
+ Closure to keep name known in each properties accessor function
+ DOMname is converted to CSSname here, so actual calls use CSSname.
+ """
+ CSSname = _toCSSname(DOMname)
+ def _get(self): return self._getP(CSSname)
+ def _set(self, value): self._setP(CSSname, value)
+ def _del(self): self._delP(CSSname)
+ return _get, _set, _del
+
+# add all CSS2Properties to CSSStyleDeclaration
+for DOMname in CSS2Properties._properties:
+ setattr(CSS2Properties, DOMname,
+ property(*__named_property_def(DOMname)))
diff --git a/libs/cssutils/css/cssrule.py b/libs/cssutils/css/cssrule.py
new file mode 100755
index 00000000..1d3ab6fb
--- /dev/null
+++ b/libs/cssutils/css/cssrule.py
@@ -0,0 +1,304 @@
+"""CSSRule implements DOM Level 2 CSS CSSRule."""
+__all__ = ['CSSRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssutils
+import xml.dom
+
+class CSSRule(cssutils.util.Base2):
+ """Abstract base interface for any type of CSS statement. This includes
+ both rule sets and at-rules. An implementation is expected to preserve
+ all rules specified in a CSS style sheet, even if the rule is not
+ recognized by the parser. Unrecognized rules are represented using the
+ :class:`CSSUnknownRule` interface.
+ """
+
+ """
+ CSSRule type constants.
+ An integer indicating which type of rule this is.
+ """
+ UNKNOWN_RULE = 0
+ ":class:`cssutils.css.CSSUnknownRule` (not used in CSSOM anymore)"
+ STYLE_RULE = 1
+ ":class:`cssutils.css.CSSStyleRule`"
+ CHARSET_RULE = 2
+ ":class:`cssutils.css.CSSCharsetRule` (not used in CSSOM anymore)"
+ IMPORT_RULE = 3
+ ":class:`cssutils.css.CSSImportRule`"
+ MEDIA_RULE = 4
+ ":class:`cssutils.css.CSSMediaRule`"
+ FONT_FACE_RULE = 5
+ ":class:`cssutils.css.CSSFontFaceRule`"
+ PAGE_RULE = 6
+ ":class:`cssutils.css.CSSPageRule`"
+ NAMESPACE_RULE = 10
+ """:class:`cssutils.css.CSSNamespaceRule`,
+ Value has changed in 0.9.7a3 due to a change in the CSSOM spec."""
+ COMMENT = 1001 # was -1, cssutils only
+ """:class:`cssutils.css.CSSComment` - not in the offical spec,
+ Value has changed in 0.9.7a3"""
+ VARIABLES_RULE = 1008
+ """:class:`cssutils.css.CSSVariablesRule` - experimental rule
+ not in the offical spec"""
+
+ MARGIN_RULE = 1006
+ """:class:`cssutils.css.MarginRule` - experimental rule
+ not in the offical spec"""
+
+ _typestrings = {UNKNOWN_RULE: u'UNKNOWN_RULE',
+ STYLE_RULE: u'STYLE_RULE',
+ CHARSET_RULE: u'CHARSET_RULE',
+ IMPORT_RULE: u'IMPORT_RULE',
+ MEDIA_RULE: u'MEDIA_RULE',
+ FONT_FACE_RULE: u'FONT_FACE_RULE',
+ PAGE_RULE: u'PAGE_RULE',
+ NAMESPACE_RULE: u'NAMESPACE_RULE',
+ COMMENT: u'COMMENT',
+ VARIABLES_RULE: u'VARIABLES_RULE',
+ MARGIN_RULE: u'MARGIN_RULE'
+ }
+
+ def __init__(self, parentRule=None, parentStyleSheet=None, readonly=False):
+ """Set common attributes for all rules."""
+ super(CSSRule, self).__init__()
+ self._parent = parentRule
+ self._parentRule = parentRule
+ self._parentStyleSheet = parentStyleSheet
+ self._setSeq(self._tempSeq())
+ #self._atkeyword = None
+ # must be set after initialization of #inheriting rule is done
+ self._readonly = False
+
+ def _setAtkeyword(self, keyword):
+ """Check if new keyword fits the rule it is used for."""
+ atkeyword = self._normalize(keyword)
+ if not self.atkeyword or (self.atkeyword == atkeyword):
+ self._atkeyword = atkeyword
+ self._keyword = keyword
+ else:
+ self._log.error(u'%s: Invalid atkeyword for this rule: %r' %
+ (self.atkeyword, keyword),
+ error=xml.dom.InvalidModificationErr)
+
+ atkeyword = property(lambda self: self._atkeyword, _setAtkeyword,
+ doc=u"Normalized keyword of an @rule (e.g. ``@import``).")
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText:
+ A parsable DOMString.
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ self._checkReadonly()
+
+ cssText = property(lambda self: u'', _setCssText,
+ doc=u"(DOM) The parsable textual representation of the "
+ u"rule. This reflects the current state of the rule "
+ u"and not its initial value.")
+
+ parent = property(lambda self: self._parent,
+ doc=u"The Parent Node of this CSSRule or None.")
+
+ parentRule = property(lambda self: self._parentRule,
+ doc=u"If this rule is contained inside another rule "
+ u"(e.g. a style rule inside an @media block), this "
+ u"is the containing rule. If this rule is not nested "
+ u"inside any other rules, this returns None.")
+
+ def _getParentStyleSheet(self):
+ # rules contained in other rules (@media) use that rules parent
+ if (self.parentRule):
+ return self.parentRule._parentStyleSheet
+ else:
+ return self._parentStyleSheet
+
+ parentStyleSheet = property(_getParentStyleSheet,
+ doc=u"The style sheet that contains this rule.")
+
+ type = property(lambda self: self.UNKNOWN_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ typeString = property(lambda self: CSSRule._typestrings[self.type],
+ doc=u"Descriptive name of this rule's type.")
+
+ wellformed = property(lambda self: False,
+ doc=u"If the rule is wellformed.")
+
+
+
+class CSSRuleRules(CSSRule):
+ """Abstract base interface for rules that contain other rules
+ like @media or @page. Methods may be overwritten if a rule
+ has specific stuff to do like checking the order of insertion like
+ @media does.
+ """
+
+ def __init__(self, parentRule=None, parentStyleSheet=None):
+
+ super(CSSRuleRules, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+
+ self.cssRules = cssutils.css.CSSRuleList()
+
+ def __iter__(self):
+ """Generator iterating over these rule's cssRules."""
+ for rule in self._cssRules:
+ yield rule
+
+ def _setCssRules(self, cssRules):
+ "Set new cssRules and update contained rules refs."
+ cssRules.append = self.insertRule
+ cssRules.extend = self.insertRule
+ cssRules.__delitem__ == self.deleteRule
+
+ for rule in cssRules:
+ rule._parentRule = self
+ rule._parentStyleSheet = None
+
+ self._cssRules = cssRules
+
+ cssRules = property(lambda self: self._cssRules, _setCssRules,
+ "All Rules in this style sheet, a "
+ ":class:`~cssutils.css.CSSRuleList`.")
+
+ def deleteRule(self, index):
+ """
+ Delete the rule at `index` from rules ``cssRules``.
+
+ :param index:
+ The `index` of the rule to be removed from the rules cssRules
+ list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is
+ raised but rules for normal Python lists are used. E.g.
+ ``deleteRule(-1)`` removes the last rule in cssRules.
+
+ `index` may also be a CSSRule object which will then be removed.
+
+ :Exceptions:
+ - :exc:`~xml.dom.IndexSizeErr`:
+ Raised if the specified index does not correspond to a rule in
+ the media rule list.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this media rule is readonly.
+ """
+ self._checkReadonly()
+
+ if isinstance(index, CSSRule):
+ for i, r in enumerate(self.cssRules):
+ if index == r:
+ index = i
+ break
+ else:
+ raise xml.dom.IndexSizeErr(u"%s: Not a rule in "
+ u"this rule'a cssRules list: %s"
+ % (self.__class__.__name__, index))
+
+ try:
+ # detach
+ self._cssRules[index]._parentRule = None
+ del self._cssRules[index]
+
+ except IndexError:
+ raise xml.dom.IndexSizeErr(u'%s: %s is not a valid index '
+ u'in the rulelist of length %i'
+ % (self.__class__.__name__,
+ index, self._cssRules.length))
+
+ def _prepareInsertRule(self, rule, index=None):
+ "return checked `index` and optional parsed `rule`"
+ self._checkReadonly()
+
+ # check index
+ if index is None:
+ index = len(self._cssRules)
+
+ elif index < 0 or index > self._cssRules.length:
+ raise xml.dom.IndexSizeErr(u'%s: Invalid index %s for '
+ u'CSSRuleList with a length of %s.'
+ % (self.__class__.__name__,
+ index, self._cssRules.length))
+
+ # check and optionally parse rule
+ if isinstance(rule, basestring):
+ tempsheet = cssutils.css.CSSStyleSheet()
+ tempsheet.cssText = rule
+ if len(tempsheet.cssRules) != 1 or (tempsheet.cssRules and
+ not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)):
+ self._log.error(u'%s: Invalid Rule: %s' % (self.__class__.__name__,
+ rule))
+ return False, False
+ rule = tempsheet.cssRules[0]
+
+ elif isinstance(rule, cssutils.css.CSSRuleList):
+ # insert all rules
+ for i, r in enumerate(rule):
+ self.insertRule(r, index + i)
+ return True, True
+
+ elif not isinstance(rule, cssutils.css.CSSRule):
+ self._log.error(u'%s: Not a CSSRule: %s' % (rule,
+ self.__class__.__name__))
+ return False, False
+
+ return rule, index
+
+ def _finishInsertRule(self, rule, index):
+ "add `rule` at `index`"
+ rule._parentRule = self
+ rule._parentStyleSheet = None
+ self._cssRules.insert(index, rule)
+ return index
+
+ def add(self, rule):
+ """Add `rule` to page rule. Same as ``insertRule(rule)``."""
+ return self.insertRule(rule)
+
+ def insertRule(self, rule, index=None):
+ """
+ Insert `rule` into the rules ``cssRules``.
+
+ :param rule:
+ the parsable text representing the `rule` to be inserted. For rule
+ sets this contains both the selector and the style declaration.
+ For at-rules, this specifies both the at-identifier and the rule
+ content.
+
+ cssutils also allows rule to be a valid
+ :class:`~cssutils.css.CSSRule` object.
+
+ :param index:
+ before the `index` the specified `rule` will be inserted.
+ If the specified `index` is equal to the length of the rules
+ rule collection, the rule will be added to the end of the rule.
+ If index is not given or None rule will be appended to rule
+ list.
+
+ :returns:
+ the index of the newly inserted rule.
+
+ :exceptions:
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the `rule` cannot be inserted at the specified `index`,
+ e.g., if an @import rule is inserted after a standard rule set
+ or other at-rule.
+ - :exc:`~xml.dom.IndexSizeErr`:
+ Raised if the specified `index` is not a valid insertion point.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified `rule` has a syntax error and is
+ unparsable.
+ """
+ return self._prepareInsertRule(rule, index)
diff --git a/libs/cssutils/css/cssrulelist.py b/libs/cssutils/css/cssrulelist.py
new file mode 100755
index 00000000..f9daff49
--- /dev/null
+++ b/libs/cssutils/css/cssrulelist.py
@@ -0,0 +1,53 @@
+"""CSSRuleList implements DOM Level 2 CSS CSSRuleList.
+Partly also http://dev.w3.org/csswg/cssom/#the-cssrulelist."""
+__all__ = ['CSSRuleList']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+class CSSRuleList(list):
+ """The CSSRuleList object represents an (ordered) list of statements.
+
+ The items in the CSSRuleList are accessible via an integral index,
+ starting from 0.
+
+ Subclasses a standard Python list so theoretically all standard list
+ methods are available. Setting methods like ``__init__``, ``append``,
+ ``extend`` or ``__setslice__`` are added later on instances of this
+ class if so desired.
+ E.g. CSSStyleSheet adds ``append`` which is not available in a simple
+ instance of this class!
+ """
+ def __init__(self, *ignored):
+ "Nothing is set as this must also be defined later."
+ pass
+
+ def __notimplemented(self, *ignored):
+ "Implemented in class using a CSSRuleList only."
+ raise NotImplementedError(
+ 'Must be implemented by class using an instance of this class.')
+
+ append = extend = __setitem__ = __setslice__ = __notimplemented
+
+ def item(self, index):
+ """(DOM) Retrieve a CSS rule by ordinal `index`. The order in this
+ collection represents the order of the rules in the CSS style
+ sheet. If index is greater than or equal to the number of rules in
+ the list, this returns None.
+
+ Returns CSSRule, the style rule at the index position in the
+ CSSRuleList, or None if that is not a valid index.
+ """
+ try:
+ return self[index]
+ except IndexError:
+ return None
+
+ length = property(lambda self: len(self),
+ doc=u"(DOM) The number of CSSRules in the list.")
+
+ def rulesOfType(self, type):
+ """Yield the rules which have the given `type` only, one of the
+ constants defined in :class:`cssutils.css.CSSRule`."""
+ for r in self:
+ if r.type == type:
+ yield r
diff --git a/libs/cssutils/css/cssstyledeclaration.py b/libs/cssutils/css/cssstyledeclaration.py
new file mode 100755
index 00000000..7573487a
--- /dev/null
+++ b/libs/cssutils/css/cssstyledeclaration.py
@@ -0,0 +1,697 @@
+"""CSSStyleDeclaration implements DOM Level 2 CSS CSSStyleDeclaration and
+extends CSS2Properties
+
+see
+ http://www.w3.org/TR/1998/REC-CSS2-19980512/syndata.html#parsing-errors
+
+Unknown properties
+------------------
+User agents must ignore a declaration with an unknown property.
+For example, if the style sheet is::
+
+ H1 { color: red; rotation: 70minutes }
+
+the user agent will treat this as if the style sheet had been::
+
+ H1 { color: red }
+
+Cssutils gives a message about any unknown properties but
+keeps any property (if syntactically correct).
+
+Illegal values
+--------------
+User agents must ignore a declaration with an illegal value. For example::
+
+ IMG { float: left } /* correct CSS2 */
+ IMG { float: left here } /* "here" is not a value of 'float' */
+ IMG { background: "red" } /* keywords cannot be quoted in CSS2 */
+ IMG { border-width: 3 } /* a unit must be specified for length values */
+
+A CSS2 parser would honor the first rule and ignore the rest, as if the
+style sheet had been::
+
+ IMG { float: left }
+ IMG { }
+ IMG { }
+ IMG { }
+
+Cssutils again will issue a message (WARNING in this case) about invalid
+CSS2 property values.
+
+TODO:
+ This interface is also used to provide a read-only access to the
+ computed values of an element. See also the ViewCSS interface.
+
+ - return computed values and not literal values
+ - simplify unit pairs/triples/quadruples
+ 2px 2px 2px 2px -> 2px for border/padding...
+ - normalize compound properties like:
+ background: no-repeat left url() #fff
+ -> background: #fff url() no-repeat left
+"""
+__all__ = ['CSSStyleDeclaration', 'Property']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssproperties import CSS2Properties
+from property import Property
+import cssutils
+import xml.dom
+
+class CSSStyleDeclaration(CSS2Properties, cssutils.util.Base2):
+ """The CSSStyleDeclaration class represents a single CSS declaration
+ block. This class may be used to determine the style properties
+ currently set in a block or to set style properties explicitly
+ within the block.
+
+ While an implementation may not recognize all CSS properties within
+ a CSS declaration block, it is expected to provide access to all
+ specified properties in the style sheet through the
+ CSSStyleDeclaration interface.
+ Furthermore, implementations that support a specific level of CSS
+ should correctly handle CSS shorthand properties for that level. For
+ a further discussion of shorthand properties, see the CSS2Properties
+ interface.
+
+ Additionally the CSS2Properties interface is implemented.
+
+ $css2propertyname
+ All properties defined in the CSS2Properties class are available
+ as direct properties of CSSStyleDeclaration with their respective
+ DOM name, so e.g. ``fontStyle`` for property 'font-style'.
+
+ These may be used as::
+
+ >>> style = CSSStyleDeclaration(cssText='color: red')
+ >>> style.color = 'green'
+ >>> print style.color
+ green
+ >>> del style.color
+ >>> print style.color
+
+
+ Format::
+
+ [Property: Value Priority?;]* [Property: Value Priority?]?
+ """
+ def __init__(self, cssText=u'', parentRule=None, readonly=False,
+ validating=None):
+ """
+ :param cssText:
+ Shortcut, sets CSSStyleDeclaration.cssText
+ :param parentRule:
+ The CSS rule that contains this declaration block or
+ None if this CSSStyleDeclaration is not attached to a CSSRule.
+ :param readonly:
+ defaults to False
+ :param validating:
+ a flag defining if this sheet should be validated on change.
+ Defaults to None, which means defer to the parent stylesheet.
+ """
+ super(CSSStyleDeclaration, self).__init__()
+ self._parentRule = parentRule
+ self.validating = validating
+ self.cssText = cssText
+ self._readonly = readonly
+
+ def __contains__(self, nameOrProperty):
+ """Check if a property (or a property with given name) is in style.
+
+ :param name:
+ a string or Property, uses normalized name and not literalname
+ """
+ if isinstance(nameOrProperty, Property):
+ name = nameOrProperty.name
+ else:
+ name = self._normalize(nameOrProperty)
+ return name in self.__nnames()
+
+ def __iter__(self):
+ """Iterator of set Property objects with different normalized names."""
+ def properties():
+ for name in self.__nnames():
+ yield self.getProperty(name)
+ return properties()
+
+ def keys(self):
+ """Analoguous to standard dict returns property names which are set in
+ this declaration."""
+ return list(self.__nnames())
+
+ def __getitem__(self, CSSName):
+ """Retrieve the value of property ``CSSName`` from this declaration.
+
+ ``CSSName`` will be always normalized.
+ """
+ return self.getPropertyValue(CSSName)
+
+ def __setitem__(self, CSSName, value):
+ """Set value of property ``CSSName``. ``value`` may also be a tuple of
+ (value, priority), e.g. style['color'] = ('red', 'important')
+
+ ``CSSName`` will be always normalized.
+ """
+ priority = None
+ if isinstance(value, tuple):
+ value, priority = value
+
+ return self.setProperty(CSSName, value, priority)
+
+ def __delitem__(self, CSSName):
+ """Delete property ``CSSName`` from this declaration.
+ If property is not in this declaration return u'' just like
+ removeProperty.
+
+ ``CSSName`` will be always normalized.
+ """
+ return self.removeProperty(CSSName)
+
+ def __setattr__(self, n, v):
+ """Prevent setting of unknown properties on CSSStyleDeclaration
+ which would not work anyway. For these
+ ``CSSStyleDeclaration.setProperty`` MUST be called explicitly!
+
+ TODO:
+ implementation of known is not really nice, any alternative?
+ """
+ known = ['_tokenizer', '_log', '_ttypes',
+ '_seq', 'seq', 'parentRule', '_parentRule', 'cssText',
+ 'valid', 'wellformed', 'validating',
+ '_readonly', '_profiles', '_validating']
+ known.extend(CSS2Properties._properties)
+ if n in known:
+ super(CSSStyleDeclaration, self).__setattr__(n, v)
+ else:
+ raise AttributeError(u'Unknown CSS Property, '
+ u'``CSSStyleDeclaration.setProperty("%s", '
+ u'...)`` MUST be used.' % n)
+
+ def __repr__(self):
+ return u"cssutils.css.%s(cssText=%r)" % (
+ self.__class__.__name__,
+ self.getCssText(separator=u' '))
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.length,
+ len(self.getProperties(all=True)),
+ id(self))
+
+ def __nnames(self):
+ """Return iterator for all different names in order as set
+ if names are set twice the last one is used (double reverse!)
+ """
+ names = []
+ for item in reversed(self.seq):
+ val = item.value
+ if isinstance(val, Property) and not val.name in names:
+ names.append(val.name)
+ return reversed(names)
+
+ # overwritten accessor functions for CSS2Properties' properties
+ def _getP(self, CSSName):
+ """(DOM CSS2Properties) Overwritten here and effectively the same as
+ ``self.getPropertyValue(CSSname)``.
+
+ Parameter is in CSSname format ('font-style'), see CSS2Properties.
+
+ Example::
+
+ >>> style = CSSStyleDeclaration(cssText='font-style:italic;')
+ >>> print style.fontStyle
+ italic
+ """
+ return self.getPropertyValue(CSSName)
+
+ def _setP(self, CSSName, value):
+ """(DOM CSS2Properties) Overwritten here and effectively the same as
+ ``self.setProperty(CSSname, value)``.
+
+ Only known CSS2Properties may be set this way, otherwise an
+ AttributeError is raised.
+ For these unknown properties ``setPropertyValue(CSSname, value)``
+ has to be called explicitly.
+ Also setting the priority of properties needs to be done with a
+ call like ``setPropertyValue(CSSname, value, priority)``.
+
+ Example::
+
+ >>> style = CSSStyleDeclaration()
+ >>> style.fontStyle = 'italic'
+ >>> # or
+ >>> style.setProperty('font-style', 'italic', '!important')
+
+ """
+ self.setProperty(CSSName, value)
+ # TODO: Shorthand ones
+
+ def _delP(self, CSSName):
+ """(cssutils only) Overwritten here and effectively the same as
+ ``self.removeProperty(CSSname)``.
+
+ Example::
+
+ >>> style = CSSStyleDeclaration(cssText='font-style:italic;')
+ >>> del style.fontStyle
+ >>> print style.fontStyle
+
+
+ """
+ self.removeProperty(CSSName)
+
+ def children(self):
+ """Generator yielding any known child in this declaration including
+ *all* properties, comments or CSSUnknownrules.
+ """
+ for item in self._seq:
+ yield item.value
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_css_CSSStyleDeclaration(self)
+
+ def _setCssText(self, cssText):
+ """Setting this attribute will result in the parsing of the new value
+ and resetting of all the properties in the declaration block
+ including the removal or addition of properties.
+
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly or a property is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ """
+ self._checkReadonly()
+ tokenizer = self._tokenize2(cssText)
+
+ # for closures: must be a mutable
+ new = {'wellformed': True}
+ def ident(expected, seq, token, tokenizer=None):
+ # a property
+
+ tokens = self._tokensupto2(tokenizer, starttoken=token,
+ semicolon=True)
+ if self._tokenvalue(tokens[-1]) == u';':
+ tokens.pop()
+ property = Property(parent=self)
+ property.cssText = tokens
+ if property.wellformed:
+ seq.append(property, 'Property')
+ else:
+ self._log.error(u'CSSStyleDeclaration: Syntax Error in '
+ u'Property: %s' % self._valuestr(tokens))
+ # does not matter in this case
+ return expected
+
+ def unexpected(expected, seq, token, tokenizer=None):
+ # error, find next ; or } to omit upto next property
+ ignored = self._tokenvalue(token) + self._valuestr(
+ self._tokensupto2(tokenizer,
+ propertyvalueendonly=True))
+ self._log.error(u'CSSStyleDeclaration: Unexpected token, ignoring '
+ 'upto %r.' % ignored,token)
+ # does not matter in this case
+ return expected
+
+ def char(expected, seq, token, tokenizer=None):
+ # a standalone ; or error...
+ if self._tokenvalue(token) == u';':
+ self._log.info(u'CSSStyleDeclaration: Stripped standalone semicolon'
+ u': %s' % self._valuestr([token]), neverraise=True)
+ return expected
+ else:
+ return unexpected(expected, seq, token, tokenizer)
+
+ # [Property: Value;]* Property: Value?
+ newseq = self._tempSeq()
+ wellformed, expected = self._parse(expected=None,
+ seq=newseq, tokenizer=tokenizer,
+ productions={'IDENT': ident, 'CHAR': char},
+ default=unexpected)
+ # wellformed set by parse
+
+ for item in newseq:
+ item.value._parent = self
+
+ # do not check wellformed as invalid things are removed anyway
+ self._setSeq(newseq)
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) A parsable textual representation of the "
+ u"declaration block excluding the surrounding curly "
+ u"braces.")
+
+ def getCssText(self, separator=None):
+ """
+ :returns:
+ serialized property cssText, each property separated by
+ given `separator` which may e.g. be ``u''`` to be able to use
+ cssText directly in an HTML style attribute. ``;`` is part of
+ each property (except the last one) and **cannot** be set with
+ separator!
+ """
+ return cssutils.ser.do_css_CSSStyleDeclaration(self, separator)
+
+ def _setParentRule(self, parentRule):
+ self._parentRule = parentRule
+# for x in self.children():
+# x.parent = self
+
+ parentRule = property(lambda self: self._parentRule, _setParentRule,
+ doc="(DOM) The CSS rule that contains this declaration block or "
+ "None if this CSSStyleDeclaration is not attached to a CSSRule.")
+
+ def getProperties(self, name=None, all=False):
+ """
+ :param name:
+ optional `name` of properties which are requested.
+ Only properties with this **always normalized** `name` are returned.
+ If `name` is ``None`` all properties are returned (at least one for
+ each set name depending on parameter `all`).
+ :param all:
+ if ``False`` (DEFAULT) only the effective properties are returned.
+ If name is given a list with only one property is returned.
+
+ if ``True`` all properties including properties set multiple times
+ with different values or priorities for different UAs are returned.
+ The order of the properties is fully kept as in the original
+ stylesheet.
+ :returns:
+ a list of :class:`~cssutils.css.Property` objects set in
+ this declaration.
+ """
+ if name and not all:
+ # single prop but list
+ p = self.getProperty(name)
+ if p:
+ return [p]
+ else:
+ return []
+ elif not all:
+ # effective Properties in name order
+ return [self.getProperty(name) for name in self.__nnames()]
+ else:
+ # all properties or all with this name
+ nname = self._normalize(name)
+ properties = []
+ for item in self.seq:
+ val = item.value
+ if isinstance(val, Property) and (
+ (bool(nname) == False) or (val.name == nname)):
+ properties.append(val)
+ return properties
+
+ def getProperty(self, name, normalize=True):
+ """
+ :param name:
+ of the CSS property, always lowercase (even if not normalized)
+ :param normalize:
+ if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent
+
+ If ``False`` may return **NOT** the effective value but the
+ effective for the unnormalized name.
+ :returns:
+ the effective :class:`~cssutils.css.Property` object.
+ """
+ nname = self._normalize(name)
+ found = None
+ for item in reversed(self.seq):
+ val = item.value
+ if isinstance(val, Property):
+ if (normalize and nname == val.name) or name == val.literalname:
+ if val.priority:
+ return val
+ elif not found:
+ found = val
+ return found
+
+ def getPropertyCSSValue(self, name, normalize=True):
+ """
+ :param name:
+ of the CSS property, always lowercase (even if not normalized)
+ :param normalize:
+ if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent
+
+ If ``False`` may return **NOT** the effective value but the
+ effective for the unnormalized name.
+ :returns:
+ :class:`~cssutils.css.CSSValue`, the value of the effective
+ property if it has been explicitly set for this declaration block.
+
+ (DOM)
+ Used to retrieve the object representation of the value of a CSS
+ property if it has been explicitly set within this declaration
+ block. Returns None if the property has not been set.
+
+ (This method returns None if the property is a shorthand
+ property. Shorthand property values can only be accessed and
+ modified as strings, using the getPropertyValue and setProperty
+ methods.)
+
+ **cssutils currently always returns a CSSValue if the property is
+ set.**
+
+ for more on shorthand properties see
+ http://www.dustindiaz.com/css-shorthand/
+ """
+ nname = self._normalize(name)
+ if nname in self._SHORTHANDPROPERTIES:
+ self._log.info(u'CSSValue for shorthand property "%s" should be '
+ u'None, this may be implemented later.' %
+ nname, neverraise=True)
+
+ p = self.getProperty(name, normalize)
+ if p:
+ return p.cssValue
+ else:
+ return None
+
+ def getPropertyValue(self, name, normalize=True):
+ """
+ :param name:
+ of the CSS property, always lowercase (even if not normalized)
+ :param normalize:
+ if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent
+
+ If ``False`` may return **NOT** the effective value but the
+ effective for the unnormalized name.
+ :returns:
+ the value of the effective property if it has been explicitly set
+ for this declaration block. Returns the empty string if the
+ property has not been set.
+ """
+ p = self.getProperty(name, normalize)
+ if p:
+ return p.value
+ else:
+ return u''
+
+ def getPropertyPriority(self, name, normalize=True):
+ """
+ :param name:
+ of the CSS property, always lowercase (even if not normalized)
+ :param normalize:
+ if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent
+
+ If ``False`` may return **NOT** the effective value but the
+ effective for the unnormalized name.
+ :returns:
+ the priority of the effective CSS property (e.g. the
+ "important" qualifier) if the property has been explicitly set in
+ this declaration block. The empty string if none exists.
+ """
+ p = self.getProperty(name, normalize)
+ if p:
+ return p.priority
+ else:
+ return u''
+
+ def removeProperty(self, name, normalize=True):
+ """
+ (DOM)
+ Used to remove a CSS property if it has been explicitly set within
+ this declaration block.
+
+ :param name:
+ of the CSS property
+ :param normalize:
+ if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent.
+ The effective Property value is returned and *all* Properties
+ with ``Property.name == name`` are removed.
+
+ If ``False`` may return **NOT** the effective value but the
+ effective for the unnormalized `name` only. Also only the
+ Properties with the literal name `name` are removed.
+ :returns:
+ the value of the property if it has been explicitly set for
+ this declaration block. Returns the empty string if the property
+ has not been set or the property name does not correspond to a
+ known CSS property
+
+
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly or the property is
+ readonly.
+ """
+ self._checkReadonly()
+ r = self.getPropertyValue(name, normalize=normalize)
+ newseq = self._tempSeq()
+ if normalize:
+ # remove all properties with name == nname
+ nname = self._normalize(name)
+ for item in self.seq:
+ if not (isinstance(item.value, Property)
+ and item.value.name == nname):
+ newseq.appendItem(item)
+ else:
+ # remove all properties with literalname == name
+ for item in self.seq:
+ if not (isinstance(item.value, Property)
+ and item.value.literalname == name):
+ newseq.appendItem(item)
+ self._setSeq(newseq)
+ return r
+
+ def setProperty(self, name, value=None, priority=u'',
+ normalize=True, replace=True):
+ """(DOM) Set a property value and priority within this declaration
+ block.
+
+ :param name:
+ of the CSS property to set (in W3C DOM the parameter is called
+ "propertyName"), always lowercase (even if not normalized)
+
+ If a property with this `name` is present it will be reset.
+
+ cssutils also allowed `name` to be a
+ :class:`~cssutils.css.Property` object, all other
+ parameter are ignored in this case
+
+ :param value:
+ the new value of the property, ignored if `name` is a Property.
+ :param priority:
+ the optional priority of the property (e.g. "important"),
+ ignored if `name` is a Property.
+ :param normalize:
+ if True (DEFAULT) `name` will be normalized (lowercase, no simple
+ escapes) so "color", "COLOR" or "C\olor" will all be equivalent
+ :param replace:
+ if True (DEFAULT) the given property will replace a present
+ property. If False a new property will be added always.
+ The difference to `normalize` is that two or more properties with
+ the same name may be set, useful for e.g. stuff like::
+
+ background: red;
+ background: rgba(255, 0, 0, 0.5);
+
+ which defines the same property but only capable UAs use the last
+ property value, older ones use the first value.
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified value has a syntax error and is
+ unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly or the property is
+ readonly.
+ """
+ self._checkReadonly()
+
+ if isinstance(name, Property):
+ newp = name
+ name = newp.literalname
+ elif not value:
+ # empty string or None effectively removed property
+ return self.removeProperty(name)
+ else:
+ newp = Property(name, value, priority)
+
+ if newp.wellformed:
+ if replace:
+ # check if update
+ nname = self._normalize(name)
+ properties = self.getProperties(name, all=(not normalize))
+ for property in reversed(properties):
+ if normalize and property.name == nname:
+ property.cssValue = newp.cssValue.cssText
+ property.priority = newp.priority
+ return
+ elif property.literalname == name:
+ property.cssValue = newp.cssValue.cssText
+ property.priority = newp.priority
+ return
+
+ # not yet set or forced omit replace
+ newp.parent = self
+ self.seq._readonly = False
+ self.seq.append(newp, 'Property')
+ self.seq._readonly = True
+
+ else:
+ self._log.warn(u'Invalid Property: %s: %s %s'
+ % (name, value, priority))
+
+ def item(self, index):
+ """(DOM) Retrieve the properties that have been explicitly set in
+ this declaration block. The order of the properties retrieved using
+ this method does not have to be the order in which they were set.
+ This method can be used to iterate over all properties in this
+ declaration block.
+
+ :param index:
+ of the property to retrieve, negative values behave like
+ negative indexes on Python lists, so -1 is the last element
+
+ :returns:
+ the name of the property at this ordinal position. The
+ empty string if no property exists at this position.
+
+ **ATTENTION:**
+ Only properties with different names are counted. If two
+ properties with the same name are present in this declaration
+ only the effective one is included.
+
+ :meth:`item` and :attr:`length` work on the same set here.
+ """
+ names = list(self.__nnames())
+ try:
+ return names[index]
+ except IndexError:
+ return u''
+
+ length = property(lambda self: len(list(self.__nnames())),
+ doc=u"(DOM) The number of distinct properties that have "
+ u"been explicitly in this declaration block. The "
+ u"range of valid indices is 0 to length-1 inclusive. "
+ u"These are properties with a different ``name`` "
+ u"only. :meth:`item` and :attr:`length` work on the "
+ u"same set here.")
+
+ def _getValidating(self):
+ try:
+ # CSSParser.parseX() sets validating of stylesheet
+ return self.parentRule.parentStyleSheet.validating
+ except AttributeError:
+ # CSSParser.parseStyle() sets validating of declaration
+ if self._validating is not None:
+ return self._validating
+ # default
+ return True
+
+ def _setValidating(self, validating):
+ self._validating = validating
+
+ validating = property(_getValidating, _setValidating,
+ doc=u"If ``True`` this declaration validates "
+ u"contained properties. The parent StyleSheet "
+ u"validation setting does *always* win though so "
+ u"even if validating is True it may not validate "
+ u"if the StyleSheet defines else!")
diff --git a/libs/cssutils/css/cssstylerule.py b/libs/cssutils/css/cssstylerule.py
new file mode 100755
index 00000000..db85b8b5
--- /dev/null
+++ b/libs/cssutils/css/cssstylerule.py
@@ -0,0 +1,234 @@
+"""CSSStyleRule implements DOM Level 2 CSS CSSStyleRule."""
+__all__ = ['CSSStyleRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssstyledeclaration import CSSStyleDeclaration
+from selectorlist import SelectorList
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSStyleRule(cssrule.CSSRule):
+ """The CSSStyleRule object represents a ruleset specified (if any) in a CSS
+ style sheet. It provides access to a declaration block as well as to the
+ associated group of selectors.
+
+ Format::
+
+ : selector [ COMMA S* selector ]*
+ LBRACE S* declaration [ ';' S* declaration ]* '}' S*
+ ;
+ """
+ def __init__(self, selectorText=None, style=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ :Parameters:
+ selectorText
+ string parsed into selectorList
+ style
+ string parsed into CSSStyleDeclaration for this CSSStyleRule
+ readonly
+ if True allows setting of properties in constructor only
+ """
+ super(CSSStyleRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+
+ self.selectorList = SelectorList()
+ if selectorText:
+ self.selectorText = selectorText
+
+ if style:
+ self.style = style
+ else:
+ self.style = CSSStyleDeclaration()
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ if self._namespaces:
+ st = (self.selectorText, self._namespaces)
+ else:
+ st = self.selectorText
+ return u"cssutils.css.%s(selectorText=%r, style=%r)" % (
+ self.__class__.__name__, st, self.style.cssText)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.selectorText,
+ self.style.cssText,
+ self._namespaces,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSStyleRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :param cssText:
+ a parseable string or a tuple of (cssText, dict-of-namespaces)
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if the specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSStyleRule, self)._setCssText(cssText)
+
+ # might be (cssText, namespaces)
+ cssText, namespaces = self._splitNamespacesOff(cssText)
+ try:
+ # use parent style sheet ones if available
+ namespaces = self.parentStyleSheet.namespaces
+ except AttributeError:
+ pass
+
+ tokenizer = self._tokenize2(cssText)
+ selectortokens = self._tokensupto2(tokenizer, blockstartonly=True)
+ styletokens = self._tokensupto2(tokenizer, blockendonly=True)
+ trail = self._nexttoken(tokenizer)
+ if trail:
+ self._log.error(u'CSSStyleRule: Trailing content: %s' %
+ self._valuestr(cssText), token=trail)
+ elif not selectortokens:
+ self._log.error(u'CSSStyleRule: No selector found: %r' %
+ self._valuestr(cssText))
+ elif self._tokenvalue(selectortokens[0]).startswith(u'@'):
+ self._log.error(u'CSSStyleRule: No style rule: %r' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ newSelectorList = SelectorList(parentRule=self)
+ newStyle = CSSStyleDeclaration(parentRule=self)
+ ok = True
+
+ bracetoken = selectortokens.pop()
+ if self._tokenvalue(bracetoken) != u'{':
+ ok = False
+ self._log.error(
+ u'CSSStyleRule: No start { of style declaration found: %r' %
+ self._valuestr(cssText), bracetoken)
+ elif not selectortokens:
+ ok = False
+ self._log.error(u'CSSStyleRule: No selector found: %r.' %
+ self._valuestr(cssText), bracetoken)
+ # SET
+ newSelectorList.selectorText = (selectortokens,
+ namespaces)
+
+ if not styletokens:
+ ok = False
+ self._log.error(
+ u'CSSStyleRule: No style declaration or "}" found: %r' %
+ self._valuestr(cssText))
+ else:
+ braceorEOFtoken = styletokens.pop()
+ val, typ = self._tokenvalue(braceorEOFtoken),\
+ self._type(braceorEOFtoken)
+ if val != u'}' and typ != 'EOF':
+ ok = False
+ self._log.error(u'CSSStyleRule: No "}" after style '
+ u'declaration found: %r'
+ % self._valuestr(cssText))
+ else:
+ if 'EOF' == typ:
+ # add again as style needs it
+ styletokens.append(braceorEOFtoken)
+ # SET, may raise:
+ newStyle.cssText = styletokens
+
+ if ok:
+ self.selectorList = newSelectorList
+ self.style = newStyle
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) The parsable textual representation of this "
+ u"rule.")
+
+ def __getNamespaces(self):
+ """Uses children namespaces if not attached to a sheet, else the sheet's
+ ones."""
+ try:
+ return self.parentStyleSheet.namespaces
+ except AttributeError:
+ return self.selectorList._namespaces
+
+ _namespaces = property(__getNamespaces,
+ doc=u"If this Rule is attached to a CSSStyleSheet "
+ u"the namespaces of that sheet are mirrored "
+ u"here. While the Rule is not attached the "
+ u"namespaces of selectorList are used.""")
+
+ def _setSelectorList(self, selectorList):
+ """
+ :param selectorList: A SelectorList which replaces the current
+ selectorList object
+ """
+ self._checkReadonly()
+ selectorList._parentRule = self
+ self._selectorList = selectorList
+
+ _selectorList = None
+ selectorList = property(lambda self: self._selectorList, _setSelectorList,
+ doc=u"The SelectorList of this rule.")
+
+ def _setSelectorText(self, selectorText):
+ """
+ wrapper for cssutils SelectorList object
+
+ :param selectorText:
+ of type string, might also be a comma separated list
+ of selectors
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if the specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ and is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+
+ sl = SelectorList(selectorText=selectorText, parentRule=self)
+ if sl.wellformed:
+ self._selectorList = sl
+
+ selectorText = property(lambda self: self._selectorList.selectorText,
+ _setSelectorText,
+ doc=u"(DOM) The textual representation of the "
+ u"selector for the rule set.")
+
+ def _setStyle(self, style):
+ """
+ :param style: A string or CSSStyleDeclaration which replaces the
+ current style object.
+ """
+ self._checkReadonly()
+ if isinstance(style, basestring):
+ self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
+ else:
+ style._parentRule = self
+ self._style = style
+
+ style = property(lambda self: self._style, _setStyle,
+ doc=u"(DOM) The declaration-block of this rule set.")
+
+ type = property(lambda self: self.STYLE_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ "type constant.")
+
+ wellformed = property(lambda self: self.selectorList.wellformed)
diff --git a/libs/cssutils/css/cssstylesheet.py b/libs/cssutils/css/cssstylesheet.py
new file mode 100755
index 00000000..88c46b8c
--- /dev/null
+++ b/libs/cssutils/css/cssstylesheet.py
@@ -0,0 +1,804 @@
+"""CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet.
+
+Partly also:
+ - http://dev.w3.org/csswg/cssom/#the-cssstylesheet
+ - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/
+
+TODO:
+ - ownerRule and ownerNode
+"""
+__all__ = ['CSSStyleSheet']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.helper import Deprecated
+from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl
+from cssrule import CSSRule
+from cssvariablesdeclaration import CSSVariablesDeclaration
+import cssutils.stylesheets
+import xml.dom
+
+class CSSStyleSheet(cssutils.stylesheets.StyleSheet):
+ """CSSStyleSheet represents a CSS style sheet.
+
+ Format::
+
+ stylesheet
+ : [ CHARSET_SYM S* STRING S* ';' ]?
+ [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
+ [ namespace [S|CDO|CDC]* ]* # according to @namespace WD
+ [ [ ruleset | media | page ] [S|CDO|CDC]* ]*
+
+ ``cssRules``
+ All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`.
+ """
+ def __init__(self, href=None, media=None, title=u'', disabled=None,
+ ownerNode=None, parentStyleSheet=None, readonly=False,
+ ownerRule=None,
+ validating=True):
+ """
+ For parameters see :class:`~cssutils.stylesheets.StyleSheet`
+ """
+ super(CSSStyleSheet, self).__init__(
+ 'text/css', href, media, title, disabled,
+ ownerNode, parentStyleSheet,
+ validating=validating)
+
+ self._ownerRule = ownerRule
+ self.cssRules = cssutils.css.CSSRuleList()
+ self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log)
+ self._variables = CSSVariablesDeclaration()
+ self._readonly = readonly
+
+ # used only during setting cssText by parse*()
+ self.__encodingOverride = None
+ self._fetcher = None
+
+ def __iter__(self):
+ "Generator which iterates over cssRules."
+ for rule in self._cssRules:
+ yield rule
+
+ def __repr__(self):
+ if self.media:
+ mediaText = self.media.mediaText
+ else:
+ mediaText = None
+ return "cssutils.css.%s(href=%r, media=%r, title=%r)" % (
+ self.__class__.__name__,
+ self.href, mediaText, self.title)
+
+ def __str__(self):
+ if self.media:
+ mediaText = self.media.mediaText
+ else:
+ mediaText = None
+ return "" % (
+ self.__class__.__name__, self.encoding, self.href,
+ mediaText, self.title, self.namespaces.namespaces,
+ id(self))
+
+ def _cleanNamespaces(self):
+ "Remove all namespace rules with same namespaceURI but last."
+ rules = self.cssRules
+ namespaceitems = self.namespaces.items()
+ i = 0
+ while i < len(rules):
+ rule = rules[i]
+ if rule.type == rule.NAMESPACE_RULE and \
+ (rule.prefix, rule.namespaceURI) not in namespaceitems:
+ self.deleteRule(i)
+ else:
+ i += 1
+
+ def _getUsedURIs(self):
+ "Return set of URIs used in the sheet."
+ useduris = set()
+ for r1 in self:
+ if r1.STYLE_RULE == r1.type:
+ useduris.update(r1.selectorList._getUsedUris())
+ elif r1.MEDIA_RULE == r1.type:
+ for r2 in r1:
+ if r2.type == r2.STYLE_RULE:
+ useduris.update(r2.selectorList._getUsedUris())
+ return useduris
+
+ def _setCssRules(self, cssRules):
+ "Set new cssRules and update contained rules refs."
+ cssRules.append = self.insertRule
+ cssRules.extend = self.insertRule
+ cssRules.__delitem__ = self.deleteRule
+
+ for rule in cssRules:
+ rule._parentStyleSheet = self
+
+ self._cssRules = cssRules
+
+ cssRules = property(lambda self: self._cssRules, _setCssRules,
+ u"All Rules in this style sheet, a "
+ u":class:`~cssutils.css.CSSRuleList`.")
+
+ def _getCssText(self):
+ "Textual representation of the stylesheet (a byte string)."
+ return cssutils.ser.do_CSSStyleSheet(self)
+
+ def _setCssText(self, cssText):
+ """Parse `cssText` and overwrites the whole stylesheet.
+
+ :param cssText:
+ a parseable string or a tuple of (cssText, dict-of-namespaces)
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ If a namespace prefix is found which is not declared.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ """
+ self._checkReadonly()
+
+ cssText, namespaces = self._splitNamespacesOff(cssText)
+ tokenizer = self._tokenize2(cssText)
+
+ def S(expected, seq, token, tokenizer=None):
+ # @charset must be at absolute beginning of style sheet
+ # or 0 for py3
+ return max(1, expected or 0)
+
+ def COMMENT(expected, seq, token, tokenizer=None):
+ "special: sets parent*"
+ self.insertRule(cssutils.css.CSSComment([token],
+ parentStyleSheet=self))
+ # or 0 for py3
+ return max(1, expected or 0)
+
+ def charsetrule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+
+ if expected > 0:
+ self._log.error(u'CSSStylesheet: CSSCharsetRule only allowed '
+ u'at beginning of stylesheet.',
+ token, xml.dom.HierarchyRequestErr)
+ return expected
+ elif rule.wellformed:
+ self.insertRule(rule)
+
+ return 1
+
+ def importrule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSImportRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+
+ if expected > 1:
+ self._log.error(u'CSSStylesheet: CSSImportRule not allowed '
+ u'here.', token, xml.dom.HierarchyRequestErr)
+ return expected
+ elif rule.wellformed:
+ self.insertRule(rule)
+
+ return 1
+
+ def namespacerule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSNamespaceRule(cssText=self._tokensupto2(tokenizer,
+ token),
+ parentStyleSheet=self)
+
+ if expected > 2:
+ self._log.error(u'CSSStylesheet: CSSNamespaceRule not allowed '
+ u'here.', token, xml.dom.HierarchyRequestErr)
+ return expected
+ elif rule.wellformed:
+ if rule.prefix not in self.namespaces:
+ # add new if not same prefix
+ self.insertRule(rule, _clean=False)
+ else:
+ # same prefix => replace namespaceURI
+ for r in self.cssRules.rulesOfType(rule.NAMESPACE_RULE):
+ if r.prefix == rule.prefix:
+ r._replaceNamespaceURI(rule.namespaceURI)
+
+ self._namespaces[rule.prefix] = rule.namespaceURI
+
+ return 2
+
+ def variablesrule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSVariablesRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+
+ if expected > 2:
+ self._log.error(u'CSSStylesheet: CSSVariablesRule not allowed '
+ u'here.', token, xml.dom.HierarchyRequestErr)
+ return expected
+ elif rule.wellformed:
+ self.insertRule(rule)
+ self._updateVariables()
+
+ return 2
+
+ def fontfacerule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return 3
+
+ def mediarule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSMediaRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return 3
+
+ def pagerule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSPageRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return 3
+
+ def unknownrule(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ if token[1] in cssutils.css.MarginRule.margins:
+ self._log.error(u'CSSStylesheet: MarginRule out CSSPageRule.',
+ token, neverraise=True)
+ rule = cssutils.css.MarginRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ else:
+ self._log.warn(u'CSSStylesheet: Unknown @rule found.',
+ token, neverraise=True)
+ rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+
+ if rule.wellformed:
+ self.insertRule(rule)
+
+ # or 0 for py3
+ return max(1, expected or 0)
+
+ def ruleset(expected, seq, token, tokenizer):
+ # parse and consume tokens in any case
+ rule = cssutils.css.CSSStyleRule(parentStyleSheet=self)
+ rule.cssText = self._tokensupto2(tokenizer, token)
+ if rule.wellformed:
+ self.insertRule(rule)
+ return 3
+
+ # save for possible reset
+ oldCssRules = self.cssRules
+ oldNamespaces = self._namespaces
+
+ self.cssRules = cssutils.css.CSSRuleList()
+ # simple during parse
+ self._namespaces = namespaces
+ self._variables = CSSVariablesDeclaration()
+
+ # not used?!
+ newseq = []
+
+ # ['CHARSET', 'IMPORT', ('VAR', NAMESPACE'), ('PAGE', 'MEDIA', ruleset)]
+ wellformed, expected = self._parse(0, newseq, tokenizer,
+ {'S': S,
+ 'COMMENT': COMMENT,
+ 'CDO': lambda *ignored: None,
+ 'CDC': lambda *ignored: None,
+ 'CHARSET_SYM': charsetrule,
+ 'FONT_FACE_SYM': fontfacerule,
+ 'IMPORT_SYM': importrule,
+ 'NAMESPACE_SYM': namespacerule,
+ 'PAGE_SYM': pagerule,
+ 'MEDIA_SYM': mediarule,
+ 'VARIABLES_SYM': variablesrule,
+ 'ATKEYWORD': unknownrule
+ },
+ default=ruleset)
+
+ if wellformed:
+ # use proper namespace object
+ self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log)
+ self._cleanNamespaces()
+
+ else:
+ # reset
+ self._cssRules = oldCssRules
+ self._namespaces = oldNamespaces
+ self._updateVariables()
+ self._cleanNamespaces()
+
+ cssText = property(_getCssText, _setCssText,
+ "Textual representation of the stylesheet (a byte string)")
+
+ def _resolveImport(self, url):
+ """Read (encoding, enctype, decodedContent) from `url` for @import
+ sheets."""
+ try:
+ # only available during parsing of a complete sheet
+ parentEncoding = self.__newEncoding
+
+ except AttributeError:
+ try:
+ # explicit @charset
+ parentEncoding = self._cssRules[0].encoding
+ except (IndexError, AttributeError):
+ # default not UTF-8 but None!
+ parentEncoding = None
+
+
+ return _readUrl(url, fetcher=self._fetcher,
+ overrideEncoding=self.__encodingOverride,
+ parentEncoding=parentEncoding)
+
+ def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None,
+ encoding=None):
+ """Set `cssText` but use `encodingOverride` to overwrite detected
+ encoding. This is used by parse and @import during setting of cssText.
+
+ If `encoding` is given use this but do not save as `encodingOverride`.
+ """
+ if encodingOverride:
+ # encoding during resolving of @import
+ self.__encodingOverride = encodingOverride
+
+ if encoding:
+ # save for nested @import
+ self.__newEncoding = encoding
+
+ self.cssText = cssText
+
+ if encodingOverride:
+ # set encodingOverride explicit again!
+ self.encoding = self.__encodingOverride
+ # del?
+ self.__encodingOverride = None
+ elif encoding:
+ # may e.g. be httpEncoding
+ self.encoding = encoding
+ try:
+ del self.__newEncoding
+ except AttributeError, e:
+ pass
+
+ def _setFetcher(self, fetcher=None):
+ """Set @import URL loader, if None the default is used."""
+ self._fetcher = fetcher
+
+ def _getEncoding(self):
+ """Encoding set in :class:`~cssutils.css.CSSCharsetRule` or if ``None``
+ resulting in default ``utf-8`` encoding being used."""
+ try:
+ return self._cssRules[0].encoding
+ except (IndexError, AttributeError):
+ return 'utf-8'
+
+ def _setEncoding(self, encoding):
+ """Set `encoding` of charset rule if present in sheet or insert a new
+ :class:`~cssutils.css.CSSCharsetRule` with given `encoding`.
+ If `encoding` is None removes charsetrule if present resulting in
+ default encoding of utf-8.
+ """
+ try:
+ rule = self._cssRules[0]
+ except IndexError:
+ rule = None
+ if rule and rule.CHARSET_RULE == rule.type:
+ if encoding:
+ rule.encoding = encoding
+ else:
+ self.deleteRule(0)
+ elif encoding:
+ self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0)
+
+ encoding = property(_getEncoding, _setEncoding,
+ "(cssutils) Reflect encoding of an @charset rule or 'utf-8' "
+ "(default) if set to ``None``")
+
+ namespaces = property(lambda self: self._namespaces,
+ doc="All Namespaces used in this CSSStyleSheet.")
+
+ def _updateVariables(self):
+ """Updates self._variables, called when @import or @variables rules
+ is added to sheet.
+ """
+ for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
+ s = r.styleSheet
+ if s:
+ for var in s.variables:
+ self._variables.setVariable(var, s.variables[var])
+# for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
+# for vr in r.styleSheet.cssRules.rulesOfType(CSSRule.VARIABLES_RULE):
+# for var in vr.variables:
+# self._variables.setVariable(var, vr.variables[var])
+ for vr in self.cssRules.rulesOfType(CSSRule.VARIABLES_RULE):
+ for var in vr.variables:
+ self._variables.setVariable(var, vr.variables[var])
+
+ variables = property(lambda self: self._variables,
+ doc=u"A :class:`cssutils.css.CSSVariablesDeclaration` "
+ u"containing all available variables in this "
+ u"CSSStyleSheet including the ones defined in "
+ u"imported sheets.")
+
+ def add(self, rule):
+ """Add `rule` to style sheet at appropriate position.
+ Same as ``insertRule(rule, inOrder=True)``.
+ """
+ return self.insertRule(rule, index=None, inOrder=True)
+
+ def deleteRule(self, index):
+ """Delete rule at `index` from the style sheet.
+
+ :param index:
+ The `index` of the rule to be removed from the StyleSheet's rule
+ list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is
+ raised but rules for normal Python lists are used. E.g.
+ ``deleteRule(-1)`` removes the last rule in cssRules.
+
+ `index` may also be a CSSRule object which will then be removed
+ from the StyleSheet.
+
+ :exceptions:
+ - :exc:`~xml.dom.IndexSizeErr`:
+ Raised if the specified index does not correspond to a rule in
+ the style sheet's rule list.
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if removing this rule would result in an invalid StyleSheet
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this style sheet is readonly.
+ """
+ self._checkReadonly()
+
+ if isinstance(index, CSSRule):
+ for i, r in enumerate(self.cssRules):
+ if index == r:
+ index = i
+ break
+ else:
+ raise xml.dom.IndexSizeErr(u"CSSStyleSheet: Not a rule in"
+ " this sheets'a cssRules list: %s"
+ % index)
+
+ try:
+ rule = self._cssRules[index]
+ except IndexError:
+ raise xml.dom.IndexSizeErr(
+ u'CSSStyleSheet: %s is not a valid index in the rulelist of '
+ u'length %i' % (index, self._cssRules.length))
+ else:
+ if rule.type == rule.NAMESPACE_RULE:
+ # check all namespacerules if used
+ uris = [r.namespaceURI for r in self
+ if r.type == r.NAMESPACE_RULE]
+ useduris = self._getUsedURIs()
+ if rule.namespaceURI in useduris and\
+ uris.count(rule.namespaceURI) == 1:
+ raise xml.dom.NoModificationAllowedErr(
+ u'CSSStyleSheet: NamespaceURI defined in this rule is '
+ u'used, cannot remove.')
+ return
+
+ rule._parentStyleSheet = None # detach
+ del self._cssRules[index] # delete from StyleSheet
+
+ def insertRule(self, rule, index=None, inOrder=False, _clean=True):
+ """
+ Used to insert a new rule into the style sheet. The new rule now
+ becomes part of the cascade.
+
+ :param rule:
+ a parsable DOMString, in cssutils also a
+ :class:`~cssutils.css.CSSRule` or :class:`~cssutils.css.CSSRuleList`
+ :param index:
+ of the rule before the new rule will be inserted.
+ If the specified `index` is equal to the length of the
+ StyleSheet's rule collection, the rule will be added to the end
+ of the style sheet.
+ If `index` is not given or ``None`` rule will be appended to rule
+ list.
+ :param inOrder:
+ if ``True`` the rule will be put to a proper location while
+ ignoring `index` and without raising
+ :exc:`~xml.dom.HierarchyRequestErr`.
+ The resulting index is returned nevertheless.
+ :returns: The index within the style sheet's rule collection
+ :Exceptions:
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at the specified `index`
+ e.g. if an @import rule is inserted after a standard rule set
+ or other at-rule.
+ - :exc:`~xml.dom.IndexSizeErr`:
+ Raised if the specified `index` is not a valid insertion point.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this style sheet is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified rule has a syntax error and is
+ unparsable.
+ """
+ self._checkReadonly()
+
+ # check position
+ if index is None:
+ index = len(self._cssRules)
+ elif index < 0 or index > self._cssRules.length:
+ raise xml.dom.IndexSizeErr(
+ u'CSSStyleSheet: Invalid index %s for CSSRuleList with a '
+ u'length of %s.' % (index, self._cssRules.length))
+ return
+
+ if isinstance(rule, basestring):
+ # init a temp sheet which has the same properties as self
+ tempsheet = CSSStyleSheet(href=self.href,
+ media=self.media,
+ title=self.title,
+ parentStyleSheet=self.parentStyleSheet,
+ ownerRule=self.ownerRule)
+ tempsheet._ownerNode = self.ownerNode
+ tempsheet._fetcher = self._fetcher
+ # prepend encoding if in this sheet to be able to use it in
+ # @import rules encoding resolution
+ # do not add if new rule startswith "@charset" (which is exact!)
+ if not rule.startswith(u'@charset') and (self._cssRules and
+ self._cssRules[0].type == self._cssRules[0].CHARSET_RULE):
+ # rule 0 is @charset!
+ newrulescount, newruleindex = 2, 1
+ rule = self._cssRules[0].cssText + rule
+ else:
+ newrulescount, newruleindex = 1, 0
+
+ # parse the new rule(s)
+ tempsheet.cssText = (rule, self._namespaces)
+
+ if len(tempsheet.cssRules) != newrulescount or (not isinstance(
+ tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)):
+ self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule)
+ return
+ rule = tempsheet.cssRules[newruleindex]
+ rule._parentStyleSheet = None # done later?
+
+ # TODO:
+ #tempsheet._namespaces = self._namespaces
+ #variables?
+
+ elif isinstance(rule, cssutils.css.CSSRuleList):
+ # insert all rules
+ for i, r in enumerate(rule):
+ self.insertRule(r, index + i)
+ return index
+
+ if not rule.wellformed:
+ self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.')
+ return
+
+ # CHECK HIERARCHY
+ # @charset
+ if rule.type == rule.CHARSET_RULE:
+ if inOrder:
+ index = 0
+ # always first and only
+ if (self._cssRules
+ and self._cssRules[0].type == rule.CHARSET_RULE):
+ self._cssRules[0].encoding = rule.encoding
+ else:
+ self._cssRules.insert(0, rule)
+ elif index != 0 or (self._cssRules and
+ self._cssRules[0].type == rule.CHARSET_RULE):
+ self._log.error(
+ u'CSSStylesheet: @charset only allowed once at the'
+ ' beginning of a stylesheet.',
+ error=xml.dom.HierarchyRequestErr)
+ return
+ else:
+ self._cssRules.insert(index, rule)
+
+ # @unknown or comment
+ elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder:
+ if index == 0 and self._cssRules and\
+ self._cssRules[0].type == rule.CHARSET_RULE:
+ self._log.error(
+ u'CSSStylesheet: @charset must be the first rule.',
+ error=xml.dom.HierarchyRequestErr)
+ return
+ else:
+ self._cssRules.insert(index, rule)
+
+ # @import
+ elif rule.type == rule.IMPORT_RULE:
+ if inOrder:
+ # automatic order
+ if rule.type in (r.type for r in self):
+ # find last of this type
+ for i, r in enumerate(reversed(self._cssRules)):
+ if r.type == rule.type:
+ index = len(self._cssRules) - i
+ break
+ else:
+ # find first point to insert
+ if self._cssRules and\
+ self._cssRules[0].type in (rule.CHARSET_RULE,
+ rule.COMMENT):
+ index = 1
+ else:
+ index = 0
+ else:
+ # after @charset
+ if index == 0 and self._cssRules and\
+ self._cssRules[0].type == rule.CHARSET_RULE:
+ self._log.error(
+ u'CSSStylesheet: Found @charset at index 0.',
+ error=xml.dom.HierarchyRequestErr)
+ return
+ # before @namespace @variables @page @font-face @media stylerule
+ for r in self._cssRules[:index]:
+ if r.type in (r.NAMESPACE_RULE,
+ r.VARIABLES_RULE,
+ r.MEDIA_RULE,
+ r.PAGE_RULE,
+ r.STYLE_RULE,
+ r.FONT_FACE_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert @import here,'
+ ' found @namespace, @variables, @media, @page or'
+ ' CSSStyleRule before index %s.' %
+ index,
+ error=xml.dom.HierarchyRequestErr)
+ return
+ self._cssRules.insert(index, rule)
+ self._updateVariables()
+
+ # @namespace
+ elif rule.type == rule.NAMESPACE_RULE:
+ if inOrder:
+ if rule.type in (r.type for r in self):
+ # find last of this type
+ for i, r in enumerate(reversed(self._cssRules)):
+ if r.type == rule.type:
+ index = len(self._cssRules) - i
+ break
+ else:
+ # find first point to insert
+ for i, r in enumerate(self._cssRules):
+ if r.type in (r.VARIABLES_RULE, r.MEDIA_RULE,
+ r.PAGE_RULE, r.STYLE_RULE,
+ r.FONT_FACE_RULE, r.UNKNOWN_RULE,
+ r.COMMENT):
+ index = i # before these
+ break
+ else:
+ # after @charset and @import
+ for r in self._cssRules[index:]:
+ if r.type in (r.CHARSET_RULE, r.IMPORT_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert @namespace here,'
+ ' found @charset or @import after index %s.' %
+ index,
+ error=xml.dom.HierarchyRequestErr)
+ return
+ # before @variables @media @page @font-face and stylerule
+ for r in self._cssRules[:index]:
+ if r.type in (r.VARIABLES_RULE,
+ r.MEDIA_RULE,
+ r.PAGE_RULE,
+ r.STYLE_RULE,
+ r.FONT_FACE_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert @namespace here,'
+ ' found @variables, @media, @page or CSSStyleRule'
+ ' before index %s.' %
+ index,
+ error=xml.dom.HierarchyRequestErr)
+ return
+
+ if not (rule.prefix in self.namespaces and
+ self.namespaces[rule.prefix] == rule.namespaceURI):
+ # no doublettes
+ self._cssRules.insert(index, rule)
+ if _clean:
+ self._cleanNamespaces()
+
+
+ # @variables
+ elif rule.type == rule.VARIABLES_RULE:
+ if inOrder:
+ if rule.type in (r.type for r in self):
+ # find last of this type
+ for i, r in enumerate(reversed(self._cssRules)):
+ if r.type == rule.type:
+ index = len(self._cssRules) - i
+ break
+ else:
+ # find first point to insert
+ for i, r in enumerate(self._cssRules):
+ if r.type in (r.MEDIA_RULE,
+ r.PAGE_RULE,
+ r.STYLE_RULE,
+ r.FONT_FACE_RULE,
+ r.UNKNOWN_RULE,
+ r.COMMENT):
+ index = i # before these
+ break
+ else:
+ # after @charset @import @namespace
+ for r in self._cssRules[index:]:
+ if r.type in (r.CHARSET_RULE,
+ r.IMPORT_RULE,
+ r.NAMESPACE_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert @variables here,'
+ ' found @charset, @import or @namespace after'
+ ' index %s.' %
+ index,
+ error=xml.dom.HierarchyRequestErr)
+ return
+ # before @media @page @font-face and stylerule
+ for r in self._cssRules[:index]:
+ if r.type in (r.MEDIA_RULE,
+ r.PAGE_RULE,
+ r.STYLE_RULE,
+ r.FONT_FACE_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert @variables here,'
+ ' found @media, @page or CSSStyleRule'
+ ' before index %s.' %
+ index,
+ error=xml.dom.HierarchyRequestErr)
+ return
+
+ self._cssRules.insert(index, rule)
+ self._updateVariables()
+
+ # all other where order is not important
+ else:
+ if inOrder:
+ # simply add to end as no specific order
+ self._cssRules.append(rule)
+ index = len(self._cssRules) - 1
+ else:
+ for r in self._cssRules[index:]:
+ if r.type in (r.CHARSET_RULE,
+ r.IMPORT_RULE,
+ r.NAMESPACE_RULE):
+ self._log.error(
+ u'CSSStylesheet: Cannot insert rule here, found '
+ u'@charset, @import or @namespace before index %s.'
+ % index, error=xml.dom.HierarchyRequestErr)
+ return
+ self._cssRules.insert(index, rule)
+
+ # post settings
+ rule._parentStyleSheet = self
+
+ if rule.IMPORT_RULE == rule.type and not rule.hrefFound:
+ # try loading the imported sheet which has new relative href now
+ rule.href = rule.href
+
+ return index
+
+ ownerRule = property(lambda self: self._ownerRule,
+ doc=u'A ref to an @import rule if it is imported, '
+ u'else ``None``.')
+
+
+ @Deprecated(u'Use ``cssutils.setSerializer(serializer)`` instead.')
+ def setSerializer(self, cssserializer):
+ """Set the cssutils global Serializer used for all output."""
+ if isinstance(cssserializer, cssutils.CSSSerializer):
+ cssutils.ser = cssserializer
+ else:
+ raise ValueError(u'Serializer must be an instance of '
+ u'cssutils.CSSSerializer.')
+
+ @Deprecated(u'Set pref in ``cssutils.ser.prefs`` instead.')
+ def setSerializerPref(self, pref, value):
+ """Set a Preference of CSSSerializer used for output.
+ See :class:`cssutils.serialize.Preferences` for possible
+ preferences to be set.
+ """
+ cssutils.ser.prefs.__setattr__(pref, value)
diff --git a/libs/cssutils/css/cssunknownrule.py b/libs/cssutils/css/cssunknownrule.py
new file mode 100755
index 00000000..c9e4361f
--- /dev/null
+++ b/libs/cssutils/css/cssunknownrule.py
@@ -0,0 +1,209 @@
+"""CSSUnknownRule implements DOM Level 2 CSS CSSUnknownRule."""
+__all__ = ['CSSUnknownRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSUnknownRule(cssrule.CSSRule):
+ """
+ Represents an at-rule not supported by this user agent, so in
+ effect all other at-rules not defined in cssutils.
+
+ Format::
+
+ @xxx until ';' or block {...}
+ """
+ def __init__(self, cssText=u'', parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ :param cssText:
+ of type string
+ """
+ super(CSSUnknownRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = None
+ if cssText:
+ self.cssText = cssText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(cssText=%r)" % (
+ self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.cssText,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSUnknownRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(CSSUnknownRule, self)._setCssText(cssText)
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if not attoken or self._type(attoken) != self._prods.ATKEYWORD:
+ self._log.error(u'CSSUnknownRule: No CSSUnknownRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ # for closures: must be a mutable
+ new = {'nesting': [], # {} [] or ()
+ 'wellformed': True
+ }
+
+ def CHAR(expected, seq, token, tokenizer=None):
+ type_, val, line, col = token
+ if expected != 'EOF':
+ if val in u'{[(':
+ new['nesting'].append(val)
+ elif val in u'}])':
+ opening = {u'}': u'{', u']': u'[', u')': u'('}[val]
+ try:
+ if new['nesting'][-1] == opening:
+ new['nesting'].pop()
+ else:
+ raise IndexError()
+ except IndexError:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Wrong nesting of '
+ u'{, [ or (.', token=token)
+
+ if val in u'};' and not new['nesting']:
+ expected = 'EOF'
+
+ seq.append(val, type_, line=line, col=col)
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Expected end of rule.',
+ token=token)
+ return expected
+
+ def FUNCTION(expected, seq, token, tokenizer=None):
+ # handled as opening (
+ type_, val, line, col = token
+ val = self._tokenvalue(token)
+ if expected != 'EOF':
+ new['nesting'].append(u'(')
+ seq.append(val, type_, line=line, col=col)
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Expected end of rule.',
+ token=token)
+ return expected
+
+ def EOF(expected, seq, token, tokenizer=None):
+ "close all blocks and return 'EOF'"
+ for x in reversed(new['nesting']):
+ closing = {u'{': u'}', u'[': u']', u'(': u')'}[x]
+ seq.append(closing, closing)
+ new['nesting'] = []
+ return 'EOF'
+
+ def INVALID(expected, seq, token, tokenizer=None):
+ # makes rule invalid
+ self._log.error(u'CSSUnknownRule: Bad syntax.',
+ token=token, error=xml.dom.SyntaxErr)
+ new['wellformed'] = False
+ return expected
+
+ def STRING(expected, seq, token, tokenizer=None):
+ type_, val, line, col = token
+ val = self._stringtokenvalue(token)
+ if expected != 'EOF':
+ seq.append(val, type_, line=line, col=col)
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Expected end of rule.',
+ token=token)
+ return expected
+
+ def URI(expected, seq, token, tokenizer=None):
+ type_, val, line, col = token
+ val = self._uritokenvalue(token)
+ if expected != 'EOF':
+ seq.append(val, type_, line=line, col=col)
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Expected end of rule.',
+ token=token)
+ return expected
+
+ def default(expected, seq, token, tokenizer=None):
+ type_, val, line, col = token
+ if expected != 'EOF':
+ seq.append(val, type_, line=line, col=col)
+ return expected
+ else:
+ new['wellformed'] = False
+ self._log.error(u'CSSUnknownRule: Expected end of rule.',
+ token=token)
+ return expected
+
+ # unknown : ATKEYWORD S* ... ; | }
+ newseq = self._tempSeq()
+ wellformed, expected = self._parse(expected=None,
+ seq=newseq, tokenizer=tokenizer,
+ productions={'CHAR': CHAR,
+ 'EOF': EOF,
+ 'FUNCTION': FUNCTION,
+ 'INVALID': INVALID,
+ 'STRING': STRING,
+ 'URI': URI,
+ 'S': default # overwrite default default!
+ },
+ default=default,
+ new=new)
+
+ # wellformed set by parse
+ wellformed = wellformed and new['wellformed']
+
+ # post conditions
+ if expected != 'EOF':
+ wellformed = False
+ self._log.error(u'CSSUnknownRule: No ending ";" or "}" found: '
+ u'%r' % self._valuestr(cssText))
+ elif new['nesting']:
+ wellformed = False
+ self._log.error(u'CSSUnknownRule: Unclosed "{", "[" or "(": %r'
+ % self._valuestr(cssText))
+
+ # set all
+ if wellformed:
+ self.atkeyword = self._tokenvalue(attoken)
+ self._setSeq(newseq)
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc=u"(DOM) The parsable textual representation.")
+
+ type = property(lambda self: self.UNKNOWN_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ wellformed = property(lambda self: bool(self.atkeyword))
+
\ No newline at end of file
diff --git a/libs/cssutils/css/cssvalue.py b/libs/cssutils/css/cssvalue.py
new file mode 100755
index 00000000..2f643453
--- /dev/null
+++ b/libs/cssutils/css/cssvalue.py
@@ -0,0 +1,1251 @@
+"""CSSValue related classes
+
+- CSSValue implements DOM Level 2 CSS CSSValue
+- CSSPrimitiveValue implements DOM Level 2 CSS CSSPrimitiveValue
+- CSSValueList implements DOM Level 2 CSS CSSValueList
+
+"""
+__all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList', 'RGBColor',
+ 'CSSVariable']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.prodparser import *
+import cssutils
+import cssutils.helper
+import math
+import re
+import xml.dom
+
+
+class CSSValue(cssutils.util._NewBase):
+ """The CSSValue interface represents a simple or a complex value.
+ A CSSValue object only occurs in a context of a CSS property.
+ """
+
+ # The value is inherited and the cssText contains "inherit".
+ CSS_INHERIT = 0
+ # The value is a CSSPrimitiveValue.
+ CSS_PRIMITIVE_VALUE = 1
+ # The value is a CSSValueList.
+ CSS_VALUE_LIST = 2
+ # The value is a custom value.
+ CSS_CUSTOM = 3
+ # The value is a CSSVariable.
+ CSS_VARIABLE = 4
+
+ _typestrings = {0: 'CSS_INHERIT' ,
+ 1: 'CSS_PRIMITIVE_VALUE',
+ 2: 'CSS_VALUE_LIST',
+ 3: 'CSS_CUSTOM',
+ 4: 'CSS_VARIABLE'}
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """
+ :param cssText:
+ the parsable cssText of the value
+ :param readonly:
+ defaults to False
+ """
+ super(CSSValue, self).__init__()
+
+ self._cssValueType = None
+ self.wellformed = False
+ self.parent = parent
+ if cssText is not None: # may be 0
+ if isinstance(cssText, int):
+ cssText = unicode(cssText) # if it is an integer
+ elif isinstance(cssText, float):
+ cssText = u'%f' % cssText # if it is a floating point number
+
+ self.cssText = cssText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.cssValueTypeString,
+ self.cssText,
+ id(self))
+
+ def _setCssText(self, cssText):
+ """
+ Format::
+
+ unary_operator
+ : '-' | '+'
+ ;
+ operator
+ : '/' S* | ',' S* | /* empty */
+ ;
+ expr
+ : term [ operator term ]*
+ ;
+ term
+ : unary_operator?
+ [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
+ ANGLE S* | TIME S* | FREQ S* ]
+ | STRING S* | IDENT S* | URI S* | hexcolor | function
+ | UNICODE-RANGE S*
+ ;
+ function
+ : FUNCTION S* expr ')' S*
+ ;
+ /*
+ * There is a constraint on the color that it must
+ * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
+ * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
+ */
+ hexcolor
+ : HASH S*
+ ;
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ (according to the attached property) or is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ TODO: Raised if the specified CSS string value represents a
+ different type of values than the values allowed by the CSS
+ property.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this value is readonly.
+ """
+ self._checkReadonly()
+
+ # used as operator is , / or S
+ nextSor = u',/'
+
+ term = Choice(Sequence(PreDef.unary(),
+ Choice(PreDef.number(nextSor=nextSor),
+ PreDef.percentage(nextSor=nextSor),
+ PreDef.dimension(nextSor=nextSor))),
+ PreDef.string(nextSor=nextSor),
+ PreDef.ident(nextSor=nextSor),
+ PreDef.uri(nextSor=nextSor),
+ PreDef.hexcolor(nextSor=nextSor),
+ PreDef.unicode_range(nextSor=nextSor),
+ # special case IE only expression
+ Prod(name='expression',
+ match=lambda t, v: t == self._prods.FUNCTION and (
+ cssutils.helper.normalize(v) in (u'expression(',
+ u'alpha(',
+ u'blur(',
+ u'chroma(',
+ u'dropshadow(',
+ u'fliph(',
+ u'flipv(',
+ u'glow(',
+ u'gray(',
+ u'invert(',
+ u'mask(',
+ u'shadow(',
+ u'wave(',
+ u'xray(') or
+ v.startswith(u'progid:DXImageTransform.Microsoft.')
+ ),
+ nextSor=nextSor,
+ toSeq=lambda t, tokens: (ExpressionValue._functionName,
+ ExpressionValue(
+ cssutils.helper.pushtoken(t, tokens),
+ parent=self)
+ )
+ ),
+ # CSS Variable var(
+ PreDef.variable(nextSor=nextSor,
+ toSeq=lambda t, tokens: ('CSSVariable',
+ CSSVariable(
+ cssutils.helper.pushtoken(t, tokens),
+ parent=self)
+ )
+ ),
+ # calc(
+ PreDef.calc(nextSor=nextSor,
+ toSeq=lambda t, tokens: (CalcValue._functionName,
+ CalcValue(
+ cssutils.helper.pushtoken(t, tokens),
+ parent=self)
+ )
+ ),
+# TODO:
+# # rgb/rgba(
+# Prod(name='RGBColor',
+# match=lambda t, v: t == self._prods.FUNCTION and (
+# cssutils.helper.normalize(v) in (u'rgb(',
+# u'rgba('
+# )
+# ),
+# nextSor=nextSor,
+# toSeq=lambda t, tokens: (RGBColor._functionName,
+# RGBColor(
+# cssutils.helper.pushtoken(t, tokens),
+# parent=self)
+# )
+# ),
+ # other functions like rgb( etc
+ PreDef.function(nextSor=nextSor,
+ toSeq=lambda t, tokens: ('FUNCTION',
+ CSSFunction(
+ cssutils.helper.pushtoken(t, tokens),
+ parent=self)
+ )
+ )
+ )
+ operator = Choice(PreDef.S(),
+ PreDef.char('comma', ',',
+ toSeq=lambda t, tokens: ('operator', t[1])),
+ PreDef.char('slash', '/',
+ toSeq=lambda t, tokens: ('operator', t[1])),
+ optional=True)
+ # CSSValue PRODUCTIONS
+ valueprods = Sequence(term,
+ Sequence(operator, # mayEnd this Sequence if whitespace
+
+ # TODO: only when setting via other class
+ # used by variabledeclaration currently
+ PreDef.char('END', ';',
+ stopAndKeep=True,
+ optional=True),
+
+ term,
+ minmax=lambda: (0, None)))
+ # parse
+ wellformed, seq, store, notused = ProdParser().parse(cssText,
+ u'CSSValue',
+ valueprods,
+ keepS=True)
+ if wellformed:
+ # - count actual values and set firstvalue which is used later on
+ # - combine comma separated list, e.g. font-family to a single item
+ # - remove S which should be an operator but is no needed
+ count, firstvalue = 0, ()
+ newseq = self._tempSeq()
+ i, end = 0, len(seq)
+ while i < end:
+ item = seq[i]
+ if item.type == self._prods.S:
+ pass
+
+ elif (item.value, item.type) == (u',', 'operator'):
+ # , separared counts as a single STRING for now
+ # URI or STRING value might be a single CHAR too!
+ newseq.appendItem(item)
+ count -= 1
+ if firstvalue:
+ # list of IDENTs is handled as STRING!
+ if firstvalue[1] == self._prods.IDENT:
+ firstvalue = firstvalue[0], 'STRING'
+
+ elif item.value == u'/':
+ # / separated items count as one
+ newseq.appendItem(item)
+
+ elif item.value == u'-' or item.value == u'+':
+ # combine +- and following number or other
+ i += 1
+ try:
+ next = seq[i]
+ except IndexError:
+ firstvalue = () # raised later
+ break
+
+ newval = item.value + next.value
+ newseq.append(newval, next.type,
+ item.line, item.col)
+ if not firstvalue:
+ firstvalue = (newval, next.type)
+ count += 1
+
+ elif item.type != cssutils.css.CSSComment:
+ newseq.appendItem(item)
+ if not firstvalue:
+ firstvalue = (item.value, item.type)
+ count += 1
+
+ else:
+ newseq.appendItem(item)
+
+ i += 1
+
+ if not firstvalue:
+ self._log.error(
+ u'CSSValue: Unknown syntax or no value: %r.' %
+ self._valuestr(cssText))
+ else:
+ # ok and set
+ self._setSeq(newseq)
+ self.wellformed = wellformed
+
+ if hasattr(self, '_value'):
+ # only in case of CSSPrimitiveValue, else remove!
+ del self._value
+
+ if count == 1:
+ # inherit, primitive or variable
+ if isinstance(firstvalue[0], basestring) and\
+ u'inherit' == cssutils.helper.normalize(firstvalue[0]):
+ self.__class__ = CSSValue
+ self._cssValueType = CSSValue.CSS_INHERIT
+ elif 'CSSVariable' == firstvalue[1]:
+ self.__class__ = CSSVariable
+ self._value = firstvalue
+ # TODO: remove major hack!
+ self._name = firstvalue[0]._name
+ else:
+ self.__class__ = CSSPrimitiveValue
+ self._value = firstvalue
+
+ elif count > 1:
+ # valuelist
+ self.__class__ = CSSValueList
+
+ # change items in list to specific type (primitive etc)
+ newseq = self._tempSeq()
+ commalist = []
+ nexttocommalist = False
+
+ def itemValue(item):
+ "Reserialized simple item.value"
+ if self._prods.STRING == item.type:
+ return cssutils.helper.string(item.value)
+ elif self._prods.URI == item.type:
+ return cssutils.helper.uri(item.value)
+ elif self._prods.FUNCTION == item.type or\
+ 'CSSVariable' == item.type:
+ return item.value.cssText
+ else:
+ return item.value
+
+ def saveifcommalist(commalist, newseq):
+ """
+ saves items in commalist to seq and items
+ if anything in there
+ """
+ if commalist:
+ newseq.replace(-1,
+ CSSPrimitiveValue(cssText=u''.join(
+ commalist)),
+ CSSPrimitiveValue,
+ newseq[-1].line,
+ newseq[-1].col)
+ del commalist[:]
+
+ for i, item in enumerate(self._seq):
+ if issubclass(type(item.value), CSSValue):
+ # set parent of CSSValueList items to the lists
+ # parent
+ item.value.parent = self.parent
+
+ if item.type in (self._prods.DIMENSION,
+ self._prods.FUNCTION,
+ self._prods.HASH,
+ self._prods.IDENT,
+ self._prods.NUMBER,
+ self._prods.PERCENTAGE,
+ self._prods.STRING,
+ self._prods.URI,
+ self._prods.UNICODE_RANGE,
+ 'CSSVariable'):
+ if nexttocommalist:
+ # wait until complete
+ commalist.append(itemValue(item))
+ else:
+ saveifcommalist(commalist, newseq)
+ # append new item
+ if hasattr(item.value, 'cssText'):
+ newseq.append(item.value,
+ item.value.__class__,
+ item.line, item.col)
+
+ else:
+ newseq.append(CSSPrimitiveValue(
+ itemValue(item)),
+ CSSPrimitiveValue,
+ item.line, item.col)
+
+ nexttocommalist = False
+
+ elif u',' == item.value:
+ if not commalist:
+ # save last item to commalist
+ commalist.append(itemValue(self._seq[i - 1]))
+ commalist.append(u',')
+ nexttocommalist = True
+
+ else:
+ if nexttocommalist:
+ commalist.append(item.value.cssText)
+ else:
+ newseq.appendItem(item)
+
+ saveifcommalist(commalist, newseq)
+ self._setSeq(newseq)
+
+ else:
+ # should not happen...
+ self.__class__ = CSSValue
+ self._cssValueType = CSSValue.CSS_CUSTOM
+
+ cssText = property(lambda self: cssutils.ser.do_css_CSSValue(self),
+ _setCssText,
+ doc="A string representation of the current value.")
+
+ cssValueType = property(lambda self: self._cssValueType,
+ doc="A (readonly) code defining the type of the value.")
+
+ cssValueTypeString = property(
+ lambda self: CSSValue._typestrings.get(self.cssValueType, None),
+ doc="(readonly) Name of cssValueType.")
+
+
+class CSSPrimitiveValue(CSSValue):
+ """Represents a single CSS Value. May be used to determine the value of a
+ specific style property currently set in a block or to set a specific
+ style property explicitly within the block. Might be obtained from the
+ getPropertyCSSValue method of CSSStyleDeclaration.
+
+ Conversions are allowed between absolute values (from millimeters to
+ centimeters, from degrees to radians, and so on) but not between
+ relative values. (For example, a pixel value cannot be converted to a
+ centimeter value.) Percentage values can't be converted since they are
+ relative to the parent value (or another property value). There is one
+ exception for color percentage values: since a color percentage value
+ is relative to the range 0-255, a color percentage value can be
+ converted to a number; (see also the RGBColor interface).
+ """
+ # constant: type of this CSSValue class
+ cssValueType = CSSValue.CSS_PRIMITIVE_VALUE
+
+ __types = cssutils.cssproductions.CSSProductions
+
+ # An integer indicating which type of unit applies to the value.
+ CSS_UNKNOWN = 0 # only obtainable via cssText
+ CSS_NUMBER = 1
+ CSS_PERCENTAGE = 2
+ CSS_EMS = 3
+ CSS_EXS = 4
+ CSS_PX = 5
+ CSS_CM = 6
+ CSS_MM = 7
+ CSS_IN = 8
+ CSS_PT = 9
+ CSS_PC = 10
+ CSS_DEG = 11
+ CSS_RAD = 12
+ CSS_GRAD = 13
+ CSS_MS = 14
+ CSS_S = 15
+ CSS_HZ = 16
+ CSS_KHZ = 17
+ CSS_DIMENSION = 18
+ CSS_STRING = 19
+ CSS_URI = 20
+ CSS_IDENT = 21
+ CSS_ATTR = 22
+ CSS_COUNTER = 23
+ CSS_RECT = 24
+ CSS_RGBCOLOR = 25
+ # NOT OFFICIAL:
+ CSS_RGBACOLOR = 26
+ CSS_UNICODE_RANGE = 27
+
+ _floattypes = (CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS,
+ CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC,
+ CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS, CSS_S,
+ CSS_HZ, CSS_KHZ, CSS_DIMENSION)
+ _stringtypes = (CSS_ATTR, CSS_IDENT, CSS_STRING, CSS_URI)
+ _countertypes = (CSS_COUNTER,)
+ _recttypes = (CSS_RECT,)
+ _rbgtypes = (CSS_RGBCOLOR, CSS_RGBACOLOR)
+ _lengthtypes = (CSS_NUMBER, CSS_EMS, CSS_EXS,
+ CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC)
+
+ # oldtype: newType: converterfunc
+ _converter = {
+ # cm <-> mm <-> in, 1 inch is equal to 2.54 centimeters.
+ # pt <-> pc, the points used by CSS 2.1 are equal to 1/72nd of an inch.
+ # pc: picas - 1 pica is equal to 12 points
+ (CSS_CM, CSS_MM): lambda x: x * 10,
+ (CSS_MM, CSS_CM): lambda x: x / 10,
+
+ (CSS_PT, CSS_PC): lambda x: x * 12,
+ (CSS_PC, CSS_PT): lambda x: x / 12,
+
+ (CSS_CM, CSS_IN): lambda x: x / 2.54,
+ (CSS_IN, CSS_CM): lambda x: x * 2.54,
+ (CSS_MM, CSS_IN): lambda x: x / 25.4,
+ (CSS_IN, CSS_MM): lambda x: x * 25.4,
+
+ (CSS_IN, CSS_PT): lambda x: x / 72,
+ (CSS_PT, CSS_IN): lambda x: x * 72,
+ (CSS_CM, CSS_PT): lambda x: x / 2.54 / 72,
+ (CSS_PT, CSS_CM): lambda x: x * 72 * 2.54,
+ (CSS_MM, CSS_PT): lambda x: x / 25.4 / 72,
+ (CSS_PT, CSS_MM): lambda x: x * 72 * 25.4,
+
+ (CSS_IN, CSS_PC): lambda x: x / 72 / 12,
+ (CSS_PC, CSS_IN): lambda x: x * 12 * 72,
+ (CSS_CM, CSS_PC): lambda x: x / 2.54 / 72 / 12,
+ (CSS_PC, CSS_CM): lambda x: x * 12 * 72 * 2.54,
+ (CSS_MM, CSS_PC): lambda x: x / 25.4 / 72 / 12,
+ (CSS_PC, CSS_MM): lambda x: x * 12 * 72 * 25.4,
+
+ # hz <-> khz
+ (CSS_KHZ, CSS_HZ): lambda x: x * 1000,
+ (CSS_HZ, CSS_KHZ): lambda x: x / 1000,
+ # s <-> ms
+ (CSS_S, CSS_MS): lambda x: x * 1000,
+ (CSS_MS, CSS_S): lambda x: x / 1000,
+
+ (CSS_RAD, CSS_DEG): lambda x: math.degrees(x),
+ (CSS_DEG, CSS_RAD): lambda x: math.radians(x),
+ # TODO: convert grad <-> deg or rad
+ #(CSS_RAD, CSS_GRAD): lambda x: math.degrees(x),
+ #(CSS_DEG, CSS_GRAD): lambda x: math.radians(x),
+ #(CSS_GRAD, CSS_RAD): lambda x: math.radians(x),
+ #(CSS_GRAD, CSS_DEG): lambda x: math.radians(x)
+ }
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """See CSSPrimitiveValue.__init__()"""
+ super(CSSPrimitiveValue, self).__init__(cssText=cssText,
+ parent=parent,
+ readonly=readonly)
+
+ def __str__(self):
+ return u""\
+ % (self.__class__.__name__,
+ self.primitiveTypeString,
+ self.cssText,
+ id(self))
+
+ _unitnames = ['CSS_UNKNOWN',
+ 'CSS_NUMBER', 'CSS_PERCENTAGE',
+ 'CSS_EMS', 'CSS_EXS',
+ 'CSS_PX',
+ 'CSS_CM', 'CSS_MM',
+ 'CSS_IN',
+ 'CSS_PT', 'CSS_PC',
+ 'CSS_DEG', 'CSS_RAD', 'CSS_GRAD',
+ 'CSS_MS', 'CSS_S',
+ 'CSS_HZ', 'CSS_KHZ',
+ 'CSS_DIMENSION',
+ 'CSS_STRING', 'CSS_URI', 'CSS_IDENT',
+ 'CSS_ATTR', 'CSS_COUNTER', 'CSS_RECT',
+ 'CSS_RGBCOLOR', 'CSS_RGBACOLOR',
+ 'CSS_UNICODE_RANGE'
+ ]
+
+ _reNumDim = re.compile(ur'([+-]?\d*\.\d+|[+-]?\d+)(.*)$', re.I | re.U | re.X)
+
+ def _unitDIMENSION(value):
+ """Check val for dimension name."""
+ units = {'em': 'CSS_EMS', 'ex': 'CSS_EXS',
+ 'px': 'CSS_PX',
+ 'cm': 'CSS_CM', 'mm': 'CSS_MM',
+ 'in': 'CSS_IN',
+ 'pt': 'CSS_PT', 'pc': 'CSS_PC',
+ 'deg': 'CSS_DEG', 'rad': 'CSS_RAD', 'grad': 'CSS_GRAD',
+ 'ms': 'CSS_MS', 's': 'CSS_S',
+ 'hz': 'CSS_HZ', 'khz': 'CSS_KHZ'
+ }
+ val, dim = CSSPrimitiveValue._reNumDim.findall(cssutils.helper.normalize(value))[0]
+ return units.get(dim, 'CSS_DIMENSION')
+
+ def _unitFUNCTION(value):
+ """Check val for function name."""
+ units = {'attr(': 'CSS_ATTR',
+ 'counter(': 'CSS_COUNTER',
+ 'rect(': 'CSS_RECT',
+ 'rgb(': 'CSS_RGBCOLOR',
+ 'rgba(': 'CSS_RGBACOLOR',
+ }
+ return units.get(re.findall(ur'^(.*?\()',
+ cssutils.helper.normalize(value.cssText),
+ re.U)[0],
+ 'CSS_UNKNOWN')
+
+ __unitbytype = {
+ __types.NUMBER: 'CSS_NUMBER',
+ __types.PERCENTAGE: 'CSS_PERCENTAGE',
+ __types.STRING: 'CSS_STRING',
+ __types.UNICODE_RANGE: 'CSS_UNICODE_RANGE',
+ __types.URI: 'CSS_URI',
+ __types.IDENT: 'CSS_IDENT',
+ __types.HASH: 'CSS_RGBCOLOR',
+ __types.DIMENSION: _unitDIMENSION,
+ __types.FUNCTION: _unitFUNCTION
+ }
+
+ def __set_primitiveType(self):
+ """primitiveType is readonly but is set lazy if accessed"""
+ # TODO: check unary and font-family STRING a, b, "c"
+ val, type_ = self._value
+ # try get by type_
+ pt = self.__unitbytype.get(type_, 'CSS_UNKNOWN')
+ if callable(pt):
+ # multiple options, check value too
+ pt = pt(val)
+ self._primitiveType = getattr(self, pt)
+
+ def _getPrimitiveType(self):
+ if not hasattr(self, '_primitivetype'):
+ self.__set_primitiveType()
+ return self._primitiveType
+
+ primitiveType = property(_getPrimitiveType,
+ doc="(readonly) The type of the value as defined "
+ "by the constants in this class.")
+
+ def _getPrimitiveTypeString(self):
+ return self._unitnames[self.primitiveType]
+
+ primitiveTypeString = property(_getPrimitiveTypeString,
+ doc="Name of primitive type of this value.")
+
+ def _getCSSPrimitiveTypeString(self, type):
+ "get TypeString by given type which may be unknown, used by setters"
+ try:
+ return self._unitnames[type]
+ except (IndexError, TypeError):
+ return u'%r (UNKNOWN TYPE)' % type
+
+ def _getNumDim(self, value=None):
+ "Split self._value in numerical and dimension part."
+ if value is None:
+ value = cssutils.helper.normalize(self._value[0])
+
+ try:
+ val, dim = CSSPrimitiveValue._reNumDim.findall(value)[0]
+ except IndexError:
+ val, dim = value, u''
+ try:
+ val = float(val)
+ if val == int(val):
+ val = int(val)
+ except ValueError:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: No float value %r' % self._value[0])
+
+ return val, dim
+
+ def getFloatValue(self, unitType=None):
+ """(DOM) This method is used to get a float value in a
+ specified unit. If this CSS value doesn't contain a float value
+ or can't be converted into the specified unit, a DOMException
+ is raised.
+
+ :param unitType:
+ to get the float value. The unit code can only be a float unit type
+ (i.e. CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, CSS_PX, CSS_CM,
+ CSS_MM, CSS_IN, CSS_PT, CSS_PC, CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS,
+ CSS_S, CSS_HZ, CSS_KHZ, CSS_DIMENSION) or None in which case
+ the current dimension is used.
+
+ :returns:
+ not necessarily a float but some cases just an integer
+ e.g. if the value is ``1px`` it return ``1`` and **not** ``1.0``
+
+ Conversions might return strange values like 1.000000000001
+ """
+ if unitType is not None and unitType not in self._floattypes:
+ raise xml.dom.InvalidAccessErr(
+ u'unitType Parameter is not a float type')
+
+ val, dim = self._getNumDim()
+
+ if unitType is not None and self.primitiveType != unitType:
+ # convert if needed
+ try:
+ val = self._converter[self.primitiveType, unitType](val)
+ except KeyError:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
+ % (self.primitiveTypeString,
+ self._getCSSPrimitiveTypeString(unitType)))
+
+ if val == int(val):
+ val = int(val)
+
+ return val
+
+ def setFloatValue(self, unitType, floatValue):
+ """(DOM) A method to set the float value with a specified unit.
+ If the property attached with this value can not accept the
+ specified unit or the float value, the value will be unchanged and
+ a DOMException will be raised.
+
+ :param unitType:
+ a unit code as defined above. The unit code can only be a float
+ unit type
+ :param floatValue:
+ the new float value which does not have to be a float value but
+ may simple be an int e.g. if setting::
+
+ setFloatValue(CSS_PX, 1)
+
+ :exceptions:
+ - :exc:`~xml.dom.InvalidAccessErr`:
+ Raised if the attached property doesn't
+ support the float value or the unit type.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this property is readonly.
+ """
+ self._checkReadonly()
+ if unitType not in self._floattypes:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: unitType %r is not a float type' %
+ self._getCSSPrimitiveTypeString(unitType))
+ try:
+ val = float(floatValue)
+ except ValueError, e:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: floatValue %r is not a float' %
+ floatValue)
+
+ oldval, dim = self._getNumDim()
+ if self.primitiveType != unitType:
+ # convert if possible
+ try:
+ val = self._converter[unitType, self.primitiveType](val)
+ except KeyError:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
+ % (self.primitiveTypeString,
+ self._getCSSPrimitiveTypeString(unitType)))
+
+ if val == int(val):
+ val = int(val)
+
+ self.cssText = '%s%s' % (val, dim)
+
+ def getStringValue(self):
+ """(DOM) This method is used to get the string value. If the
+ CSS value doesn't contain a string value, a DOMException is raised.
+
+ Some properties (like 'font-family' or 'voice-family')
+ convert a whitespace separated list of idents to a string.
+
+ Only the actual value is returned so e.g. all the following return the
+ actual value ``a``: url(a), attr(a), "a", 'a'
+ """
+ if self.primitiveType not in self._stringtypes:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue %r is not a string type'
+ % self.primitiveTypeString)
+
+ if CSSPrimitiveValue.CSS_ATTR == self.primitiveType:
+ return self._value[0].cssText[5:-1]
+ else:
+ return self._value[0]
+
+ def setStringValue(self, stringType, stringValue):
+ """(DOM) A method to set the string value with the specified
+ unit. If the property attached to this value can't accept the
+ specified unit or the string value, the value will be unchanged and
+ a DOMException will be raised.
+
+ :param stringType:
+ a string code as defined above. The string code can only be a
+ string unit type (i.e. CSS_STRING, CSS_URI, CSS_IDENT, and
+ CSS_ATTR).
+ :param stringValue:
+ the new string value
+ Only the actual value is expected so for (CSS_URI, "a") the
+ new value will be ``url(a)``. For (CSS_STRING, "'a'")
+ the new value will be ``"\\'a\\'"`` as the surrounding ``'`` are
+ not part of the string value
+
+ :exceptions:
+ - :exc:`~xml.dom.InvalidAccessErr`:
+ Raised if the CSS value doesn't contain a
+ string value or if the string value can't be converted into
+ the specified unit.
+
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this property is readonly.
+ """
+ self._checkReadonly()
+ # self not stringType
+ if self.primitiveType not in self._stringtypes:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue %r is not a string type'
+ % self.primitiveTypeString)
+ # given stringType is no StringType
+ if stringType not in self._stringtypes:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: stringType %s is not a string type'
+ % self._getCSSPrimitiveTypeString(stringType))
+
+ if self._primitiveType != stringType:
+ raise xml.dom.InvalidAccessErr(
+ u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
+ % (self.primitiveTypeString,
+ self._getCSSPrimitiveTypeString(stringType)))
+
+ if CSSPrimitiveValue.CSS_STRING == self._primitiveType:
+ self.cssText = cssutils.helper.string(stringValue)
+ elif CSSPrimitiveValue.CSS_URI == self._primitiveType:
+ self.cssText = cssutils.helper.uri(stringValue)
+ elif CSSPrimitiveValue.CSS_ATTR == self._primitiveType:
+ self.cssText = u'attr(%s)' % stringValue
+ else:
+ self.cssText = stringValue
+ self._primitiveType = stringType
+
+ def getCounterValue(self):
+ """(DOM) This method is used to get the Counter value. If
+ this CSS value doesn't contain a counter value, a DOMException
+ is raised. Modification to the corresponding style property
+ can be achieved using the Counter interface.
+
+ **Not implemented.**
+ """
+ if not self.CSS_COUNTER == self.primitiveType:
+ raise xml.dom.InvalidAccessErr(u'Value is not a counter type')
+ # TODO: use Counter class
+ raise NotImplementedError()
+
+ def getRGBColorValue(self):
+ """(DOM) This method is used to get the RGB color. If this
+ CSS value doesn't contain a RGB color value, a DOMException
+ is raised. Modification to the corresponding style property
+ can be achieved using the RGBColor interface.
+ """
+ if self.primitiveType not in self._rbgtypes:
+ raise xml.dom.InvalidAccessErr(u'Value is not a RGBColor value')
+ return RGBColor(self._value[0])
+
+ def getRectValue(self):
+ """(DOM) This method is used to get the Rect value. If this CSS
+ value doesn't contain a rect value, a DOMException is raised.
+ Modification to the corresponding style property can be achieved
+ using the Rect interface.
+
+ **Not implemented.**
+ """
+ if self.primitiveType not in self._recttypes:
+ raise xml.dom.InvalidAccessErr(u'value is not a Rect value')
+ # TODO: use Rect class
+ raise NotImplementedError()
+
+ def _getCssText(self):
+ """Overwrites CSSValue."""
+ return cssutils.ser.do_css_CSSPrimitiveValue(self)
+
+ def _setCssText(self, cssText):
+ """Use CSSValue."""
+ return super(CSSPrimitiveValue, self)._setCssText(cssText)
+
+ cssText = property(_getCssText, _setCssText,
+ doc="A string representation of the current value.")
+
+
+class CSSValueList(CSSValue):
+ """The CSSValueList interface provides the abstraction of an ordered
+ collection of CSS values.
+
+ Some properties allow an empty list into their syntax. In that case,
+ these properties take the none identifier. So, an empty list means
+ that the property has the value none.
+
+ The items in the CSSValueList are accessible via an integral index,
+ starting from 0.
+ """
+ cssValueType = CSSValue.CSS_VALUE_LIST
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """Init a new CSSValueList"""
+ super(CSSValueList, self).__init__(cssText=cssText,
+ parent=parent,
+ readonly=readonly)
+ self._items = []
+
+ def __iter__(self):
+ "CSSValueList is iterable."
+ for item in self.__items():
+ yield item.value
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.cssValueTypeString,
+ self.cssText,
+ self.length,
+ id(self))
+
+ def __items(self):
+ return [item for item in self._seq
+ if isinstance(item.value, CSSValue)]
+
+ def item(self, index):
+ """(DOM) Retrieve a CSSValue by ordinal `index`. The
+ order in this collection represents the order of the values in the
+ CSS style property. If `index` is greater than or equal to the number
+ of values in the list, this returns ``None``.
+ """
+ try:
+ return self.__items()[index].value
+ except IndexError:
+ return None
+
+ length = property(lambda self: len(self.__items()),
+ doc=u"(DOM attribute) The number of CSSValues in the "
+ u"list.")
+
+
+class CSSFunction(CSSPrimitiveValue):
+ """A CSS function value like rect() etc."""
+ _functionName = u'CSSFunction'
+ primitiveType = CSSPrimitiveValue.CSS_UNKNOWN
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """
+ Init a new CSSFunction
+
+ :param cssText:
+ the parsable cssText of the value
+ :param readonly:
+ defaults to False
+ """
+ super(CSSFunction, self).__init__(parent=parent)
+ self._funcType = None
+ self.valid = False
+ self.wellformed = False
+ if cssText is not None:
+ self.cssText = cssText
+ self._readonly = readonly
+
+ def _productiondefinition(self):
+ """Return definition used for parsing."""
+ types = self._prods # rename!
+
+ value = Sequence(PreDef.unary(),
+ Prod(name='PrimitiveValue',
+ match=lambda t, v: t in (types.DIMENSION,
+ types.HASH,
+ types.IDENT,
+ types.NUMBER,
+ types.PERCENTAGE,
+ types.STRING),
+ toSeq=lambda t, tokens: (t[0],
+ CSSPrimitiveValue(t[1]))
+ )
+ )
+ valueOrFunc = Choice(value,
+ # FUNC is actually not in spec but used in e.g. Prince
+ PreDef.function(toSeq=lambda t,
+ tokens: ('FUNCTION',
+ CSSFunction(
+ cssutils.helper.pushtoken(t, tokens))
+ )
+ )
+ )
+ funcProds = Sequence(Prod(name='FUNC',
+ match=lambda t, v: t == types.FUNCTION,
+ toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1]))),
+ Choice(Sequence(valueOrFunc,
+ # more values starting with Comma
+ # should use store where colorType is saved to
+ # define min and may, closure?
+ Sequence(PreDef.comma(),
+ valueOrFunc,
+ minmax=lambda: (0, None)),
+ PreDef.funcEnd(stop=True)),
+ PreDef.funcEnd(stop=True))
+ )
+ return funcProds
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+ # store: colorType, parts
+ wellformed, seq, store, unusedtokens = ProdParser().parse(cssText,
+ self._functionName,
+ self._productiondefinition(),
+ keepS=True)
+ if wellformed:
+ # combine +/- and following CSSPrimitiveValue, remove S
+ newseq = self._tempSeq()
+ i, end = 0, len(seq)
+ while i < end:
+ item = seq[i]
+ if item.type == self._prods.S:
+ pass
+ elif item.value == u'+' or item.value == u'-':
+ i += 1
+ next = seq[i]
+ newval = next.value
+ if isinstance(newval, CSSPrimitiveValue):
+ newval.setFloatValue(newval.primitiveType,
+ float(item.value + str(newval.getFloatValue())))
+ newseq.append(newval, next.type,
+ item.line, item.col)
+ else:
+ # expressions only?
+ newseq.appendItem(item)
+ newseq.appendItem(next)
+ else:
+ newseq.appendItem(item)
+
+ i += 1
+
+ self.wellformed = True
+ self._setSeq(newseq)
+ self._funcType = newseq[0].value
+
+ cssText = property(lambda self: cssutils.ser.do_css_FunctionValue(self),
+ _setCssText)
+
+ funcType = property(lambda self: self._funcType)
+
+
+class RGBColor(CSSFunction):
+ """A CSS color like RGB, RGBA or a simple value like `#000` or `red`."""
+
+ _functionName = u'Function rgb()'
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """
+ Init a new RGBColor
+
+ :param cssText:
+ the parsable cssText of the value
+ :param readonly:
+ defaults to False
+ """
+ super(CSSFunction, self).__init__(parent=parent)
+ self._colorType = None
+ self.valid = False
+ self.wellformed = False
+ if cssText is not None:
+ try:
+ # if it is a Function object
+ cssText = cssText.cssText
+ except AttributeError:
+ pass
+ self.cssText = cssText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.colorType,
+ self.cssText,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+ types = self._prods # rename!
+ valueProd = Prod(name='value',
+ match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE),
+ toSeq=lambda t, v: (CSSPrimitiveValue, CSSPrimitiveValue(v)),
+ toStore='parts'
+ )
+ # COLOR PRODUCTION
+ funccolor = Sequence(Prod(name='FUNC',
+ match=lambda t, v: t == types.FUNCTION and cssutils.helper.normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla('),
+ toSeq=lambda t, v: (t, v),#cssutils.helper.normalize(v)),
+ toStore='colorType'),
+ PreDef.unary(),
+ valueProd,
+ # 2 or 3 more values starting with Comma
+ Sequence(PreDef.comma(),
+ PreDef.unary(),
+ valueProd,
+ minmax=lambda: (2, 3)),
+ PreDef.funcEnd()
+ )
+ colorprods = Choice(funccolor,
+ PreDef.hexcolor('colorType'),
+ Prod(name='named color',
+ match=lambda t, v: t == types.IDENT,
+ toStore='colorType'
+ )
+ )
+ # store: colorType, parts
+ wellformed, seq, store, unusedtokens = ProdParser().parse(cssText,
+ u'RGBColor',
+ colorprods,
+ keepS=True,
+ store={'parts': []})
+
+ if wellformed:
+ self.wellformed = True
+ if store['colorType'].type == self._prods.HASH:
+ self._colorType = 'HEX'
+ elif store['colorType'].type == self._prods.IDENT:
+ self._colorType = 'Named Color'
+ else:
+ self._colorType = store['colorType'].value[:-1]
+ #self._colorType = cssutils.helper.normalize(store['colorType'].value)[:-1]
+
+ self._setSeq(seq)
+
+ cssText = property(lambda self: cssutils.ser.do_css_RGBColor(self),
+ _setCssText)
+
+ colorType = property(lambda self: self._colorType)
+
+
+class CalcValue(CSSFunction):
+ """Calc Function"""
+ _functionName = u'Function calc()'
+
+ def _productiondefinition(self):
+ """Return defintion used for parsing."""
+ types = self._prods # rename!
+
+ def toSeq(t, tokens):
+ "Do not normalize function name!"
+ return t[0], t[1]
+
+ funcProds = Sequence(Prod(name='calc',
+ match=lambda t, v: t == types.FUNCTION,
+ toSeq=toSeq
+ ),
+ Sequence(Choice(Prod(name='nested function',
+ match=lambda t, v: t == self._prods.FUNCTION,
+ toSeq=lambda t, tokens: (CSSFunction._functionName,
+ CSSFunction(cssutils.helper.pushtoken(t,
+ tokens)))
+ ),
+ Prod(name='part',
+ match=lambda t, v: v != u')',
+ toSeq=lambda t, tokens: (t[0], t[1])),
+ ),
+ minmax=lambda: (0, None)),
+ PreDef.funcEnd(stop=True))
+ return funcProds
+
+ def _getCssText(self):
+ return cssutils.ser.do_css_CalcValue(self)
+
+ def _setCssText(self, cssText):
+ return super(CalcValue, self)._setCssText(cssText)
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"A string representation of the current value.")
+
+
+class ExpressionValue(CSSFunction):
+ """Special IE only CSSFunction which may contain *anything*.
+ Used for expressions and ``alpha(opacity=100)`` currently."""
+ _functionName = u'Expression (IE only)'
+
+ def _productiondefinition(self):
+ """Return defintion used for parsing."""
+ types = self._prods # rename!
+
+ def toSeq(t, tokens):
+ "Do not normalize function name!"
+ return t[0], t[1]
+
+ funcProds = Sequence(Prod(name='expression',
+ match=lambda t, v: t == types.FUNCTION,
+ toSeq=toSeq
+ ),
+ Sequence(Choice(Prod(name='nested function',
+ match=lambda t, v: t == self._prods.FUNCTION,
+ toSeq=lambda t, tokens: (ExpressionValue._functionName,
+ ExpressionValue(cssutils.helper.pushtoken(t,
+ tokens)))
+ ),
+ Prod(name='part',
+ match=lambda t, v: v != u')',
+ toSeq=lambda t, tokens: (t[0], t[1])),
+ ),
+ minmax=lambda: (0, None)),
+ PreDef.funcEnd(stop=True))
+ return funcProds
+
+ def _getCssText(self):
+ return cssutils.ser.do_css_ExpressionValue(self)
+
+ def _setCssText(self, cssText):
+ #self._log.warn(u'CSSValue: Unoffial and probably invalid MS value used!')
+ return super(ExpressionValue, self)._setCssText(cssText)
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"A string representation of the current value.")
+
+
+class CSSVariable(CSSValue):
+ """The CSSVariable represents a call to CSS Variable."""
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """Init a new CSSVariable.
+
+ :param cssText:
+ the parsable cssText of the value, e.g. ``var(x)``
+ :param readonly:
+ defaults to False
+ """
+ self._name = None
+ super(CSSVariable, self).__init__(cssText=cssText,
+ parent=parent,
+ readonly=readonly)
+
+ def __repr__(self):
+ return u"cssutils.css.%s(%r)" % (self.__class__.__name__, self.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.name,
+ self.value,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+
+ types = self._prods # rename!
+
+ funcProds = Sequence(Prod(name='var',
+ match=lambda t, v: t == types.FUNCTION
+ ),
+ PreDef.ident(toStore='ident'),
+ PreDef.funcEnd(stop=True))
+
+ # store: name of variable
+ store = {'ident': None}
+ wellformed, seq, store, unusedtokens = ProdParser().parse(cssText,
+ u'CSSVariable',
+ funcProds,
+ keepS=True)
+ if wellformed:
+ self._name = store['ident'].value
+ self._setSeq(seq)
+ self.wellformed = True
+
+ cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self),
+ _setCssText,
+ doc=u"A string representation of the current variable.")
+
+ cssValueType = CSSValue.CSS_VARIABLE
+
+ # TODO: writable? check if var (value) available?
+ name = property(lambda self: self._name)
+
+ def _getValue(self):
+ "Find contained sheet and @variables there"
+ try:
+ variables = self.parent.parent.parentRule.parentStyleSheet.variables
+ except AttributeError:
+ return None
+ else:
+ try:
+ return variables[self.name]
+ except KeyError:
+ return None
+
+ value = property(_getValue)
diff --git a/libs/cssutils/css/cssvariablesdeclaration.py b/libs/cssutils/css/cssvariablesdeclaration.py
new file mode 100755
index 00000000..2b33f670
--- /dev/null
+++ b/libs/cssutils/css/cssvariablesdeclaration.py
@@ -0,0 +1,330 @@
+"""CSSVariablesDeclaration
+http://disruptive-innovations.com/zoo/cssvariables/#mozTocId496530
+"""
+__all__ = ['CSSVariablesDeclaration']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: cssstyledeclaration.py 1819 2009-08-01 20:52:43Z cthedot $'
+
+from cssutils.prodparser import *
+from cssutils.helper import normalize
+from value import PropertyValue
+import cssutils
+import itertools
+import xml.dom
+
+class CSSVariablesDeclaration(cssutils.util._NewBase):
+ """The CSSVariablesDeclaration interface represents a single block of
+ variable declarations.
+ """
+ def __init__(self, cssText=u'', parentRule=None, readonly=False):
+ """
+ :param cssText:
+ Shortcut, sets CSSVariablesDeclaration.cssText
+ :param parentRule:
+ The CSS rule that contains this declaration block or
+ None if this CSSVariablesDeclaration is not attached to a CSSRule.
+ :param readonly:
+ defaults to False
+
+ Format::
+
+ variableset
+ : vardeclaration [ ';' S* vardeclaration ]* S*
+ ;
+
+ vardeclaration
+ : varname ':' S* term
+ ;
+
+ varname
+ : IDENT S*
+ ;
+ """
+ super(CSSVariablesDeclaration, self).__init__()
+ self._parentRule = parentRule
+ self._vars = {}
+ if cssText:
+ self.cssText = cssText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(cssText=%r)" % (self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__,
+ self.length,
+ id(self))
+
+ def __contains__(self, variableName):
+ """Check if a variable is in variable declaration block.
+
+ :param variableName:
+ a string
+ """
+ return normalize(variableName) in self.keys()
+
+ def __getitem__(self, variableName):
+ """Retrieve the value of variable ``variableName`` from this
+ declaration.
+ """
+ return self.getVariableValue(variableName)
+
+ def __setitem__(self, variableName, value):
+ self.setVariable(variableName, value)
+
+ def __delitem__(self, variableName):
+ return self.removeVariable(variableName)
+
+ def __iter__(self):
+ """Iterator of names of set variables."""
+ for name in self.keys():
+ yield name
+
+ def keys(self):
+ """Analoguous to standard dict returns variable names which are set in
+ this declaration."""
+ return self._vars.keys()
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_css_CSSVariablesDeclaration(self)
+
+ def _setCssText(self, cssText):
+ """Setting this attribute will result in the parsing of the new value
+ and resetting of all the properties in the declaration block
+ including the removal or addition of properties.
+
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly or a property is readonly.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+
+ Format::
+
+ variableset
+ : vardeclaration [ ';' S* vardeclaration ]*
+ ;
+
+ vardeclaration
+ : varname ':' S* term
+ ;
+
+ varname
+ : IDENT S*
+ ;
+
+ expr
+ : [ VARCALL | term ] [ operator [ VARCALL | term ] ]*
+ ;
+
+ """
+ self._checkReadonly()
+
+ vardeclaration = Sequence(
+ PreDef.ident(),
+ PreDef.char(u':', u':', toSeq=False),
+ #PreDef.S(toSeq=False, optional=True),
+ Prod(name=u'term', match=lambda t, v: True,
+ toSeq=lambda t, tokens: (u'value',
+ PropertyValue(itertools.chain([t],
+ tokens),
+ parent=self)
+ )
+ )
+ )
+ prods = Sequence(vardeclaration,
+ Sequence(PreDef.S(optional=True),
+ PreDef.char(u';', u';', toSeq=False),
+ PreDef.S(optional=True),
+ vardeclaration,
+ minmax=lambda: (0, None)),
+ PreDef.S(optional=True),
+ PreDef.char(u';', u';', toSeq=False, optional=True)
+ )
+ # parse
+ wellformed, seq, store, notused = \
+ ProdParser().parse(cssText,
+ u'CSSVariableDeclaration',
+ prods)
+ if wellformed:
+ newseq = self._tempSeq()
+ newvars = {}
+
+ # seq contains only name: value pairs plus comments etc
+ nameitem = None
+ for item in seq:
+ if u'IDENT' == item.type:
+ nameitem = item
+ elif u'value' == item.type:
+ nname = normalize(nameitem.value)
+ if nname in newvars:
+ # replace var with same name
+ for i, it in enumerate(newseq):
+ if normalize(it.value[0]) == nname:
+ newseq.replace(i,
+ (nameitem.value, item.value),
+ 'var',
+ nameitem.line, nameitem.col)
+ else:
+ # saved non normalized name for reserialization
+ newseq.append((nameitem.value, item.value),
+ 'var',
+ nameitem.line, nameitem.col)
+
+# newseq.append((nameitem.value, item.value),
+# 'var',
+# nameitem.line, nameitem.col)
+
+ newvars[nname] = item.value
+
+ else:
+ newseq.appendItem(item)
+
+ self._setSeq(newseq)
+ self._vars = newvars
+ self.wellformed = True
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) A parsable textual representation of the declaration "
+ u"block excluding the surrounding curly braces.")
+
+ def _setParentRule(self, parentRule):
+ self._parentRule = parentRule
+
+ parentRule = property(lambda self: self._parentRule, _setParentRule,
+ doc=u"(DOM) The CSS rule that contains this"
+ u" declaration block or None if this block"
+ u" is not attached to a CSSRule.")
+
+ def getVariableValue(self, variableName):
+ """Used to retrieve the value of a variable if it has been explicitly
+ set within this variable declaration block.
+
+ :param variableName:
+ The name of the variable.
+ :returns:
+ the value of the variable if it has been explicitly set in this
+ variable declaration block. Returns the empty string if the
+ variable has not been set.
+ """
+ try:
+ return self._vars[normalize(variableName)].cssText
+ except KeyError, e:
+ return u''
+
+ def removeVariable(self, variableName):
+ """Used to remove a variable if it has been explicitly set within this
+ variable declaration block.
+
+ :param variableName:
+ The name of the variable.
+ :returns:
+ the value of the variable if it has been explicitly set for this
+ variable declaration block. Returns the empty string if the
+ variable has not been set.
+
+ :exceptions:
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly is readonly.
+ """
+ normalname = variableName
+ try:
+ r = self._vars[normalname]
+ except KeyError, e:
+ return u''
+ else:
+ self.seq._readonly = False
+ if normalname in self._vars:
+ for i, x in enumerate(self.seq):
+ if x.value[0] == variableName:
+ del self.seq[i]
+ self.seq._readonly = True
+ del self._vars[normalname]
+
+ return r.cssText
+
+ def setVariable(self, variableName, value):
+ """Used to set a variable value within this variable declaration block.
+
+ :param variableName:
+ The name of the CSS variable.
+ :param value:
+ The new value of the variable, may also be a PropertyValue object.
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified value has a syntax error and is
+ unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this declaration is readonly or the property is
+ readonly.
+ """
+ self._checkReadonly()
+
+ # check name
+ wellformed, seq, store, unused = \
+ ProdParser().parse(normalize(variableName),
+ u'variableName',
+ Sequence(PreDef.ident()))
+ if not wellformed:
+ self._log.error(u'Invalid variableName: %r: %r'
+ % (variableName, value))
+ else:
+ # check value
+ if isinstance(value, PropertyValue):
+ v = value
+ else:
+ v = PropertyValue(cssText=value, parent=self)
+
+ if not v.wellformed:
+ self._log.error(u'Invalid variable value: %r: %r'
+ % (variableName, value))
+ else:
+ # update seq
+ self.seq._readonly = False
+
+ variableName = normalize(variableName)
+
+ if variableName in self._vars:
+ for i, x in enumerate(self.seq):
+ if x.value[0] == variableName:
+ self.seq.replace(i,
+ [variableName, v],
+ x.type,
+ x.line,
+ x.col)
+ break
+ else:
+ self.seq.append([variableName, v], 'var')
+ self.seq._readonly = True
+ self._vars[variableName] = v
+
+ def item(self, index):
+ """Used to retrieve the variables that have been explicitly set in
+ this variable declaration block. The order of the variables
+ retrieved using this method does not have to be the order in which
+ they were set. This method can be used to iterate over all variables
+ in this variable declaration block.
+
+ :param index:
+ of the variable name to retrieve, negative values behave like
+ negative indexes on Python lists, so -1 is the last element
+
+ :returns:
+ The name of the variable at this ordinal position. The empty
+ string if no variable exists at this position.
+ """
+ try:
+ return self.keys()[index]
+ except IndexError:
+ return u''
+
+ length = property(lambda self: len(self._vars),
+ doc=u"The number of variables that have been explicitly set in this"
+ u" variable declaration block. The range of valid indices is 0"
+ u" to length-1 inclusive.")
diff --git a/libs/cssutils/css/cssvariablesrule.py b/libs/cssutils/css/cssvariablesrule.py
new file mode 100755
index 00000000..ce5335bf
--- /dev/null
+++ b/libs/cssutils/css/cssvariablesrule.py
@@ -0,0 +1,198 @@
+"""CSSVariables implements (and only partly) experimental
+`CSS Variables `_
+"""
+__all__ = ['CSSVariablesRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: cssfontfacerule.py 1818 2009-07-30 21:39:00Z cthedot $'
+
+from cssvariablesdeclaration import CSSVariablesDeclaration
+import cssrule
+import cssutils
+import xml.dom
+
+class CSSVariablesRule(cssrule.CSSRule):
+ """
+ The CSSVariablesRule interface represents a @variables rule within a CSS
+ style sheet. The @variables rule is used to specify variables.
+
+ cssutils uses a :class:`~cssutils.css.CSSVariablesDeclaration` to
+ represent the variables.
+
+ Format::
+
+ variables
+ VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S*
+ variableset* '}' S*
+ ;
+
+ for variableset see :class:`cssutils.css.CSSVariablesDeclaration`
+
+ **Media are not implemented. Reason is that cssutils is using CSS
+ variables in a kind of preprocessing and therefor no media information
+ is available at this stage. For now do not use media!**
+
+ Example::
+
+ @variables {
+ CorporateLogoBGColor: #fe8d12;
+ }
+
+ div.logoContainer {
+ background-color: var(CorporateLogoBGColor);
+ }
+ """
+ def __init__(self, mediaText=None, variables=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ If readonly allows setting of properties in constructor only.
+ """
+ super(CSSVariablesRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+ self._atkeyword = u'@variables'
+
+ # dummy
+ self._media = cssutils.stylesheets.MediaList(mediaText,
+ readonly=readonly)
+
+ if variables:
+ self.variables = variables
+ else:
+ self.variables = CSSVariablesDeclaration(parentRule=self)
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ return u"cssutils.css.%s(mediaText=%r, variables=%r)" % (
+ self.__class__.__name__,
+ self._media.mediaText,
+ self.variables.cssText)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self._media.mediaText,
+ self.variables.cssText,
+ self.valid,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_CSSVariablesRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+
+ Format::
+
+ variables
+ : VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S*
+ variableset* '}' S*
+ ;
+
+ variableset
+ : LBRACE S* vardeclaration [ ';' S* vardeclaration ]* '}' S*
+ ;
+ """
+ super(CSSVariablesRule, self)._setCssText(cssText)
+
+ tokenizer = self._tokenize2(cssText)
+ attoken = self._nexttoken(tokenizer, None)
+ if self._type(attoken) != self._prods.VARIABLES_SYM:
+ self._log.error(u'CSSVariablesRule: No CSSVariablesRule found: %s' %
+ self._valuestr(cssText),
+ error=xml.dom.InvalidModificationErr)
+ else:
+ newVariables = CSSVariablesDeclaration(parentRule=self)
+ ok = True
+
+ beforetokens, brace = self._tokensupto2(tokenizer,
+ blockstartonly=True,
+ separateEnd=True)
+ if self._tokenvalue(brace) != u'{':
+ ok = False
+ self._log.error(u'CSSVariablesRule: No start { of variable '
+ u'declaration found: %r'
+ % self._valuestr(cssText), brace)
+
+ # parse stuff before { which should be comments and S only
+ new = {'wellformed': True}
+ newseq = self._tempSeq()#[]
+
+ beforewellformed, expected = self._parse(expected=':',
+ seq=newseq, tokenizer=self._tokenize2(beforetokens),
+ productions={})
+ ok = ok and beforewellformed and new['wellformed']
+
+ variablestokens, braceorEOFtoken = self._tokensupto2(tokenizer,
+ blockendonly=True,
+ separateEnd=True)
+
+ val, type_ = self._tokenvalue(braceorEOFtoken), \
+ self._type(braceorEOFtoken)
+ if val != u'}' and type_ != 'EOF':
+ ok = False
+ self._log.error(u'CSSVariablesRule: No "}" after variables '
+ u'declaration found: %r'
+ % self._valuestr(cssText))
+
+ nonetoken = self._nexttoken(tokenizer)
+ if nonetoken:
+ ok = False
+ self._log.error(u'CSSVariablesRule: Trailing content found.',
+ token=nonetoken)
+
+ if 'EOF' == type_:
+ # add again as variables needs it
+ variablestokens.append(braceorEOFtoken)
+ # SET but may raise:
+ newVariables.cssText = variablestokens
+
+ if ok:
+ # contains probably comments only upto {
+ self._setSeq(newseq)
+ self.variables = newVariables
+
+ cssText = property(_getCssText, _setCssText,
+ doc=u"(DOM) The parsable textual representation of this "
+ u"rule.")
+
+ media = property(doc=u"NOT IMPLEMENTED! As cssutils resolves variables "\
+ u"during serializing media information is lost.")
+
+ def _setVariables(self, variables):
+ """
+ :param variables:
+ a CSSVariablesDeclaration or string
+ """
+ self._checkReadonly()
+ if isinstance(variables, basestring):
+ self._variables = CSSVariablesDeclaration(cssText=variables,
+ parentRule=self)
+ else:
+ variables._parentRule = self
+ self._variables = variables
+
+ variables = property(lambda self: self._variables, _setVariables,
+ doc=u"(DOM) The variables of this rule set, a "
+ u":class:`cssutils.css.CSSVariablesDeclaration`.")
+
+ type = property(lambda self: self.VARIABLES_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ valid = property(lambda self: True, doc='NOT IMPLEMTED REALLY (TODO)')
+
+ # constant but needed:
+ wellformed = property(lambda self: True)
diff --git a/libs/cssutils/css/marginrule.py b/libs/cssutils/css/marginrule.py
new file mode 100755
index 00000000..0c789fad
--- /dev/null
+++ b/libs/cssutils/css/marginrule.py
@@ -0,0 +1,215 @@
+"""MarginRule implements DOM Level 2 CSS MarginRule."""
+__all__ = ['MarginRule']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.prodparser import *
+from cssstyledeclaration import CSSStyleDeclaration
+import cssrule
+import cssutils
+import xml.dom
+
+class MarginRule(cssrule.CSSRule):
+ """
+ A margin at-rule consists of an ATKEYWORD that identifies the margin box
+ (e.g. '@top-left') and a block of declarations (said to be in the margin
+ context).
+
+ Format::
+
+ margin :
+ margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ ;
+
+ margin_sym :
+ TOPLEFTCORNER_SYM |
+ TOPLEFT_SYM |
+ TOPCENTER_SYM |
+ TOPRIGHT_SYM |
+ TOPRIGHTCORNER_SYM |
+ BOTTOMLEFTCORNER_SYM |
+ BOTTOMLEFT_SYM |
+ BOTTOMCENTER_SYM |
+ BOTTOMRIGHT_SYM |
+ BOTTOMRIGHTCORNER_SYM |
+ LEFTTOP_SYM |
+ LEFTMIDDLE_SYM |
+ LEFTBOTTOM_SYM |
+ RIGHTTOP_SYM |
+ RIGHTMIDDLE_SYM |
+ RIGHTBOTTOM_SYM
+ ;
+
+ e.g.::
+
+ @top-left {
+ content: "123";
+ }
+ """
+ margins = ['@top-left-corner',
+ '@top-left',
+ '@top-center',
+ '@top-right',
+ '@top-right-corner',
+ '@bottom-left-corner',
+ '@bottom-left',
+ '@bottom-center',
+ '@bottom-right',
+ '@bottom-right-corner',
+ '@left-top',
+ '@left-middle',
+ '@left-bottom',
+ '@right-top',
+ '@right-middle',
+ '@right-bottom'
+ ]
+
+ def __init__(self, margin=None, style=None, parentRule=None,
+ parentStyleSheet=None, readonly=False):
+ """
+ :param atkeyword:
+ The margin area, e.g. '@top-left' for this rule
+ :param style:
+ CSSStyleDeclaration for this MarginRule
+ """
+ super(MarginRule, self).__init__(parentRule=parentRule,
+ parentStyleSheet=parentStyleSheet)
+
+ self._atkeyword = self._keyword = None
+
+ if margin:
+ self.margin = margin
+
+ if style:
+ self.style = style
+ else:
+ self.style = CSSStyleDeclaration(parentRule=self)
+
+ self._readonly = readonly
+
+ def _setMargin(self, margin):
+ """Check if new keyword fits the rule it is used for."""
+ n = self._normalize(margin)
+
+ if n not in MarginRule.margins:
+ self._log.error(u'Invalid margin @keyword for this %s rule: %r' %
+ (self.margin, margin),
+ error=xml.dom.InvalidModificationErr)
+
+ else:
+ self._atkeyword = n
+ self._keyword = margin
+
+ margin = property(lambda self: self._atkeyword, _setMargin,
+ doc=u"Margin area of parent CSSPageRule. "
+ u"`margin` and `atkeyword` are both normalized "
+ u"@keyword of the @rule.")
+
+ atkeyword = margin
+
+ def __repr__(self):
+ return u"cssutils.css.%s(margin=%r, style=%r)" % (self.__class__.__name__,
+ self.margin,
+ self.style.cssText)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.margin,
+ self.style.cssText,
+ id(self))
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_MarginRule(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ Raised if the specified CSS string value represents a different
+ type of rule than the current one.
+ - :exc:`~xml.dom.HierarchyRequestErr`:
+ Raised if the rule cannot be inserted at this point in the
+ style sheet.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ super(MarginRule, self)._setCssText(cssText)
+
+ # TEMP: all style tokens are saved in store to fill styledeclaration
+ # TODO: resolve when all generators
+ styletokens = Prod(name='styletokens',
+ match=lambda t, v: v != u'}',
+ #toSeq=False,
+ toStore='styletokens',
+ storeToken=True
+ )
+
+ prods = Sequence(Prod(name='@ margin',
+ match=lambda t, v:
+ t == 'ATKEYWORD' and
+ self._normalize(v) in MarginRule.margins,
+ toStore='margin'
+ # TODO?
+ #, exception=xml.dom.InvalidModificationErr
+ ),
+ PreDef.char('OPEN', u'{'),
+ Sequence(Choice(PreDef.unknownrule(toStore='@'),
+ styletokens),
+ minmax=lambda: (0, None)
+ ),
+ PreDef.char('CLOSE', u'}', stopAndKeep=True)
+ )
+ # parse
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ u'MarginRule',
+ prods)
+
+ if ok:
+ # TODO: use seq for serializing instead of fixed stuff?
+ self._setSeq(seq)
+
+ if 'margin' in store:
+ # may raise:
+ self.margin = store['margin'].value
+ else:
+ self._log.error(u'No margin @keyword for this %s rule' %
+ self.margin,
+ error=xml.dom.InvalidModificationErr)
+
+ # new empty style
+ self.style = CSSStyleDeclaration(parentRule=self)
+
+ if 'styletokens' in store:
+ # may raise:
+ self.style.cssText = store['styletokens']
+
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc=u"(DOM) The parsable textual representation.")
+
+ def _setStyle(self, style):
+ """
+ :param style: A string or CSSStyleDeclaration which replaces the
+ current style object.
+ """
+ self._checkReadonly()
+ if isinstance(style, basestring):
+ self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
+ else:
+ style._parentRule = self
+ self._style = style
+
+ style = property(lambda self: self._style, _setStyle,
+ doc=u"(DOM) The declaration-block of this rule set.")
+
+ type = property(lambda self: self.MARGIN_RULE,
+ doc=u"The type of this rule, as defined by a CSSRule "
+ u"type constant.")
+
+ wellformed = property(lambda self: bool(self.atkeyword))
+
\ No newline at end of file
diff --git a/libs/cssutils/css/property.py b/libs/cssutils/css/property.py
new file mode 100755
index 00000000..fad240c0
--- /dev/null
+++ b/libs/cssutils/css/property.py
@@ -0,0 +1,510 @@
+"""Property is a single CSS property in a CSSStyleDeclaration."""
+__all__ = ['Property']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.helper import Deprecated
+from value import PropertyValue
+import cssutils
+import xml.dom
+
+class Property(cssutils.util.Base):
+ """A CSS property in a StyleDeclaration of a CSSStyleRule (cssutils).
+
+ Format::
+
+ property = name
+ : IDENT S*
+ ;
+
+ expr = value
+ : term [ operator term ]*
+ ;
+ term
+ : unary_operator?
+ [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
+ ANGLE S* | TIME S* | FREQ S* | function ]
+ | STRING S* | IDENT S* | URI S* | hexcolor
+ ;
+ function
+ : FUNCTION S* expr ')' S*
+ ;
+ /*
+ * There is a constraint on the color that it must
+ * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
+ * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
+ */
+ hexcolor
+ : HASH S*
+ ;
+
+ prio
+ : IMPORTANT_SYM S*
+ ;
+
+ """
+ def __init__(self, name=None, value=None, priority=u'',
+ _mediaQuery=False, parent=None):
+ """
+ :param name:
+ a property name string (will be normalized)
+ :param value:
+ a property value string
+ :param priority:
+ an optional priority string which currently must be u'',
+ u'!important' or u'important'
+ :param _mediaQuery:
+ if ``True`` value is optional (used by MediaQuery)
+ :param parent:
+ the parent object, normally a
+ :class:`cssutils.css.CSSStyleDeclaration`
+ """
+ super(Property, self).__init__()
+ self.seqs = [[], None, []]
+ self.wellformed = False
+ self._mediaQuery = _mediaQuery
+ self.parent = parent
+
+ self.__nametoken = None
+ self._name = u''
+ self._literalname = u''
+ self.seqs[1] = PropertyValue(parent=self)
+ if name:
+ self.name = name
+ self.propertyValue = value
+
+ self._priority = u''
+ self._literalpriority = u''
+ if priority:
+ self.priority = priority
+
+ def __repr__(self):
+ return u"cssutils.css.%s(name=%r, value=%r, priority=%r)" % (
+ self.__class__.__name__,
+ self.literalname,
+ self.propertyValue.cssText,
+ self.priority)
+
+ def __str__(self):
+ return u"<%s.%s object name=%r value=%r priority=%r valid=%r at 0x%x>" \
+ % (self.__class__.__module__,
+ self.__class__.__name__,
+ self.name,
+ self.propertyValue.cssText,
+ self.priority,
+ self.valid,
+ id(self))
+
+ def _isValidating(self):
+ """Return True if validation is enabled."""
+ try:
+ return self.parent.validating
+ except AttributeError:
+ # default (no parent)
+ return True
+
+ def _getCssText(self):
+ """Return serialized property cssText."""
+ return cssutils.ser.do_Property(self)
+
+ def _setCssText(self, cssText):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error and
+ is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if the rule is readonly.
+ """
+ # check and prepare tokenlists for setting
+ tokenizer = self._tokenize2(cssText)
+ nametokens = self._tokensupto2(tokenizer, propertynameendonly=True)
+ if nametokens:
+ wellformed = True
+
+ valuetokens = self._tokensupto2(tokenizer,
+ propertyvalueendonly=True)
+ prioritytokens = self._tokensupto2(tokenizer,
+ propertypriorityendonly=True)
+
+ if self._mediaQuery and not valuetokens:
+ # MediaQuery may consist of name only
+ self.name = nametokens
+ self.propertyValue = None
+ self.priority = None
+ return
+
+ # remove colon from nametokens
+ colontoken = nametokens.pop()
+ if self._tokenvalue(colontoken) != u':':
+ wellformed = False
+ self._log.error(u'Property: No ":" after name found: %s' %
+ self._valuestr(cssText), colontoken)
+ elif not nametokens:
+ wellformed = False
+ self._log.error(u'Property: No property name found: %s' %
+ self._valuestr(cssText), colontoken)
+
+ if valuetokens:
+ if self._tokenvalue(valuetokens[-1]) == u'!':
+ # priority given, move "!" to prioritytokens
+ prioritytokens.insert(0, valuetokens.pop(-1))
+ else:
+ wellformed = False
+ self._log.error(u'Property: No property value found: %s' %
+ self._valuestr(cssText), colontoken)
+
+ if wellformed:
+ self.wellformed = True
+ self.name = nametokens
+ self.propertyValue = valuetokens
+ self.priority = prioritytokens
+
+ # also invalid values are set!
+
+ if self._isValidating():
+ self.validate()
+
+ else:
+ self._log.error(u'Property: No property name found: %s' %
+ self._valuestr(cssText))
+
+ cssText = property(fget=_getCssText, fset=_setCssText,
+ doc="A parsable textual representation.")
+
+ def _setName(self, name):
+ """
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified name has a syntax error and is
+ unparsable.
+ """
+ # for closures: must be a mutable
+ new = {'literalname': None,
+ 'wellformed': True}
+
+ def _ident(expected, seq, token, tokenizer=None):
+ # name
+ if 'name' == expected:
+ new['literalname'] = self._tokenvalue(token).lower()
+ seq.append(new['literalname'])
+ return 'EOF'
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Property: Unexpected ident.', token)
+ return expected
+
+ newseq = []
+ wellformed, expected = self._parse(expected='name',
+ seq=newseq,
+ tokenizer=self._tokenize2(name),
+ productions={'IDENT': _ident})
+ wellformed = wellformed and new['wellformed']
+
+ # post conditions
+ # define a token for error logging
+ if isinstance(name, list):
+ token = name[0]
+ self.__nametoken = token
+ else:
+ token = None
+
+ if not new['literalname']:
+ wellformed = False
+ self._log.error(u'Property: No name found: %s' %
+ self._valuestr(name), token=token)
+
+ if wellformed:
+ self.wellformed = True
+ self._literalname = new['literalname']
+ self._name = self._normalize(self._literalname)
+ self.seqs[0] = newseq
+
+ # validate
+ if self._isValidating() and self._name not in cssutils.profile.knownNames:
+ # self.valid = False
+ self._log.warn(u'Property: Unknown Property name.',
+ token=token, neverraise=True)
+ else:
+ pass
+# self.valid = True
+# if self.propertyValue:
+# self.propertyValue._propertyName = self._name
+# #self.valid = self.propertyValue.valid
+ else:
+ self.wellformed = False
+
+ name = property(lambda self: self._name, _setName,
+ doc="Name of this property.")
+
+ literalname = property(lambda self: self._literalname,
+ doc="Readonly literal (not normalized) name "
+ "of this property")
+
+ def _setPropertyValue(self, cssText):
+ """
+ See css.PropertyValue
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ (according to the attached property) or is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ TODO: Raised if the specified CSS string value represents a different
+ type of values than the values allowed by the CSS property.
+ """
+ if self._mediaQuery and not cssText:
+ self.seqs[1] = PropertyValue(parent=self)
+ else:
+ self.seqs[1].cssText = cssText
+ self.wellformed = self.wellformed and self.seqs[1].wellformed
+
+ propertyValue = property(lambda self: self.seqs[1],
+ _setPropertyValue,
+ doc=u"(cssutils) PropertyValue object of property")
+
+
+ def _getValue(self):
+ if self.propertyValue:
+ # value without comments
+ return self.propertyValue.value
+ else:
+ return u''
+
+ def _setValue(self, value):
+ self._setPropertyValue(value)
+
+ value = property(_getValue, _setValue,
+ doc="The textual value of this Properties propertyValue.")
+
+ def _setPriority(self, priority):
+ """
+ priority
+ a string, currently either u'', u'!important' or u'important'
+
+ Format::
+
+ prio
+ : IMPORTANT_SYM S*
+ ;
+
+ "!"{w}"important" {return IMPORTANT_SYM;}
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified priority has a syntax error and is
+ unparsable.
+ In this case a priority not equal to None, "" or "!{w}important".
+ As CSSOM defines CSSStyleDeclaration.getPropertyPriority resulting
+ in u'important' this value is also allowed to set a Properties
+ priority
+ """
+ if self._mediaQuery:
+ self._priority = u''
+ self._literalpriority = u''
+ if priority:
+ self._log.error(u'Property: No priority in a MediaQuery - '
+ u'ignored.')
+ return
+
+ if isinstance(priority, basestring) and\
+ u'important' == self._normalize(priority):
+ priority = u'!%s' % priority
+
+ # for closures: must be a mutable
+ new = {'literalpriority': u'',
+ 'wellformed': True}
+
+ def _char(expected, seq, token, tokenizer=None):
+ # "!"
+ val = self._tokenvalue(token)
+ if u'!' == expected == val:
+ seq.append(val)
+ return 'important'
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Property: Unexpected char.', token)
+ return expected
+
+ def _ident(expected, seq, token, tokenizer=None):
+ # "important"
+ val = self._tokenvalue(token)
+ if 'important' == expected:
+ new['literalpriority'] = val
+ seq.append(val)
+ return 'EOF'
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Property: Unexpected ident.', token)
+ return expected
+
+ newseq = []
+ wellformed, expected = self._parse(expected='!',
+ seq=newseq,
+ tokenizer=self._tokenize2(priority),
+ productions={'CHAR': _char,
+ 'IDENT': _ident})
+ wellformed = wellformed and new['wellformed']
+
+ # post conditions
+ if priority and not new['literalpriority']:
+ wellformed = False
+ self._log.info(u'Property: Invalid priority: %s' %
+ self._valuestr(priority))
+
+ if wellformed:
+ self.wellformed = self.wellformed and wellformed
+ self._literalpriority = new['literalpriority']
+ self._priority = self._normalize(self.literalpriority)
+ self.seqs[2] = newseq
+ # validate priority
+ if self._priority not in (u'', u'important'):
+ self._log.error(u'Property: No CSS priority value: %s' %
+ self._priority)
+
+ priority = property(lambda self: self._priority, _setPriority,
+ doc="Priority of this property.")
+
+ literalpriority = property(lambda self: self._literalpriority,
+ doc="Readonly literal (not normalized) priority of this property")
+
+ def _setParent(self, parent):
+ self._parent = parent
+
+ parent = property(lambda self: self._parent, _setParent,
+ doc="The Parent Node (normally a CSSStyledeclaration) of this "
+ "Property")
+
+ def validate(self):
+ """Validate value against `profiles` which are checked dynamically.
+ properties in e.g. @font-face rules are checked against
+ ``cssutils.profile.CSS3_FONT_FACE`` only.
+
+ For each of the following cases a message is reported:
+
+ - INVALID (so the property is known but not valid)
+ ``ERROR Property: Invalid value for "{PROFILE-1[/PROFILE-2...]"
+ property: ...``
+
+ - VALID but not in given profiles or defaultProfiles
+ ``WARNING Property: Not valid for profile "{PROFILE-X}" but valid
+ "{PROFILE-Y}" property: ...``
+
+ - VALID in current profile
+ ``DEBUG Found valid "{PROFILE-1[/PROFILE-2...]" property...``
+
+ - UNKNOWN property
+ ``WARNING Unknown Property name...`` is issued
+
+ so for example::
+
+ cssutils.log.setLevel(logging.DEBUG)
+ parser = cssutils.CSSParser()
+ s = parser.parseString('''body {
+ unknown-property: x;
+ color: 4;
+ color: rgba(1,2,3,4);
+ color: red
+ }''')
+
+ # Log output:
+
+ WARNING Property: Unknown Property name. [2:9: unknown-property]
+ ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
+ DEBUG Property: Found valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color]
+ DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
+
+
+ and when setting an explicit default profile::
+
+ cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2
+ s = parser.parseString('''body {
+ unknown-property: x;
+ color: 4;
+ color: rgba(1,2,3,4);
+ color: red
+ }''')
+
+ # Log output:
+
+ WARNING Property: Unknown Property name. [2:9: unknown-property]
+ ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
+ WARNING Property: Not valid for profile "CSS Level 2.1" but valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color]
+ DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
+ """
+ valid = False
+
+ profiles = None
+ try:
+ # if @font-face use that profile
+ rule = self.parent.parentRule
+ except AttributeError:
+ pass
+ else:
+ if rule is not None:
+ if rule.type == rule.FONT_FACE_RULE:
+ profiles = [cssutils.profile.CSS3_FONT_FACE]
+ #TODO: same for @page
+
+ if self.name and self.value:
+
+ cv = self.propertyValue
+ # TODO
+# if cv.cssValueType == cv.CSS_VARIABLE and not cv.value:
+# # TODO: false alarms too!
+# cssutils.log.warn(u'No value for variable "%s" found, keeping '
+# u'variable.' % cv.name, neverraise=True)
+
+ if self.name in cssutils.profile.knownNames:
+ # add valid, matching, validprofiles...
+ valid, matching, validprofiles = \
+ cssutils.profile.validateWithProfile(self.name,
+ self.value,
+ profiles)
+
+ if not valid:
+ self._log.error(u'Property: Invalid value for '
+ u'"%s" property: %s'
+ % (u'/'.join(validprofiles), self.value),
+ token=self.__nametoken,
+ neverraise=True)
+
+ # TODO: remove logic to profiles!
+ elif valid and not matching:#(profiles and profiles not in validprofiles):
+ if not profiles:
+ notvalidprofiles = u'/'.join(cssutils.profile.defaultProfiles)
+ else:
+ notvalidprofiles = profiles
+ self._log.warn(u'Property: Not valid for profile "%s" '
+ u'but valid "%s" value: %s '
+ % (notvalidprofiles, u'/'.join(validprofiles),
+ self.value),
+ token = self.__nametoken,
+ neverraise=True)
+ valid = False
+
+ elif valid:
+ self._log.debug(u'Property: Found valid "%s" value: %s'
+ % (u'/'.join(validprofiles), self.value),
+ token = self.__nametoken,
+ neverraise=True)
+
+ if self._priority not in (u'', u'important'):
+ valid = False
+
+ return valid
+
+ valid = property(validate, doc=u"Check if value of this property is valid "
+ u"in the properties context.")
+
+
+ @Deprecated(u'Use ``property.propertyValue`` instead.')
+ def _getCSSValue(self):
+ return self.propertyValue
+
+ @Deprecated(u'Use ``property.propertyValue`` instead.')
+ def _setCSSValue(self, cssText):
+ self._setPropertyValue(cssText)
+
+ cssValue = property(_getCSSValue, _setCSSValue,
+ doc="(DEPRECATED) Use ``property.propertyValue`` instead.")
diff --git a/libs/cssutils/css/selector.py b/libs/cssutils/css/selector.py
new file mode 100755
index 00000000..87840a0d
--- /dev/null
+++ b/libs/cssutils/css/selector.py
@@ -0,0 +1,813 @@
+"""Selector is a single Selector of a CSSStyleRule SelectorList.
+Partly implements http://www.w3.org/TR/css3-selectors/.
+
+TODO
+ - .contains(selector)
+ - .isSubselector(selector)
+"""
+__all__ = ['Selector']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.helper import Deprecated
+from cssutils.util import _SimpleNamespaces
+import cssutils
+import xml.dom
+
+class Selector(cssutils.util.Base2):
+ """
+ (cssutils) a single selector in a :class:`~cssutils.css.SelectorList`
+ of a :class:`~cssutils.css.CSSStyleRule`.
+
+ Format::
+
+ # implemented in SelectorList
+ selectors_group
+ : selector [ COMMA S* selector ]*
+ ;
+
+ selector
+ : simple_selector_sequence [ combinator simple_selector_sequence ]*
+ ;
+
+ combinator
+ /* combinators can be surrounded by white space */
+ : PLUS S* | GREATER S* | TILDE S* | S+
+ ;
+
+ simple_selector_sequence
+ : [ type_selector | universal ]
+ [ HASH | class | attrib | pseudo | negation ]*
+ | [ HASH | class | attrib | pseudo | negation ]+
+ ;
+
+ type_selector
+ : [ namespace_prefix ]? element_name
+ ;
+
+ namespace_prefix
+ : [ IDENT | '*' ]? '|'
+ ;
+
+ element_name
+ : IDENT
+ ;
+
+ universal
+ : [ namespace_prefix ]? '*'
+ ;
+
+ class
+ : '.' IDENT
+ ;
+
+ attrib
+ : '[' S* [ namespace_prefix ]? IDENT S*
+ [ [ PREFIXMATCH |
+ SUFFIXMATCH |
+ SUBSTRINGMATCH |
+ '=' |
+ INCLUDES |
+ DASHMATCH ] S* [ IDENT | STRING ] S*
+ ]? ']'
+ ;
+
+ pseudo
+ /* '::' starts a pseudo-element, ':' a pseudo-class */
+ /* Exceptions: :first-line, :first-letter, :before and :after. */
+ /* Note that pseudo-elements are restricted to one per selector and */
+ /* occur only in the last simple_selector_sequence. */
+ : ':' ':'? [ IDENT | functional_pseudo ]
+ ;
+
+ functional_pseudo
+ : FUNCTION S* expression ')'
+ ;
+
+ expression
+ /* In CSS3, the expressions are identifiers, strings, */
+ /* or of the form "an+b" */
+ : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
+ ;
+
+ negation
+ : NOT S* negation_arg S* ')'
+ ;
+
+ negation_arg
+ : type_selector | universal | HASH | class | attrib | pseudo
+ ;
+
+ """
+ def __init__(self, selectorText=None, parent=None,
+ readonly=False):
+ """
+ :Parameters:
+ selectorText
+ initial value of this selector
+ parent
+ a SelectorList
+ readonly
+ default to False
+ """
+ super(Selector, self).__init__()
+
+ self.__namespaces = _SimpleNamespaces(log=self._log)
+ self._element = None
+ self._parent = parent
+ self._specificity = (0, 0, 0, 0)
+
+ if selectorText:
+ self.selectorText = selectorText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ if self.__getNamespaces():
+ st = (self.selectorText, self._getUsedNamespaces())
+ else:
+ st = self.selectorText
+ return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__,
+ st)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.selectorText,
+ self.specificity,
+ self._getUsedNamespaces(),
+ id(self))
+
+ def _getUsedUris(self):
+ "Return list of actually used URIs in this Selector."
+ uris = set()
+ for item in self.seq:
+ type_, val = item.type, item.value
+ if type_.endswith(u'-selector') or type_ == u'universal' and \
+ isinstance(val, tuple) and val[0] not in (None, u'*'):
+ uris.add(val[0])
+ return uris
+
+ def _getUsedNamespaces(self):
+ "Return actually used namespaces only."
+ useduris = self._getUsedUris()
+ namespaces = _SimpleNamespaces(log=self._log)
+ for p, uri in self._namespaces.items():
+ if uri in useduris:
+ namespaces[p] = uri
+ return namespaces
+
+ def __getNamespaces(self):
+ "Use own namespaces if not attached to a sheet, else the sheet's ones."
+ try:
+ return self._parent.parentRule.parentStyleSheet.namespaces
+ except AttributeError:
+ return self.__namespaces
+
+ _namespaces = property(__getNamespaces,
+ doc=u"If this Selector is attached to a "
+ u"CSSStyleSheet the namespaces of that sheet "
+ u"are mirrored here. While the Selector (or "
+ u"parent SelectorList or parentRule(s) of that "
+ u"are not attached a own dict of {prefix: "
+ u"namespaceURI} is used.")
+
+
+ element = property(lambda self: self._element,
+ doc=u"Effective element target of this selector.")
+
+ parent = property(lambda self: self._parent,
+ doc=u"(DOM) The SelectorList that contains this Selector "
+ u"or None if this Selector is not attached to a "
+ u"SelectorList.")
+
+ def _getSelectorText(self):
+ """Return serialized format."""
+ return cssutils.ser.do_css_Selector(self)
+
+ def _setSelectorText(self, selectorText):
+ """
+ :param selectorText:
+ parsable string or a tuple of (selectorText, dict-of-namespaces).
+ Given namespaces are ignored if this object is attached to a
+ CSSStyleSheet!
+
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if the specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ and is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+
+ # might be (selectorText, namespaces)
+ selectorText, namespaces = self._splitNamespacesOff(selectorText)
+
+ try:
+ # uses parent stylesheets namespaces if available,
+ # otherwise given ones
+ namespaces = self.parent.parentRule.parentStyleSheet.namespaces
+ except AttributeError:
+ pass
+ tokenizer = self._tokenize2(selectorText)
+ if not tokenizer:
+ self._log.error(u'Selector: No selectorText given.')
+ else:
+ # prepare tokenlist:
+ # "*" -> type "universal"
+ # "*"|IDENT + "|" -> combined to "namespace_prefix"
+ # "|" -> type "namespace_prefix"
+ # "." + IDENT -> combined to "class"
+ # ":" + IDENT, ":" + FUNCTION -> pseudo-class
+ # FUNCTION "not(" -> negation
+ # "::" + IDENT, "::" + FUNCTION -> pseudo-element
+ tokens = []
+ for t in tokenizer:
+ typ, val, lin, col = t
+ if val == u':' and tokens and\
+ self._tokenvalue(tokens[-1]) == ':':
+ # combine ":" and ":"
+ tokens[-1] = (typ, u'::', lin, col)
+
+ elif typ == 'IDENT' and tokens\
+ and self._tokenvalue(tokens[-1]) == u'.':
+ # class: combine to .IDENT
+ tokens[-1] = ('class', u'.'+val, lin, col)
+ elif typ == 'IDENT' and tokens and \
+ self._tokenvalue(tokens[-1]).startswith(u':') and\
+ not self._tokenvalue(tokens[-1]).endswith(u'('):
+ # pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b"
+ if self._tokenvalue(tokens[-1]).startswith(u'::'):
+ t = 'pseudo-element'
+ else:
+ t = 'pseudo-class'
+ tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
+
+ elif typ == 'FUNCTION' and val == u'not(' and tokens and \
+ u':' == self._tokenvalue(tokens[-1]):
+ tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
+ elif typ == 'FUNCTION' and tokens\
+ and self._tokenvalue(tokens[-1]).startswith(u':'):
+ # pseudo-X: combine to :FUNCTION( or ::FUNCTION(
+ if self._tokenvalue(tokens[-1]).startswith(u'::'):
+ t = 'pseudo-element'
+ else:
+ t = 'pseudo-class'
+ tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
+
+ elif val == u'*' and tokens and\
+ self._type(tokens[-1]) == 'namespace_prefix' and\
+ self._tokenvalue(tokens[-1]).endswith(u'|'):
+ # combine prefix|*
+ tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
+ lin, col)
+ elif val == u'*':
+ # universal: "*"
+ tokens.append(('universal', val, lin, col))
+
+ elif val == u'|' and tokens and\
+ self._type(tokens[-1]) in (self._prods.IDENT, 'universal')\
+ and self._tokenvalue(tokens[-1]).find(u'|') == -1:
+ # namespace_prefix: "IDENT|" or "*|"
+ tokens[-1] = ('namespace_prefix',
+ self._tokenvalue(tokens[-1])+u'|', lin, col)
+ elif val == u'|':
+ # namespace_prefix: "|"
+ tokens.append(('namespace_prefix', val, lin, col))
+
+ else:
+ tokens.append(t)
+
+ tokenizer = iter(tokens)
+
+ # for closures: must be a mutable
+ new = {'context': [''], # stack of: 'attrib', 'negation', 'pseudo'
+ 'element': None,
+ '_PREFIX': None,
+ 'specificity': [0, 0, 0, 0], # mutable, finally a tuple!
+ 'wellformed': True
+ }
+ # used for equality checks and setting of a space combinator
+ S = u' '
+
+ def append(seq, val, typ=None, token=None):
+ """
+ appends to seq
+
+ namespace_prefix, IDENT will be combined to a tuple
+ (prefix, name) where prefix might be None, the empty string
+ or a prefix.
+
+ Saved are also:
+ - specificity definition: style, id, class/att, type
+ - element: the element this Selector is for
+ """
+ context = new['context'][-1]
+ if token:
+ line, col = token[2], token[3]
+ else:
+ line, col = None, None
+
+ if typ == '_PREFIX':
+ # SPECIAL TYPE: save prefix for combination with next
+ new['_PREFIX'] = val[:-1]
+ # handle next time
+ return
+
+ if new['_PREFIX'] is not None:
+ # as saved from before and reset to None
+ prefix, new['_PREFIX'] = new['_PREFIX'], None
+ elif typ == 'universal' and '|' in val:
+ # val == *|* or prefix|*
+ prefix, val = val.split('|')
+ else:
+ prefix = None
+
+ # namespace
+ if (typ.endswith('-selector') or typ == 'universal') and not (
+ 'attribute-selector' == typ and not prefix):
+ # att **IS NOT** in default ns
+ if prefix == u'*':
+ # *|name: in ANY_NS
+ namespaceURI = cssutils._ANYNS
+ elif prefix is None:
+ # e or *: default namespace with prefix u''
+ # or local-name()
+ namespaceURI = namespaces.get(u'', None)
+ elif prefix == u'':
+ # |name or |*: in no (or the empty) namespace
+ namespaceURI = u''
+ else:
+ # explicit namespace prefix
+ # does not raise KeyError, see _SimpleNamespaces
+ namespaceURI = namespaces[prefix]
+
+ if namespaceURI is None:
+ new['wellformed'] = False
+ self._log.error(u'Selector: No namespaceURI found '
+ u'for prefix %r' % prefix,
+ token=token,
+ error=xml.dom.NamespaceErr)
+ return
+
+ # val is now (namespaceprefix, name) tuple
+ val = (namespaceURI, val)
+
+ # specificity
+ if not context or context == 'negation':
+ if 'id' == typ:
+ new['specificity'][1] += 1
+ elif 'class' == typ or '[' == val:
+ new['specificity'][2] += 1
+ elif typ in ('type-selector', 'negation-type-selector',
+ 'pseudo-element'):
+ new['specificity'][3] += 1
+ if not context and typ in ('type-selector', 'universal'):
+ # define element
+ new['element'] = val
+
+ seq.append(val, typ, line=line, col=col)
+
+ # expected constants
+ simple_selector_sequence = 'type_selector universal HASH class ' \
+ 'attrib pseudo negation '
+ simple_selector_sequence2 = 'HASH class attrib pseudo negation '
+
+ element_name = 'element_name'
+
+ negation_arg = 'type_selector universal HASH class attrib pseudo'
+ negationend = ')'
+
+ attname = 'prefix attribute'
+ attname2 = 'attribute'
+ attcombinator = 'combinator ]' # optional
+ attvalue = 'value' # optional
+ attend = ']'
+
+ expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
+ expression = expressionstart + ' )'
+
+ combinator = ' combinator'
+
+ def _COMMENT(expected, seq, token, tokenizer=None):
+ "special implementation for comment token"
+ append(seq, cssutils.css.CSSComment([token]), 'COMMENT',
+ token=token)
+ return expected
+
+ def _S(expected, seq, token, tokenizer=None):
+ # S
+ context = new['context'][-1]
+ if context.startswith('pseudo-'):
+ if seq and seq[-1].value not in u'+-':
+ # e.g. x:func(a + b)
+ append(seq, S, 'S', token=token)
+ return expected
+
+ elif context != 'attrib' and 'combinator' in expected:
+ append(seq, S, 'descendant', token=token)
+ return simple_selector_sequence + combinator
+
+ else:
+ return expected
+
+ def _universal(expected, seq, token, tokenizer=None):
+ # *|* or prefix|*
+ context = new['context'][-1]
+ val = self._tokenvalue(token)
+ if 'universal' in expected:
+ append(seq, val, 'universal', token=token)
+
+ if 'negation' == context:
+ return negationend
+ else:
+ return simple_selector_sequence2 + combinator
+
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected universal.', token=token)
+ return expected
+
+ def _namespace_prefix(expected, seq, token, tokenizer=None):
+ # prefix| => element_name
+ # or prefix| => attribute_name if attrib
+ context = new['context'][-1]
+ val = self._tokenvalue(token)
+ if 'attrib' == context and 'prefix' in expected:
+ # [PREFIX|att]
+ append(seq, val, '_PREFIX', token=token)
+ return attname2
+ elif 'type_selector' in expected:
+ # PREFIX|*
+ append(seq, val, '_PREFIX', token=token)
+ return element_name
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected namespace prefix.', token=token)
+ return expected
+
+ def _pseudo(expected, seq, token, tokenizer=None):
+ # pseudo-class or pseudo-element :a ::a :a( ::a(
+ """
+ /* '::' starts a pseudo-element, ':' a pseudo-class */
+ /* Exceptions: :first-line, :first-letter, :before and
+ :after. */
+ /* Note that pseudo-elements are restricted to one per selector
+ and */
+ /* occur only in the last simple_selector_sequence. */
+ """
+ context = new['context'][-1]
+ val, typ = self._tokenvalue(token, normalize=True),\
+ self._type(token)
+ if 'pseudo' in expected:
+ if val in (':first-line',
+ ':first-letter',
+ ':before',
+ ':after'):
+ # always pseudo-element ???
+ typ = 'pseudo-element'
+ append(seq, val, typ, token=token)
+
+ if val.endswith(u'('):
+ # function
+ # "pseudo-" "class" or "element"
+ new['context'].append(typ)
+ return expressionstart
+ elif 'negation' == context:
+ return negationend
+ elif 'pseudo-element' == typ:
+ # only one per element, check at ) also!
+ return combinator
+ else:
+ return simple_selector_sequence2 + combinator
+
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected start of pseudo.', token=token)
+ return expected
+
+ def _expression(expected, seq, token, tokenizer=None):
+ # [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
+ context = new['context'][-1]
+ val, typ = self._tokenvalue(token), self._type(token)
+ if context.startswith('pseudo-'):
+ append(seq, val, typ, token=token)
+ return expression
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected %s.' % typ, token=token)
+ return expected
+
+ def _attcombinator(expected, seq, token, tokenizer=None):
+ # context: attrib
+ # PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES |
+ # DASHMATCH
+ context = new['context'][-1]
+ val, typ = self._tokenvalue(token), self._type(token)
+ if 'attrib' == context and 'combinator' in expected:
+ # combinator in attrib
+ append(seq, val, typ.lower(), token=token)
+ return attvalue
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected %s.' % typ, token=token)
+ return expected
+
+ def _string(expected, seq, token, tokenizer=None):
+ # identifier
+ context = new['context'][-1]
+ typ, val = self._type(token), self._stringtokenvalue(token)
+
+ # context: attrib
+ if 'attrib' == context and 'value' in expected:
+ # attrib: [...=VALUE]
+ append(seq, val, typ, token=token)
+ return attend
+
+ # context: pseudo
+ elif context.startswith('pseudo-'):
+ # :func(...)
+ append(seq, val, typ, token=token)
+ return expression
+
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected STRING.', token=token)
+ return expected
+
+ def _ident(expected, seq, token, tokenizer=None):
+ # identifier
+ context = new['context'][-1]
+ val, typ = self._tokenvalue(token), self._type(token)
+
+ # context: attrib
+ if 'attrib' == context and 'attribute' in expected:
+ # attrib: [...|ATT...]
+ append(seq, val, 'attribute-selector', token=token)
+ return attcombinator
+
+ elif 'attrib' == context and 'value' in expected:
+ # attrib: [...=VALUE]
+ append(seq, val, 'attribute-value', token=token)
+ return attend
+
+ # context: negation
+ elif 'negation' == context:
+ # negation: (prefix|IDENT)
+ append(seq, val, 'negation-type-selector', token=token)
+ return negationend
+
+ # context: pseudo
+ elif context.startswith('pseudo-'):
+ # :func(...)
+ append(seq, val, typ, token=token)
+ return expression
+
+ elif 'type_selector' in expected or element_name == expected:
+ # element name after ns or complete type_selector
+ append(seq, val, 'type-selector', token=token)
+ return simple_selector_sequence2 + combinator
+
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Selector: Unexpected IDENT.', token=token)
+ return expected
+
+ def _class(expected, seq, token, tokenizer=None):
+ # .IDENT
+ context = new['context'][-1]
+ val = self._tokenvalue(token)
+ if 'class' in expected:
+ append(seq, val, 'class', token=token)
+
+ if 'negation' == context:
+ return negationend
+ else:
+ return simple_selector_sequence2 + combinator
+
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Selector: Unexpected class.', token=token)
+ return expected
+
+ def _hash(expected, seq, token, tokenizer=None):
+ # #IDENT
+ context = new['context'][-1]
+ val = self._tokenvalue(token)
+ if 'HASH' in expected:
+ append(seq, val, 'id', token=token)
+
+ if 'negation' == context:
+ return negationend
+ else:
+ return simple_selector_sequence2 + combinator
+
+ else:
+ new['wellformed'] = False
+ self._log.error(u'Selector: Unexpected HASH.', token=token)
+ return expected
+
+ def _char(expected, seq, token, tokenizer=None):
+ # + > ~ ) [ ] + -
+ context = new['context'][-1]
+ val = self._tokenvalue(token)
+
+ # context: attrib
+ if u']' == val and 'attrib' == context and ']' in expected:
+ # end of attrib
+ append(seq, val, 'attribute-end', token=token)
+ context = new['context'].pop() # attrib is done
+ context = new['context'][-1]
+ if 'negation' == context:
+ return negationend
+ else:
+ return simple_selector_sequence2 + combinator
+
+ elif u'=' == val and 'attrib' == context\
+ and 'combinator' in expected:
+ # combinator in attrib
+ append(seq, val, 'equals', token=token)
+ return attvalue
+
+ # context: negation
+ elif u')' == val and 'negation' == context and u')' in expected:
+ # not(negation_arg)"
+ append(seq, val, 'negation-end', token=token)
+ new['context'].pop() # negation is done
+ context = new['context'][-1]
+ return simple_selector_sequence + combinator
+
+ # context: pseudo (at least one expression)
+ elif val in u'+-' and context.startswith('pseudo-'):
+ # :func(+ -)"
+ _names = {'+': 'plus', '-': 'minus'}
+ if val == u'+' and seq and seq[-1].value == S:
+ seq.replace(-1, val, _names[val])
+ else:
+ append(seq, val, _names[val],
+ token=token)
+ return expression
+
+ elif u')' == val and context.startswith('pseudo-') and\
+ expression == expected:
+ # :func(expression)"
+ append(seq, val, 'function-end', token=token)
+ new['context'].pop() # pseudo is done
+ if 'pseudo-element' == context:
+ return combinator
+ else:
+ return simple_selector_sequence + combinator
+
+ # context: ROOT
+ elif u'[' == val and 'attrib' in expected:
+ # start of [attrib]
+ append(seq, val, 'attribute-start', token=token)
+ new['context'].append('attrib')
+ return attname
+
+ elif val in u'+>~' and 'combinator' in expected:
+ # no other combinator except S may be following
+ _names = {
+ '>': 'child',
+ '+': 'adjacent-sibling',
+ '~': 'following-sibling'}
+ if seq and seq[-1].value == S:
+ seq.replace(-1, val, _names[val])
+ else:
+ append(seq, val, _names[val], token=token)
+ return simple_selector_sequence
+
+ elif u',' == val:
+ # not a selectorlist
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Single selector only.',
+ error=xml.dom.InvalidModificationErr,
+ token=token)
+ return expected
+
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected CHAR.', token=token)
+ return expected
+
+ def _negation(expected, seq, token, tokenizer=None):
+ # not(
+ context = new['context'][-1]
+ val = self._tokenvalue(token, normalize=True)
+ if 'negation' in expected:
+ new['context'].append('negation')
+ append(seq, val, 'negation-start', token=token)
+ return negation_arg
+ else:
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected negation.', token=token)
+ return expected
+
+ def _atkeyword(expected, seq, token, tokenizer=None):
+ "invalidates selector"
+ new['wellformed'] = False
+ self._log.error(
+ u'Selector: Unexpected ATKEYWORD.', token=token)
+ return expected
+
+
+ # expected: only|not or mediatype, mediatype, feature, and
+ newseq = self._tempSeq()
+
+ wellformed, expected = self._parse(
+ expected=simple_selector_sequence,
+ seq=newseq, tokenizer=tokenizer,
+ productions={'CHAR': _char,
+ 'class': _class,
+ 'HASH': _hash,
+ 'STRING': _string,
+ 'IDENT': _ident,
+ 'namespace_prefix': _namespace_prefix,
+ 'negation': _negation,
+ 'pseudo-class': _pseudo,
+ 'pseudo-element': _pseudo,
+ 'universal': _universal,
+ # pseudo
+ 'NUMBER': _expression,
+ 'DIMENSION': _expression,
+ # attribute
+ 'PREFIXMATCH': _attcombinator,
+ 'SUFFIXMATCH': _attcombinator,
+ 'SUBSTRINGMATCH': _attcombinator,
+ 'DASHMATCH': _attcombinator,
+ 'INCLUDES': _attcombinator,
+
+ 'S': _S,
+ 'COMMENT': _COMMENT,
+ 'ATKEYWORD': _atkeyword})
+ wellformed = wellformed and new['wellformed']
+
+ # post condition
+ if len(new['context']) > 1 or not newseq:
+ wellformed = False
+ self._log.error(u'Selector: Invalid or incomplete selector: %s'
+ % self._valuestr(selectorText))
+
+ if expected == 'element_name':
+ wellformed = False
+ self._log.error(u'Selector: No element name found: %s'
+ % self._valuestr(selectorText))
+
+ if expected == simple_selector_sequence and newseq:
+ wellformed = False
+ self._log.error(u'Selector: Cannot end with combinator: %s'
+ % self._valuestr(selectorText))
+
+ if newseq and hasattr(newseq[-1].value, 'strip') \
+ and newseq[-1].value.strip() == u'':
+ del newseq[-1]
+
+ # set
+ if wellformed:
+ self.__namespaces = namespaces
+ self._element = new['element']
+ self._specificity = tuple(new['specificity'])
+ self._setSeq(newseq)
+ # filter that only used ones are kept
+ self.__namespaces = self._getUsedNamespaces()
+
+ selectorText = property(_getSelectorText, _setSelectorText,
+ doc=u"(DOM) The parsable textual representation of "
+ u"the selector.")
+
+ specificity = property(lambda self: self._specificity,
+ doc="""Specificity of this selector (READONLY).
+ Tuple of (a, b, c, d) where:
+
+ a
+ presence of style in document, always 0 if not used on a
+ document
+ b
+ number of ID selectors
+ c
+ number of .class selectors
+ d
+ number of Element (type) selectors""")
+
+ wellformed = property(lambda self: bool(len(self.seq)))
+
+
+ @Deprecated('Use property parent instead')
+ def _getParentList(self):
+ return self.parent
+
+ parentList = property(_getParentList,
+ doc="DEPRECATED, see property parent instead")
diff --git a/libs/cssutils/css/selectorlist.py b/libs/cssutils/css/selectorlist.py
new file mode 100755
index 00000000..2072cd53
--- /dev/null
+++ b/libs/cssutils/css/selectorlist.py
@@ -0,0 +1,234 @@
+"""SelectorList is a list of CSS Selector objects.
+
+TODO
+ - remove duplicate Selectors. -> CSSOM canonicalize
+
+ - ??? CSS2 gives a special meaning to the comma (,) in selectors.
+ However, since it is not known if the comma may acquire other
+ meanings in future versions of CSS, the whole statement should be
+ ignored if there is an error anywhere in the selector, even though
+ the rest of the selector may look reasonable in CSS2.
+
+ Illegal example(s):
+
+ For example, since the "&" is not a valid token in a CSS2 selector,
+ a CSS2 user agent must ignore the whole second line, and not set
+ the color of H3 to red:
+"""
+__all__ = ['SelectorList']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from selector import Selector
+import cssutils
+import xml.dom
+
+class SelectorList(cssutils.util.Base, cssutils.util.ListSeq):
+ """A list of :class:`~cssutils.css.Selector` objects
+ of a :class:`~cssutils.css.CSSStyleRule`."""
+ def __init__(self, selectorText=None, parentRule=None,
+ readonly=False):
+ """
+ :Parameters:
+ selectorText
+ parsable list of Selectors
+ parentRule
+ the parent CSSRule if available
+ """
+ super(SelectorList, self).__init__()
+
+ self._parentRule = parentRule
+
+ if selectorText:
+ self.selectorText = selectorText
+
+ self._readonly = readonly
+
+ def __repr__(self):
+ if self._namespaces:
+ st = (self.selectorText, self._namespaces)
+ else:
+ st = self.selectorText
+ return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__,
+ st)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.selectorText,
+ self._namespaces,
+ id(self))
+
+ def __setitem__(self, index, newSelector):
+ """Overwrite ListSeq.__setitem__
+
+ Any duplicate Selectors are **not** removed.
+ """
+ newSelector = self.__prepareset(newSelector)
+ if newSelector:
+ self.seq[index] = newSelector
+
+ def __prepareset(self, newSelector, namespaces=None):
+ "Used by appendSelector and __setitem__"
+ if not namespaces:
+ namespaces = {}
+ self._checkReadonly()
+ if not isinstance(newSelector, Selector):
+ newSelector = Selector((newSelector, namespaces),
+ parent=self)
+ if newSelector.wellformed:
+ newSelector._parent = self # maybe set twice but must be!
+ return newSelector
+
+ def __getNamespaces(self):
+ """Use children namespaces if not attached to a sheet, else the sheet's
+ ones.
+ """
+ try:
+ return self.parentRule.parentStyleSheet.namespaces
+ except AttributeError:
+ namespaces = {}
+ for selector in self.seq:
+ namespaces.update(selector._namespaces)
+ return namespaces
+
+ def _getUsedUris(self):
+ "Used by CSSStyleSheet to check if @namespace rules are needed"
+ uris = set()
+ for s in self:
+ uris.update(s._getUsedUris())
+ return uris
+
+ _namespaces = property(__getNamespaces, doc="""If this SelectorList is
+ attached to a CSSStyleSheet the namespaces of that sheet are mirrored
+ here. While the SelectorList (or parentRule(s) are
+ not attached the namespaces of all children Selectors are used.""")
+
+ def append(self, newSelector):
+ "Same as :meth:`appendSelector`."
+ self.appendSelector(newSelector)
+
+ def appendSelector(self, newSelector):
+ """
+ Append `newSelector` to this list (a string will be converted to a
+ :class:`~cssutils.css.Selector`).
+
+ :param newSelector:
+ comma-separated list of selectors (as a single string) or a tuple of
+ `(newSelector, dict-of-namespaces)`
+ :returns: New :class:`~cssutils.css.Selector` or ``None`` if
+ `newSelector` is not wellformed.
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if the specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ and is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+
+ # might be (selectorText, namespaces)
+ newSelector, namespaces = self._splitNamespacesOff(newSelector)
+ try:
+ # use parent's only if available
+ namespaces = self.parentRule.parentStyleSheet.namespaces
+ except AttributeError:
+ # use already present namespaces plus new given ones
+ _namespaces = self._namespaces
+ _namespaces.update(namespaces)
+ namespaces = _namespaces
+
+ newSelector = self.__prepareset(newSelector, namespaces)
+ if newSelector:
+ seq = self.seq[:]
+ del self.seq[:]
+ for s in seq:
+ if s.selectorText != newSelector.selectorText:
+ self.seq.append(s)
+ self.seq.append(newSelector)
+ return newSelector
+
+ def _getSelectorText(self):
+ "Return serialized format."
+ return cssutils.ser.do_css_SelectorList(self)
+
+ def _setSelectorText(self, selectorText):
+ """
+ :param selectorText:
+ comma-separated list of selectors or a tuple of
+ (selectorText, dict-of-namespaces)
+ :exceptions:
+ - :exc:`~xml.dom.NamespaceErr`:
+ Raised if the specified selector uses an unknown namespace
+ prefix.
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ and is unparsable.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this rule is readonly.
+ """
+ self._checkReadonly()
+
+ # might be (selectorText, namespaces)
+ selectorText, namespaces = self._splitNamespacesOff(selectorText)
+ try:
+ # use parent's only if available
+ namespaces = self.parentRule.parentStyleSheet.namespaces
+ except AttributeError:
+ pass
+
+ wellformed = True
+ tokenizer = self._tokenize2(selectorText)
+ newseq = []
+
+ expected = True
+ while True:
+ # find all upto and including next ",", EOF or nothing
+ selectortokens = self._tokensupto2(tokenizer, listseponly=True)
+ if selectortokens:
+ if self._tokenvalue(selectortokens[-1]) == ',':
+ expected = selectortokens.pop()
+ else:
+ expected = None
+
+ selector = Selector((selectortokens, namespaces),
+ parent=self)
+ if selector.wellformed:
+ newseq.append(selector)
+ else:
+ wellformed = False
+ self._log.error(u'SelectorList: Invalid Selector: %s' %
+ self._valuestr(selectortokens))
+ else:
+ break
+
+ # post condition
+ if u',' == expected:
+ wellformed = False
+ self._log.error(u'SelectorList: Cannot end with ",": %r' %
+ self._valuestr(selectorText))
+ elif expected:
+ wellformed = False
+ self._log.error(u'SelectorList: Unknown Syntax: %r' %
+ self._valuestr(selectorText))
+ if wellformed:
+ self.seq = newseq
+
+ selectorText = property(_getSelectorText, _setSelectorText,
+ doc=u"(cssutils) The textual representation of the "
+ u"selector for a rule set.")
+
+ length = property(lambda self: len(self),
+ doc=u"The number of :class:`~cssutils.css.Selector` "
+ u"objects in the list.")
+
+ parentRule = property(lambda self: self._parentRule,
+ doc=u"(DOM) The CSS rule that contains this "
+ u"SelectorList or ``None`` if this SelectorList "
+ u"is not attached to a CSSRule.")
+
+ wellformed = property(lambda self: bool(len(self.seq)))
+
diff --git a/libs/cssutils/css/value.py b/libs/cssutils/css/value.py
new file mode 100755
index 00000000..f9ab050a
--- /dev/null
+++ b/libs/cssutils/css/value.py
@@ -0,0 +1,871 @@
+"""Value related classes.
+
+DOM Level 2 CSS CSSValue, CSSPrimitiveValue and CSSValueList are **no longer**
+supported and are replaced by these new classes.
+"""
+__all__ = ['PropertyValue',
+ 'Value',
+ 'ColorValue',
+ 'DimensionValue',
+ 'URIValue',
+ 'CSSFunction',
+ 'CSSVariable',
+ 'MSValue'
+ ]
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from cssutils.prodparser import *
+import cssutils
+from cssutils.helper import normalize, pushtoken
+import colorsys
+import math
+import re
+import xml.dom
+import urlparse
+
+class PropertyValue(cssutils.util._NewBase):
+ """
+ An unstructured list like holder for all values defined for a
+ :class:`~cssutils.css.Property`. Contains :class:`~cssutils.css.Value`
+ or subclass objects. Currently there is no access to the combinators of
+ the defined values which might simply be space or comma or slash.
+
+ You may:
+
+ - iterate over all contained Value objects (not the separators like ``,``,
+ ``/`` or `` `` though!)
+ - get a Value item by index or use ``PropertyValue[index]``
+ - find out the number of values defined (unstructured)
+ """
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ """
+ :param cssText:
+ the parsable cssText of the value
+ :param readonly:
+ defaults to False
+ """
+ super(PropertyValue, self).__init__()
+
+ self.parent = parent
+ self.wellformed = False
+
+ if cssText is not None: # may be 0
+ if isinstance(cssText, (int, float)):
+ cssText = unicode(cssText) # if it is a number
+ self.cssText = cssText
+
+ self._readonly = readonly
+
+ def __len__(self):
+ return len(list(self.__items()))
+
+ def __getitem__(self, index):
+ try:
+ return list(self.__items())[index]
+ except IndexError:
+ return None
+
+ def __iter__(self):
+ "Generator which iterates over values."
+ for item in self.__items():
+ yield item
+
+ def __repr__(self):
+ return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u"" % (self.__class__.__name__,
+ self.length, self.cssText, id(self))
+
+ def __items(self, seq=None):
+ "a generator of Value obects only, no , / or ' '"
+ if seq is None:
+ seq = self.seq
+ return (x.value for x in seq if isinstance(x.value, Value))
+
+ def _setCssText(self, cssText):
+ if isinstance(cssText, (int, float)):
+ cssText = unicode(cssText) # if it is a number
+ """
+ Format::
+
+ unary_operator
+ : '-' | '+'
+ ;
+ operator
+ : '/' S* | ',' S* | /* empty */
+ ;
+ expr
+ : term [ operator term ]*
+ ;
+ term
+ : unary_operator?
+ [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
+ ANGLE S* | TIME S* | FREQ S* ]
+ | STRING S* | IDENT S* | URI S* | hexcolor | function
+ | UNICODE-RANGE S*
+ ;
+ function
+ : FUNCTION S* expr ')' S*
+ ;
+ /*
+ * There is a constraint on the color that it must
+ * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
+ * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
+ */
+ hexcolor
+ : HASH S*
+ ;
+
+ :exceptions:
+ - :exc:`~xml.dom.SyntaxErr`:
+ Raised if the specified CSS string value has a syntax error
+ (according to the attached property) or is unparsable.
+ - :exc:`~xml.dom.InvalidModificationErr`:
+ TODO: Raised if the specified CSS string value represents a
+ different type of values than the values allowed by the CSS
+ property.
+ - :exc:`~xml.dom.NoModificationAllowedErr`:
+ Raised if this value is readonly.
+ """
+ self._checkReadonly()
+
+ # used as operator is , / or S
+ nextSor = u',/'
+ term = Choice(_ColorProd(self, nextSor),
+ _DimensionProd(self, nextSor),
+ _URIProd(self, nextSor),
+ _ValueProd(self, nextSor),
+# _CalcValueProd(self, nextSor),
+# _Rect(self, nextSor),
+ # all other functions
+ _CSSVariableProd(self, nextSor),
+ _MSValueProd(self, nextSor),
+ _CSSFunctionProd(self, nextSor)
+ )
+ operator = Choice(PreDef.S(toSeq=False),
+ PreDef.char('comma', ',',
+ toSeq=lambda t, tokens: ('operator', t[1])),
+ PreDef.char('slash', '/',
+ toSeq=lambda t, tokens: ('operator', t[1])),
+ optional=True)
+ prods = Sequence(term,
+ Sequence(# mayEnd this Sequence if whitespace
+ operator,
+ # TODO: only when setting via other class
+ # used by variabledeclaration currently
+ PreDef.char('END', ';',
+ stopAndKeep=True,
+ optional=True),
+ # TODO: } and !important ends too!
+ term,
+ minmax=lambda: (0, None)))
+ # parse
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ u'PropertyValue',
+ prods)
+ # must be at least one value!
+ ok = ok and len(list(self.__items(seq))) > 0
+ if ok:
+ self._setSeq(seq)
+ self.wellformed = True
+ else:
+ self._log.error(u'PropertyValue: Unknown syntax or no value: %s' %
+ self._valuestr(cssText))
+
+ cssText = property(lambda self: cssutils.ser.do_css_PropertyValue(self),
+ _setCssText,
+ doc="A string representation of the current value.")
+
+ def item(self, index):
+ """
+ The value at position `index`. Alternatively simple use
+ ``PropertyValue[index]``.
+
+ :param index:
+ the parsable cssText of the value
+ :exceptions:
+ - :exc:`~IndexError`:
+ Raised if index if out of bounds
+ """
+ return self[index]
+
+ length = property(lambda self: len(self),
+ doc=u"Number of values set.")
+
+ value = property(lambda self: cssutils.ser.do_css_PropertyValue(self,
+ valuesOnly=True),
+ doc=u"A string representation of the current value "
+ u"without any comments used for validation.")
+
+
+class Value(cssutils.util._NewBase):
+ """
+ Represents a single CSS value. For now simple values of
+ IDENT, STRING, or UNICODE-RANGE values are represented directly
+ as Value objects. Other values like e.g. FUNCTIONs are represented by
+ subclasses with an extended API.
+ """
+ IDENT = u'IDENT'
+ STRING = u'STRING'
+ UNICODE_RANGE = u'UNICODE-RANGE'
+ URI = u'URI'
+
+ DIMENSION = u'DIMENSION'
+ NUMBER = u'NUMBER'
+ PERCENTAGE = u'PERCENTAGE'
+
+ COLOR_VALUE = u'COLOR_VALUE'
+ HASH = u'HASH'
+
+ FUNCTION = u'FUNCTION'
+ VARIABLE = u'VARIABLE'
+
+ _type = None
+ _value = u''
+
+ def __init__(self, cssText=None, parent=None, readonly=False):
+ super(Value, self).__init__()
+
+ self.parent = parent
+
+ if cssText:
+ self.cssText = cssText
+
+ def __repr__(self):
+ return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
+ self.cssText)
+
+ def __str__(self):
+ return u""\
+ % (self.__class__.__name__,
+ self.type, self.value, self.cssText,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+
+ prods = Choice(PreDef.hexcolor(stop=True),
+ PreDef.ident(stop=True),
+ PreDef.string(stop=True),
+ PreDef.unicode_range(stop=True),
+ )
+ ok, seq, store, unused = ProdParser().parse(cssText, u'Value', prods)
+ if ok:
+ # only 1 value anyway!
+ self._type = seq[0].type
+ self._value = seq[0].value
+
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_Value(self),
+ _setCssText,
+ doc=u'String value of this value.')
+
+ type = property(lambda self: self._type, #_setType,
+ doc=u"Type of this value, for now the production type "
+ u"like e.g. `DIMENSION` or `STRING`. All types are "
+ u"defined as constants in :class:`~cssutils.css.Value`.")
+
+ def _setValue(self, value):
+ # TODO: check!
+ self._value = value
+
+ value = property(lambda self: self._value, _setValue,
+ doc=u"Actual value if possible: An int or float or else "
+ u" a string")
+
+
+class ColorValue(Value):
+ """
+ A color value like rgb(), rgba(), hsl(), hsla() or #rgb, #rrggbb
+
+ TODO: Color Keywords
+ """
+ from colors import COLORS
+
+ type = Value.COLOR_VALUE
+ # hexcolor, FUNCTION?
+ _colorType = None
+ _red = 0
+ _green = 0
+ _blue = 0
+ _alpha = 0
+
+ def __str__(self):
+ return u""\
+ % (self.__class__.__name__,
+ self.type, self.value,
+ self.colorType, self.red, self.green, self.blue, self.alpha,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+ types = self._prods # rename!
+
+ component = Choice(PreDef.unary(toSeq=lambda t, tokens: (t[0],
+ DimensionValue(pushtoken(t, tokens),
+ parent=self)
+ )),
+ PreDef.number(toSeq=lambda t, tokens: (t[0],
+ DimensionValue(pushtoken(t, tokens),
+ parent=self)
+ )),
+ PreDef.percentage(toSeq=lambda t, tokens: (t[0],
+ DimensionValue(pushtoken(t, tokens),
+ parent=self)
+ ))
+ )
+ noalp = Sequence(Prod(name='FUNCTION',
+ match=lambda t, v: t == types.FUNCTION and
+ v in (u'rgb(', u'hsl('),
+ toSeq=lambda t, tokens: (t[0], normalize(t[1]))),
+ component,
+ Sequence(PreDef.comma(),
+ component,
+ minmax=lambda: (2, 2)
+ ),
+ PreDef.funcEnd(stop=True)
+ )
+ witha = Sequence(Prod(name='FUNCTION',
+ match=lambda t, v: t == types.FUNCTION and
+ v in (u'rgba(', u'hsla('),
+ toSeq=lambda t, tokens: (t[0],
+ normalize(t[1]))
+ ),
+ component,
+ Sequence(PreDef.comma(),
+ component,
+ minmax=lambda: (3, 3)
+ ),
+ PreDef.funcEnd(stop=True)
+ )
+ namedcolor = Prod(name='Named Color',
+ match=lambda t, v: t == 'IDENT' and (
+ normalize(v) in self.COLORS.keys()
+ ),
+ stop=True)
+
+ prods = Choice(PreDef.hexcolor(stop=True),
+ namedcolor,
+ noalp,
+ witha)
+
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ self.type,
+ prods)
+ if ok:
+ t, v = seq[0].type, seq[0].value
+ if u'IDENT' == t:
+ rgba = self.COLORS[normalize(v)]
+ if u'HASH' == t:
+ if len(v) == 4:
+ # HASH #rgb
+ rgba = (int(2*v[1], 16),
+ int(2*v[2], 16),
+ int(2*v[3], 16),
+ 1.0)
+ else:
+ # HASH #rrggbb
+ rgba = (int(v[1:3], 16),
+ int(v[3:5], 16),
+ int(v[5:7], 16),
+ 1.0)
+
+ elif u'FUNCTION' == t:
+ functiontype, raw, check = None, [], u''
+ HSL = False
+
+ for item in seq:
+ try:
+ type_ = item.value.type
+ except AttributeError, e:
+ # type of function, e.g. rgb(
+ if item.type == 'FUNCTION':
+ functiontype = item.value
+ HSL = functiontype in (u'hsl(', u'hsla(')
+ continue
+
+ # save components
+ if type_ == Value.NUMBER:
+ raw.append(item.value.value)
+ check += u'N'
+ elif type_ == Value.PERCENTAGE:
+ if HSL:
+ # save as percentage fraction
+ raw.append(item.value.value / 100.0)
+ else:
+ # save as real value of percentage of 255
+ raw.append(int(255 * item.value.value / 100))
+ check += u'P'
+
+ if HSL:
+ # convert to rgb
+ # h is 360 based (circle)
+ h, s, l = raw[0] / 360.0, raw[1], raw[2]
+ # ORDER h l s !!!
+ r, g, b = colorsys.hls_to_rgb(h, l, s)
+ # back to 255 based
+ rgba = [int(round(r*255)),
+ int(round(g*255)),
+ int(round(b*255))]
+
+ if len(raw) > 3:
+ rgba.append(raw[3])
+
+ else:
+ # rgb, rgba
+ rgba = raw
+
+ if len(rgba) < 4:
+ rgba.append(1.0)
+
+ # validate
+ checks = {u'rgb(': ('NNN', 'PPP'),
+ u'rgba(': ('NNNN', 'PPPN'),
+ u'hsl(': ('NPP',),
+ u'hsla(': ('NPPN',)
+ }
+ if check not in checks[functiontype]:
+ self._log.error(u'ColorValue has invalid %s) parameters: '
+ u'%s (N=Number, P=Percentage)' %
+ (functiontype, check))
+
+ self._colorType = t
+ self._red, self._green, self._blue, self._alpha = tuple(rgba)
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_ColorValue(self),
+ _setCssText,
+ doc=u"String value of this value.")
+
+ value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True),
+ doc=u'Same as cssText but without comments.')
+
+ type = property(lambda self: Value.COLOR_VALUE,
+ doc=u"Type is fixed to Value.COLOR_VALUE.")
+
+ def _getName(self):
+ for n, v in self.COLORS.items():
+ if v == (self.red, self.green, self.blue, self.alpha):
+ return n
+
+ colorType = property(lambda self: self._colorType,
+ doc=u"IDENT (red), HASH (#f00) or FUNCTION (rgb(255, 0, 0).")
+
+ name = property(_getName,
+ doc=u'Name of the color if known (in ColorValue.COLORS) '
+ u'else None')
+
+ red = property(lambda self: self._red,
+ doc=u'red part as integer between 0 and 255')
+ green = property(lambda self: self._green,
+ doc=u'green part as integer between 0 and 255')
+ blue = property(lambda self: self._blue,
+ doc=u'blue part as integer between 0 and 255')
+ alpha = property(lambda self: self._alpha,
+ doc=u'alpha part as float between 0.0 and 1.0')
+
+class DimensionValue(Value):
+ """
+ A numerical value with an optional dimenstion like e.g. "px" or "%".
+
+ Covers DIMENSION, PERCENTAGE or NUMBER values.
+ """
+ __reNumDim = re.compile(ur'^(\d*\.\d+|\d+)(.*)$', re.I | re.U | re.X)
+ _dimension = None
+ _sign = None
+
+ def __str__(self):
+ return u""\
+ % (self.__class__.__name__,
+ self.type, self.value, self.dimension, self.cssText,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+
+ prods = Sequence(PreDef.unary(),
+ Choice(PreDef.dimension(stop=True),
+ PreDef.number(stop=True),
+ PreDef.percentage(stop=True)
+ )
+ )
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ u'DimensionValue',
+ prods)
+ if ok:
+ sign = val = u''
+ dim = type_ = None
+
+ # find
+ for item in seq:
+ if item.value in u'+-':
+ sign = item.value
+ else:
+ type_ = item.type
+
+ # number + optional dim
+ v, d = self.__reNumDim.findall(
+ normalize(item.value))[0]
+ if u'.' in v:
+ val = float(sign + v)
+ else:
+ val = int(sign + v)
+ if d:
+ dim = d
+
+ self._sign = sign
+ self._value = val
+ self._dimension = dim
+ self._type = type_
+
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_Value(self),
+ _setCssText,
+ doc=u"String value of this value including dimension.")
+
+ dimension = property(lambda self: self._dimension, #_setValue,
+ doc=u"Dimension if a DIMENSION or PERCENTAGE value, "
+ u"else None")
+class URIValue(Value):
+ """
+ An URI value like ``url(example.png)``.
+ """
+ _type = Value.URI
+ _uri = Value._value
+
+ def __str__(self):
+ return u""\
+ % (self.__class__.__name__,
+ self.type, self.value, self.uri, self.cssText,
+ id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+
+ prods = Sequence(PreDef.uri(stop=True))
+
+ ok, seq, store, unused = ProdParser().parse(cssText, u'URIValue', prods)
+ if ok:
+ # only 1 value only anyway
+ self._type = seq[0].type
+ self._value = seq[0].value
+
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_Value(self),
+ _setCssText,
+ doc=u'String value of this value.')
+
+ def _setUri(self, uri):
+ # TODO: check?
+ self._value = uri
+
+ uri = property(lambda self: self._value, _setUri,
+ doc=u"Actual URL without delimiters or the empty string")
+
+ def absoluteUri(self):
+ """Actual URL, made absolute if possible, else same as `uri`."""
+ # Ancestry: PropertyValue, Property, CSSStyleDeclaration, CSSStyleRule,
+ # CSSStyleSheet
+ try:
+ # TODO: better way?
+ styleSheet = self.parent.parent.parent.parentRule.parentStyleSheet
+ except AttributeError, e:
+ return self.uri
+ else:
+ return urlparse.urljoin(styleSheet.href, self.uri)
+
+ absoluteUri = property(absoluteUri, doc=absoluteUri.__doc__)
+
+
+class CSSFunction(Value):
+ """
+ A function value.
+ """
+ _functionName = 'Function'
+
+ def _productions(self):
+ """Return definition used for parsing."""
+ types = self._prods # rename!
+
+ itemProd = Choice(_ColorProd(self),
+ _DimensionProd(self),
+ _URIProd(self),
+ _ValueProd(self),
+ #_CalcValueProd(self),
+ _CSSVariableProd(self),
+ _CSSFunctionProd(self)
+ )
+ funcProds = Sequence(Prod(name='FUNCTION',
+ match=lambda t, v: t == types.FUNCTION,
+ toSeq=lambda t, tokens: (t[0],
+ normalize(t[1]))),
+ Choice(Sequence(itemProd,
+ Sequence(PreDef.comma(),
+ itemProd,
+ minmax=lambda: (0, None)),
+ PreDef.funcEnd(stop=True)),
+ PreDef.funcEnd(stop=True))
+ )
+ return funcProds
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ self.type,
+ self._productions())
+ if ok:
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_CSSFunction(self),
+ _setCssText,
+ doc=u"String value of this value.")
+
+ value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True),
+ doc=u'Same as cssText but without comments.')
+
+ type = property(lambda self: Value.FUNCTION,
+ doc=u"Type is fixed to Value.FUNCTION.")
+
+class MSValue(CSSFunction):
+ """An IE specific Microsoft only function value which is much looser
+ in what is syntactically allowed."""
+ _functionName = 'MSValue'
+
+ def _productions(self):
+ """Return definition used for parsing."""
+ types = self._prods # rename!
+
+ func = Prod(name='MSValue-Sub',
+ match=lambda t, v: t == self._prods.FUNCTION,
+ toSeq=lambda t, tokens: (MSValue._functionName,
+ MSValue(pushtoken(t,
+ tokens
+ ),
+ parent=self
+ )
+ )
+ )
+
+
+ funcProds = Sequence(Prod(name='FUNCTION',
+ match=lambda t, v: t == types.FUNCTION,
+ toSeq=lambda t, tokens: (t[0], t[1])
+ ),
+ Sequence(Choice(_ColorProd(self),
+ _DimensionProd(self),
+ _URIProd(self),
+ _ValueProd(self),
+ _MSValueProd(self),
+ #_CalcValueProd(self),
+ _CSSVariableProd(self),
+ func,
+ #_CSSFunctionProd(self),
+ Prod(name='MSValuePart',
+ match=lambda t, v: v != u')',
+ toSeq=lambda t, tokens: (t[0], t[1])
+ )
+ ),
+ minmax=lambda: (0, None)
+ ),
+ PreDef.funcEnd(stop=True)
+ )
+ return funcProds
+
+ def _setCssText(self, cssText):
+ super(MSValue, self)._setCssText(cssText)
+
+ cssText = property(lambda self: cssutils.ser.do_css_MSValue(self),
+ _setCssText,
+ doc=u"String value of this value.")
+
+
+class CSSVariable(CSSFunction):
+ """The CSSVariable represents a CSS variables like ``var(varname)``.
+
+ A variable has a (nonnormalized!) `name` and a `value` which is
+ tried to be resolved from any available CSSVariablesRule definition.
+ """
+ _functionName = 'CSSVariable'
+ _name = None
+
+ def __str__(self):
+ return u"" % (
+ self.__class__.__name__, self.name, self.value, id(self))
+
+ def _setCssText(self, cssText):
+ self._checkReadonly()
+
+ types = self._prods # rename!
+ prods = Sequence(Prod(name='var',
+ match=lambda t, v: t == types.FUNCTION and
+ normalize(v) == u'var('
+ ),
+ PreDef.ident(toStore='ident'),
+ PreDef.funcEnd(stop=True))
+
+ # store: name of variable
+ store = {'ident': None}
+ ok, seq, store, unused = ProdParser().parse(cssText,
+ u'CSSVariable',
+ prods)
+ if ok:
+ self._name = store['ident'].value
+ self._setSeq(seq)
+ self.wellformed = ok
+
+ cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self),
+ _setCssText, doc=u"String representation of variable.")
+
+ # TODO: writable? check if var (value) available?
+ name = property(lambda self: self._name,
+ doc=u"The name identifier of this variable referring to "
+ u"a value in a "
+ u":class:`cssutils.css.CSSVariablesDeclaration`.")
+
+ type = property(lambda self: Value.VARIABLE,
+ doc=u"Type is fixed to Value.VARIABLE.")
+
+ def _getValue(self):
+ "Find contained sheet and @variables there"
+ rel = self
+ while True:
+ # find node which has parentRule to get to StyleSheet
+ if hasattr(rel, 'parent'):
+ rel = rel.parent
+ else:
+ break
+ try:
+ variables = rel.parentRule.parentStyleSheet.variables
+ except AttributeError:
+ return None
+ else:
+ try:
+ return variables[self.name]
+ except KeyError:
+ return None
+
+ value = property(_getValue,
+ doc=u'The resolved actual value or None.')
+
+
+# helper for productions
+def _ValueProd(parent, nextSor=False):
+ return Prod(name='Value',
+ match=lambda t, v: t in ('IDENT', 'STRING', 'UNICODE-RANGE'),
+ nextSor = nextSor,
+ toSeq=lambda t, tokens: ('Value', Value(
+ pushtoken(t,
+ tokens),
+ parent=parent)
+ )
+ )
+
+
+def _DimensionProd(parent, nextSor=False):
+ return Prod(name='Dimension',
+ match=lambda t, v: t in (u'DIMENSION',
+ u'NUMBER',
+ u'PERCENTAGE') or v in u'+-',
+ nextSor = nextSor,
+ toSeq=lambda t, tokens: (t[0], DimensionValue(
+ pushtoken(t,
+ tokens),
+ parent=parent)
+ )
+ )
+
+def _URIProd(parent, nextSor=False):
+ return Prod(name='URIValue',
+ match=lambda t, v: t == 'URI',
+ nextSor = nextSor,
+ toSeq=lambda t, tokens: ('URIValue', URIValue(
+ pushtoken(t,
+ tokens),
+ parent=parent)
+ )
+ )
+
+reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$')
+
+def _ColorProd(parent, nextSor=False):
+ return Prod(name='ColorValue',
+ match=lambda t, v:
+ (t == 'HASH' and
+ reHexcolor.match(v)
+ ) or
+ (t == 'FUNCTION' and
+ normalize(v) in (u'rgb(',
+ u'rgba(',
+ u'hsl(',
+ u'hsla(')
+ ) or
+ (t == 'IDENT' and
+ normalize(v) in ColorValue.COLORS.keys()
+ ),
+ nextSor = nextSor,
+ toSeq=lambda t, tokens: ('ColorValue', ColorValue(
+ pushtoken(t,
+ tokens),
+ parent=parent)
+ )
+ )
+
+def _CSSFunctionProd(parent, nextSor=False):
+ return PreDef.function(nextSor=nextSor,
+ toSeq=lambda t, tokens: (CSSFunction._functionName,
+ CSSFunction(
+ pushtoken(t, tokens),
+ parent=parent)
+ )
+ )
+
+def _CSSVariableProd(parent, nextSor=False):
+ return PreDef.variable(nextSor=nextSor,
+ toSeq=lambda t, tokens: (CSSVariable._functionName,
+ CSSVariable(
+ pushtoken(t, tokens),
+ parent=parent)
+ )
+ )
+
+def _MSValueProd(parent, nextSor=False):
+ return Prod(name=MSValue._functionName,
+ match=lambda t, v: (#t == self._prods.FUNCTION and (
+ normalize(v) in (u'expression(',
+ u'alpha(',
+ u'blur(',
+ u'chroma(',
+ u'dropshadow(',
+ u'fliph(',
+ u'flipv(',
+ u'glow(',
+ u'gray(',
+ u'invert(',
+ u'mask(',
+ u'shadow(',
+ u'wave(',
+ u'xray(') or
+ v.startswith(u'progid:DXImageTransform.Microsoft.')
+ ),
+ nextSor=nextSor,
+ toSeq=lambda t, tokens: (MSValue._functionName,
+ MSValue(pushtoken(t,
+ tokens
+ ),
+ parent=parent
+ )
+ )
+ )
diff --git a/libs/cssutils/css2productions.py b/libs/cssutils/css2productions.py
new file mode 100755
index 00000000..a435e71e
--- /dev/null
+++ b/libs/cssutils/css2productions.py
@@ -0,0 +1,131 @@
+"""productions for CSS 2.1
+
+CSS2_1_MACROS and CSS2_1_PRODUCTIONS are from both
+http://www.w3.org/TR/CSS21/grammar.html and
+http://www.w3.org/TR/css3-syntax/#grammar0
+
+
+"""
+__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+# option case-insensitive
+MACROS = {
+ 'h': r'[0-9a-f]',
+ #'nonascii': r'[\200-\377]',
+ 'nonascii': r'[^\0-\177]', # CSS3
+ 'unicode': r'\\{h}{1,6}(\r\n|[ \t\r\n\f])?',
+
+ 'escape': r'{unicode}|\\[^\r\n\f0-9a-f]',
+ 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}',
+ 'nmchar': r'[_a-zA-Z0-9-]|{nonascii}|{escape}',
+ 'string1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*\"',
+ 'string2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*\'",
+ 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*',
+ 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*",
+ 'comment': r'\/\*[^*]*\*+([^/*][^*]*\*+)*\/',
+ # CSS list 080725 19:43
+ # \/\*([^*\\]|{escape})*\*+(([^/*\\]|{escape})[^*]*\*+)*\/
+
+ 'ident': r'[-]?{nmstart}{nmchar}*',
+ 'name': r'{nmchar}+',
+ # CHANGED TO SPEC: added "-?"
+ 'num': r'-?[0-9]*\.[0-9]+|[0-9]+',
+ 'string': r'{string1}|{string2}',
+ 'invalid': r'{invalid1}|{invalid2}',
+ 'url': r'([!#$%&*-~]|{nonascii}|{escape})*',
+ 's': r'[ \t\r\n\f]+',
+ 'w': r'{s}?',
+ 'nl': r'\n|\r\n|\r|\f',
+ 'range': r'\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h})))))',
+
+ 'A': r'a|\\0{0,4}(41|61)(\r\n|[ \t\r\n\f])?',
+ 'C': r'c|\\0{0,4}(43|63)(\r\n|[ \t\r\n\f])?',
+ 'D': r'd|\\0{0,4}(44|64)(\r\n|[ \t\r\n\f])?',
+ 'E': r'e|\\0{0,4}(45|65)(\r\n|[ \t\r\n\f])?',
+ 'F': r'f|\\0{0,4}(46|66)(\r\n|[ \t\r\n\f])?',
+ 'G': r'g|\\0{0,4}(47|67)(\r\n|[ \t\r\n\f])?|\\g',
+ 'H': r'h|\\0{0,4}(48|68)(\r\n|[ \t\r\n\f])?|\\h',
+ 'I': r'i|\\0{0,4}(49|69)(\r\n|[ \t\r\n\f])?|\\i',
+ 'K': r'k|\\0{0,4}(4b|6b)(\r\n|[ \t\r\n\f])?|\\k',
+ 'M': r'm|\\0{0,4}(4d|6d)(\r\n|[ \t\r\n\f])?|\\m',
+ 'N': r'n|\\0{0,4}(4e|6e)(\r\n|[ \t\r\n\f])?|\\n',
+ 'O': r'o|\\0{0,4}(51|71)(\r\n|[ \t\r\n\f])?|\\o',
+ 'P': r'p|\\0{0,4}(50|70)(\r\n|[ \t\r\n\f])?|\\p',
+ 'R': r'r|\\0{0,4}(52|72)(\r\n|[ \t\r\n\f])?|\\r',
+ 'S': r's|\\0{0,4}(53|73)(\r\n|[ \t\r\n\f])?|\\s',
+ 'T': r't|\\0{0,4}(54|74)(\r\n|[ \t\r\n\f])?|\\t',
+ 'X': r'x|\\0{0,4}(58|78)(\r\n|[ \t\r\n\f])?|\\x',
+ 'Z': r'z|\\0{0,4}(5a|7a)(\r\n|[ \t\r\n\f])?|\\z',
+ }
+
+PRODUCTIONS = [
+ ('URI', r'url\({w}{string}{w}\)'), #"url("{w}{string}{w}")" {return URI;}
+ ('URI', r'url\({w}{url}{w}\)'), #"url("{w}{url}{w}")" {return URI;}
+ ('FUNCTION', r'{ident}\('), #{ident}"(" {return FUNCTION;}
+
+ ('IMPORT_SYM', r'@{I}{M}{P}{O}{R}{T}'), #"@import" {return IMPORT_SYM;}
+ ('PAGE_SYM', r'@{P}{A}{G}{E}'), #"@page" {return PAGE_SYM;}
+ ('MEDIA_SYM', r'@{M}{E}{D}{I}{A}'), #"@media" {return MEDIA_SYM;}
+ ('FONT_FACE_SYM', r'@{F}{O}{N}{T}\-{F}{A}{C}{E}'), #"@font-face" {return FONT_FACE_SYM;}
+
+ # CHANGED TO SPEC: only @charset
+ ('CHARSET_SYM', r'@charset '), #"@charset " {return CHARSET_SYM;}
+
+ ('NAMESPACE_SYM', r'@{N}{A}{M}{E}{S}{P}{A}{C}{E}'), #"@namespace" {return NAMESPACE_SYM;}
+
+ # CHANGED TO SPEC: ATKEYWORD
+ ('ATKEYWORD', r'\@{ident}'),
+
+ ('IDENT', r'{ident}'), #{ident} {return IDENT;}
+ ('STRING', r'{string}'), #{string} {return STRING;}
+ ('INVALID', r'{invalid}'), # {return INVALID; /* unclosed string */}
+ ('HASH', r'\#{name}'), #"#"{name} {return HASH;}
+ ('PERCENTAGE', r'{num}%'), #{num}% {return PERCENTAGE;}
+ ('LENGTH', r'{num}{E}{M}'), #{num}em {return EMS;}
+ ('LENGTH', r'{num}{E}{X}'), #{num}ex {return EXS;}
+ ('LENGTH', r'{num}{P}{X}'), #{num}px {return LENGTH;}
+ ('LENGTH', r'{num}{C}{M}'), #{num}cm {return LENGTH;}
+ ('LENGTH', r'{num}{M}{M}'), #{num}mm {return LENGTH;}
+ ('LENGTH', r'{num}{I}{N}'), #{num}in {return LENGTH;}
+ ('LENGTH', r'{num}{P}{T}'), #{num}pt {return LENGTH;}
+ ('LENGTH', r'{num}{P}{C}'), #{num}pc {return LENGTH;}
+ ('ANGLE', r'{num}{D}{E}{G}'), #{num}deg {return ANGLE;}
+ ('ANGLE', r'{num}{R}{A}{D}'), #{num}rad {return ANGLE;}
+ ('ANGLE', r'{num}{G}{R}{A}{D}'), #{num}grad {return ANGLE;}
+ ('TIME', r'{num}{M}{S}'), #{num}ms {return TIME;}
+ ('TIME', r'{num}{S}'), #{num}s {return TIME;}
+ ('FREQ', r'{num}{H}{Z}'), #{num}Hz {return FREQ;}
+ ('FREQ', r'{num}{K}{H}{Z}'), #{num}kHz {return FREQ;}
+ ('DIMEN', r'{num}{ident}'), #{num}{ident} {return DIMEN;}
+ ('NUMBER', r'{num}'), #{num} {return NUMBER;}
+ #('UNICODERANGE', r'U\+{range}'), #U\+{range} {return UNICODERANGE;}
+ #('UNICODERANGE', r'U\+{h}{1,6}-{h}{1,6}'), #U\+{h}{1,6}-{h}{1,6} {return UNICODERANGE;}
+ # --- CSS3 ---
+ ('UNICODE-RANGE', r'[0-9A-F?]{1,6}(\-[0-9A-F]{1,6})?'),
+ ('CDO', r'\<\!\-\-'), #"" {return CDC;}
+ ('S', r'{s}'),# {return S;}
+
+ # \/\*[^*]*\*+([^/*][^*]*\*+)*\/ /* ignore comments */
+ # {s}+\/\*[^*]*\*+([^/*][^*]*\*+)*\/ {unput(' '); /*replace by space*/}
+
+ ('INCLUDES', r'\~\='), #"~=" {return INCLUDES;}
+ ('DASHMATCH', r'\|\='), #"|=" {return DASHMATCH;}
+ ('LBRACE', r'\{'), #{w}"{" {return LBRACE;}
+ ('PLUS', r'\+'), #{w}"+" {return PLUS;}
+ ('GREATER', r'\>'), #{w}">" {return GREATER;}
+ ('COMMA', r'\,'), #{w}"," {return COMMA;}
+ ('IMPORTANT_SYM', r'\!({w}|{comment})*{I}{M}{P}{O}{R}{T}{A}{N}{T}'), #"!{w}important" {return IMPORTANT_SYM;}
+ ('COMMENT', '\/\*[^*]*\*+([^/][^*]*\*+)*\/'), # /* ignore comments */
+ ('CLASS', r'\.'), #. {return *yytext;}
+
+ # --- CSS3! ---
+ ('CHAR', r'[^"\']'),
+ ]
+
+class CSSProductions(object):
+ pass
+for i, t in enumerate(PRODUCTIONS):
+ setattr(CSSProductions, t[0].replace('-', '_'), t[0])
\ No newline at end of file
diff --git a/libs/cssutils/cssproductions.py b/libs/cssutils/cssproductions.py
new file mode 100755
index 00000000..9f7b3d80
--- /dev/null
+++ b/libs/cssutils/cssproductions.py
@@ -0,0 +1,124 @@
+"""productions for cssutils based on a mix of CSS 2.1 and CSS 3 Syntax
+productions
+
+- http://www.w3.org/TR/css3-syntax
+- http://www.w3.org/TR/css3-syntax/#grammar0
+
+open issues
+ - numbers contain "-" if present
+ - HASH: #aaa is, #000 is not anymore,
+ CSS2.1: 'nmchar': r'[_a-z0-9-]|{nonascii}|{escape}',
+ CSS3: 'nmchar': r'[_a-z-]|{nonascii}|{escape}',
+"""
+__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+# a complete list of css3 macros
+MACROS = {
+ 'nonascii': r'[^\0-\177]',
+ 'unicode': r'\\[0-9A-Fa-f]{1,6}(?:{nl}|{s})?',
+ #'escape': r'{unicode}|\\[ -~\200-\777]',
+ 'escape': r'{unicode}|\\[^\n\r\f0-9a-f]',
+ 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}',
+ 'nmchar': r'[-_a-zA-Z0-9]|{nonascii}|{escape}',
+ 'string1': r'"([^\n\r\f\\"]|\\{nl}|{escape})*"',
+ 'string2': r"'([^\n\r\f\\']|\\{nl}|{escape})*'",
+ 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*',
+ 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*",
+
+ 'comment': r'\/\*[^*]*\*+([^/][^*]*\*+)*\/',
+ 'ident': r'[-]?{nmstart}{nmchar}*',
+ 'name': r'{nmchar}+',
+ 'num': r'[0-9]*\.[0-9]+|[0-9]+', #r'[-]?\d+|[-]?\d*\.\d+',
+ 'string': r'{string1}|{string2}',
+ # from CSS2.1
+ 'invalid': r'{invalid1}|{invalid2}',
+ 'url': r'[\x09\x21\x23-\x26\x28\x2a-\x7E]|{nonascii}|{escape}',
+
+ 's': r'\t|\r|\n|\f|\x20',
+ 'w': r'{s}*',
+ 'nl': r'\n|\r\n|\r|\f',
+
+ 'A': r'A|a|\\0{0,4}(?:41|61)(?:\r\n|[ \t\r\n\f])?',
+ 'B': r'B|b|\\0{0,4}(?:42|62)(?:\r\n|[ \t\r\n\f])?',
+ 'C': r'C|c|\\0{0,4}(?:43|63)(?:\r\n|[ \t\r\n\f])?',
+ 'D': r'D|d|\\0{0,4}(?:44|64)(?:\r\n|[ \t\r\n\f])?',
+ 'E': r'E|e|\\0{0,4}(?:45|65)(?:\r\n|[ \t\r\n\f])?',
+ 'F': r'F|f|\\0{0,4}(?:46|66)(?:\r\n|[ \t\r\n\f])?',
+ 'G': r'G|g|\\0{0,4}(?:47|67)(?:\r\n|[ \t\r\n\f])?|\\G|\\g',
+ 'H': r'H|h|\\0{0,4}(?:48|68)(?:\r\n|[ \t\r\n\f])?|\\H|\\h',
+ 'I': r'I|i|\\0{0,4}(?:49|69)(?:\r\n|[ \t\r\n\f])?|\\I|\\i',
+ 'K': r'K|k|\\0{0,4}(?:4b|6b)(?:\r\n|[ \t\r\n\f])?|\\K|\\k',
+ 'L': r'L|l|\\0{0,4}(?:4c|6c)(?:\r\n|[ \t\r\n\f])?|\\L|\\l',
+ 'M': r'M|m|\\0{0,4}(?:4d|6d)(?:\r\n|[ \t\r\n\f])?|\\M|\\m',
+ 'N': r'N|n|\\0{0,4}(?:4e|6e)(?:\r\n|[ \t\r\n\f])?|\\N|\\n',
+ 'O': r'O|o|\\0{0,4}(?:4f|6f)(?:\r\n|[ \t\r\n\f])?|\\O|\\o',
+ 'P': r'P|p|\\0{0,4}(?:50|70)(?:\r\n|[ \t\r\n\f])?|\\P|\\p',
+ 'R': r'R|r|\\0{0,4}(?:52|72)(?:\r\n|[ \t\r\n\f])?|\\R|\\r',
+ 'S': r'S|s|\\0{0,4}(?:53|73)(?:\r\n|[ \t\r\n\f])?|\\S|\\s',
+ 'T': r'T|t|\\0{0,4}(?:54|74)(?:\r\n|[ \t\r\n\f])?|\\T|\\t',
+ 'U': r'U|u|\\0{0,4}(?:55|75)(?:\r\n|[ \t\r\n\f])?|\\U|\\u',
+ 'V': r'V|v|\\0{0,4}(?:56|76)(?:\r\n|[ \t\r\n\f])?|\\V|\\v',
+ 'X': r'X|x|\\0{0,4}(?:58|78)(?:\r\n|[ \t\r\n\f])?|\\X|\\x',
+ 'Z': r'Z|z|\\0{0,4}(?:5a|7a)(?:\r\n|[ \t\r\n\f])?|\\Z|\\z',
+ }
+
+# The following productions are the complete list of tokens
+# used by cssutils, a mix of CSS3 and some CSS2.1 productions.
+# The productions are **ordered**:
+PRODUCTIONS = [
+ # UTF8_BOM or UTF8_BOM_SIG will only be checked at beginning of CSS
+ ('BOM', '\xfe\xff|\xef\xbb\xbf'),
+
+ ('S', r'{s}+'), # 1st in list of general productions
+ ('URI', r'{U}{R}{L}\({w}({string}|{url}*){w}\)'),
+ ('FUNCTION', r'{ident}\('),
+ ('UNICODE-RANGE', r'{U}\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?'),
+ ('IDENT', r'{ident}'),
+ ('DIMENSION', r'{num}{ident}'),
+ ('PERCENTAGE', r'{num}\%'),
+ ('NUMBER', r'{num}'),
+ ('HASH', r'\#{name}'),
+ ('COMMENT', r'{comment}'), #r'\/\*[^*]*\*+([^/][^*]*\*+)*\/'),
+ ('STRING', r'{string}'),
+ ('INVALID', r'{invalid}'), # from CSS2.1
+ ('ATKEYWORD', r'@{ident}'), # other keywords are done in the tokenizer
+ ('INCLUDES', '\~\='),
+ ('DASHMATCH', r'\|\='),
+ ('PREFIXMATCH', r'\^\='),
+ ('SUFFIXMATCH', r'\$\='),
+ ('SUBSTRINGMATCH', r'\*\='),
+ ('CDO', r'\<\!\-\-'),
+ ('CDC', r'\-\-\>'),
+ ('CHAR', r'[^"\']') # MUST always be last
+ # valid ony at start so not checked everytime
+ #('CHARSET_SYM', r'@charset '), # from Errata includes ending space!
+ # checked specially if fullsheet is parsed
+ ]
+
+
+
+class CSSProductions(object):
+ """
+ most attributes are set later
+ """
+ EOF = True
+ # removed from productions as they simply are ATKEYWORD until
+ # tokenizing
+ CHARSET_SYM = u'CHARSET_SYM'
+ FONT_FACE_SYM = u'FONT_FACE_SYM'
+ MEDIA_SYM = u'MEDIA_SYM'
+ IMPORT_SYM = u'IMPORT_SYM'
+ NAMESPACE_SYM = u'NAMESPACE_SYM'
+ PAGE_SYM = u'PAGE_SYM'
+ VARIABLES_SYM = u'VARIABLES_SYM'
+
+for i, t in enumerate(PRODUCTIONS):
+ setattr(CSSProductions, t[0].replace('-', '_'), t[0])
+
+
+# may be enabled by settings.set
+_DXImageTransform = (u'FUNCTION',
+ ur'progid\:DXImageTransform\.Microsoft\..+\('
+ )
diff --git a/libs/cssutils/errorhandler.py b/libs/cssutils/errorhandler.py
new file mode 100755
index 00000000..916adff5
--- /dev/null
+++ b/libs/cssutils/errorhandler.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+"""cssutils ErrorHandler
+
+ErrorHandler
+ used as log with usual levels (debug, info, warn, error)
+
+ if instanciated with ``raiseExceptions=True`` raises exeptions instead
+ of logging
+
+log
+ defaults to instance of ErrorHandler for any kind of log message from
+ lexerm, parser etc.
+
+ - raiseExceptions = [False, True]
+ - setloglevel(loglevel)
+"""
+__all__ = ['ErrorHandler']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+import logging
+import urllib2
+import xml.dom
+
+class _ErrorHandler(object):
+ """
+ handles all errors and log messages
+ """
+ def __init__(self, log, defaultloglevel=logging.INFO,
+ raiseExceptions=True):
+ """
+ inits log if none given
+
+ log
+ for parse messages, default logs to sys.stderr
+ defaultloglevel
+ if none give this is logging.DEBUG
+ raiseExceptions
+ - True: Errors will be raised e.g. during building
+ - False: Errors will be written to the log, this is the
+ default behaviour when parsing
+ """
+ # may be disabled during setting of known valid items
+ self.enabled = True
+
+ if log:
+ self._log = log
+ else:
+ import sys
+ self._log = logging.getLogger('CSSUTILS')
+ hdlr = logging.StreamHandler(sys.stderr)
+ formatter = logging.Formatter('%(levelname)s\t%(message)s')
+ hdlr.setFormatter(formatter)
+ self._log.addHandler(hdlr)
+ self._log.setLevel(defaultloglevel)
+
+ self.raiseExceptions = raiseExceptions
+
+ def __getattr__(self, name):
+ "use self._log items"
+ calls = ('debug', 'info', 'warn', 'error', 'critical', 'fatal')
+ other = ('setLevel', 'getEffectiveLevel', 'addHandler', 'removeHandler')
+
+ if name in calls:
+ self._logcall = getattr(self._log, name)
+ return self.__handle
+ elif name in other:
+ return getattr(self._log, name)
+ else:
+ raise AttributeError(
+ '(errorhandler) No Attribute %r found' % name)
+
+ def __handle(self, msg=u'', token=None, error=xml.dom.SyntaxErr,
+ neverraise=False, args=None):
+ """
+ handles all calls
+ logs or raises exception
+ """
+ if self.enabled:
+ if error is None:
+ error = xml.dom.SyntaxErr
+
+ line, col = None, None
+ if token:
+ if isinstance(token, tuple):
+ value, line, col = token[1], token[2], token[3]
+ else:
+ value, line, col = token.value, token.line, token.col
+ msg = u'%s [%s:%s: %s]' % (
+ msg, line, col, value)
+
+ if error and self.raiseExceptions and not neverraise:
+ if isinstance(error, urllib2.HTTPError) or isinstance(error, urllib2.URLError):
+ raise
+ elif issubclass(error, xml.dom.DOMException):
+ error.line = line
+ error.col = col
+ raise error(msg)
+ else:
+ self._logcall(msg)
+
+ def setLog(self, log):
+ """set log of errorhandler's log"""
+ self._log = log
+
+
+class ErrorHandler(_ErrorHandler):
+ "Singleton, see _ErrorHandler"
+ instance = None
+
+ def __init__(self,
+ log=None, defaultloglevel=logging.INFO, raiseExceptions=True):
+
+ if ErrorHandler.instance is None:
+ ErrorHandler.instance = _ErrorHandler(log=log,
+ defaultloglevel=defaultloglevel,
+ raiseExceptions=raiseExceptions)
+ self.__dict__ = ErrorHandler.instance.__dict__
diff --git a/libs/cssutils/helper.py b/libs/cssutils/helper.py
new file mode 100755
index 00000000..b3c55ff3
--- /dev/null
+++ b/libs/cssutils/helper.py
@@ -0,0 +1,137 @@
+"""cssutils helper
+"""
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: errorhandler.py 1234 2008-05-22 20:26:12Z cthedot $'
+
+import os
+import re
+import sys
+import urllib
+
+class Deprecated(object):
+ """This is a decorator which can be used to mark functions
+ as deprecated. It will result in a warning being emitted
+ when the function is used.
+
+ It accepts a single paramter ``msg`` which is shown with the warning.
+ It should contain information which function or method to use instead.
+ """
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __call__(self, func):
+ def newFunc(*args, **kwargs):
+ import warnings
+ warnings.warn("Call to deprecated method %r. %s" %
+ (func.__name__, self.msg),
+ category=DeprecationWarning,
+ stacklevel=2)
+ return func(*args, **kwargs)
+ newFunc.__name__ = func.__name__
+ newFunc.__doc__ = func.__doc__
+ newFunc.__dict__.update(func.__dict__)
+ return newFunc
+
+# simple escapes, all non unicodes
+_simpleescapes = re.compile(ur'(\\[^0-9a-fA-F])').sub
+def normalize(x):
+ """
+ normalizes x, namely:
+
+ - remove any \ before non unicode sequences (0-9a-zA-Z) so for
+ x=="c\olor\" return "color" (unicode escape sequences should have
+ been resolved by the tokenizer already)
+ - lowercase
+ """
+ if x:
+ def removeescape(matchobj):
+ return matchobj.group(0)[1:]
+ x = _simpleescapes(removeescape, x)
+ return x.lower()
+ else:
+ return x
+
+def path2url(path):
+ """Return file URL of `path`"""
+ return u'file:' + urllib.pathname2url(os.path.abspath(path))
+
+def pushtoken(token, tokens):
+ """Return new generator starting with token followed by all tokens in
+ ``tokens``"""
+ # TODO: may use itertools.chain?
+ yield token
+ for t in tokens:
+ yield t
+
+def string(value):
+ """
+ Serialize value with quotes e.g.::
+
+ ``a \'string`` => ``'a \'string'``
+ """
+ # \n = 0xa, \r = 0xd, \f = 0xc
+ value = value.replace(u'\n', u'\\a ').replace(
+ u'\r', u'\\d ').replace(
+ u'\f', u'\\c ').replace(
+ u'"', u'\\"')
+
+ if value.endswith(u'\\'):
+ value = value[:-1] + u'\\\\'
+
+ return u'"%s"' % value
+
+def stringvalue(string):
+ """
+ Retrieve actual value of string without quotes. Escaped
+ quotes inside the value are resolved, e.g.::
+
+ ``'a \'string'`` => ``a 'string``
+ """
+ return string.replace(u'\\'+string[0], string[0])[1:-1]
+
+_match_forbidden_in_uri = re.compile(ur'''.*?[\(\)\s\;,'"]''', re.U).match
+def uri(value):
+ """
+ Serialize value by adding ``url()`` and with quotes if needed e.g.::
+
+ ``"`` => ``url("\"")``
+ """
+ if _match_forbidden_in_uri(value):
+ value = string(value)
+ return u'url(%s)' % value
+
+def urivalue(uri):
+ """
+ Return actual content without surrounding "url(" and ")"
+ and removed surrounding quotes too including contained
+ escapes of quotes, e.g.::
+
+ ``url("\"")`` => ``"``
+ """
+ uri = uri[uri.find('(')+1:-1].strip()
+ if uri and (uri[0] in '\'"') and (uri[0] == uri[-1]):
+ return stringvalue(uri)
+ else:
+ return uri
+
+#def normalnumber(num):
+# """
+# Return normalized number as string.
+# """
+# sign = ''
+# if num.startswith('-'):
+# sign = '-'
+# num = num[1:]
+# elif num.startswith('+'):
+# num = num[1:]
+#
+# if float(num) == 0.0:
+# return '0'
+# else:
+# if num.find('.') == -1:
+# return sign + str(int(num))
+# else:
+# a, b = num.split('.')
+# if not a:
+# a = '0'
+# return '%s%s.%s' % (sign, int(a), b)
diff --git a/libs/cssutils/parse.py b/libs/cssutils/parse.py
new file mode 100755
index 00000000..715a320a
--- /dev/null
+++ b/libs/cssutils/parse.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python
+"""A validating CSSParser"""
+__all__ = ['CSSParser']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+
+from helper import path2url
+import codecs
+import cssutils
+import os
+import sys
+import tokenize2
+import urllib
+
+from cssutils import css
+
+if sys.version_info < (2,6):
+ bytes = str
+
+class CSSParser(object):
+ """Parse a CSS StyleSheet from URL, string or file and return a DOM Level 2
+ CSS StyleSheet object.
+
+ Usage::
+
+ parser = CSSParser()
+ # optionally
+ parser.setFetcher(fetcher)
+ sheet = parser.parseFile('test1.css', 'ascii')
+ print sheet.cssText
+ """
+ def __init__(self, log=None, loglevel=None, raiseExceptions=None,
+ fetcher=None, parseComments=True,
+ validate=True):
+ """
+ :param log:
+ logging object
+ :param loglevel:
+ logging loglevel
+ :param raiseExceptions:
+ if log should simply log (default) or raise errors during
+ parsing. Later while working with the resulting sheets
+ the setting used in cssutils.log.raiseExeptions is used
+ :param fetcher:
+ see ``setFetcher(fetcher)``
+ :param parseComments:
+ if comments should be added to CSS DOM or simply omitted
+ :param validate:
+ if parsing should validate, may be overwritten in parse methods
+ """
+ if log is not None:
+ cssutils.log.setLog(log)
+ if loglevel is not None:
+ cssutils.log.setLevel(loglevel)
+
+ # remember global setting
+ self.__globalRaising = cssutils.log.raiseExceptions
+ if raiseExceptions:
+ self.__parseRaising = raiseExceptions
+ else:
+ # DEFAULT during parse
+ self.__parseRaising = False
+
+ self.__tokenizer = tokenize2.Tokenizer(doComments=parseComments)
+ self.setFetcher(fetcher)
+
+ self._validate = validate
+
+ def __parseSetting(self, parse):
+ """during parse exceptions may be handled differently depending on
+ init parameter ``raiseExceptions``
+ """
+ if parse:
+ cssutils.log.raiseExceptions = self.__parseRaising
+ else:
+ cssutils.log.raiseExceptions = self.__globalRaising
+
+ def parseStyle(self, cssText, encoding='utf-8', validate=None):
+ """Parse given `cssText` which is assumed to be the content of
+ a HTML style attribute.
+
+ :param cssText:
+ CSS string to parse
+ :param encoding:
+ It will be used to decode `cssText` if given as a (byte)
+ string.
+ :param validate:
+ If given defines if validation is used. Uses CSSParser settings as
+ fallback
+ :returns:
+ :class:`~cssutils.css.CSSStyleDeclaration`
+ """
+ self.__parseSetting(True)
+ if isinstance(cssText, bytes):
+ # TODO: use codecs.getdecoder('css') here?
+ cssText = cssText.decode(encoding)
+ if validate is None:
+ validate = self._validate
+ style = css.CSSStyleDeclaration(cssText, validating=validate)
+ self.__parseSetting(False)
+ return style
+
+ def parseString(self, cssText, encoding=None, href=None, media=None,
+ title=None,
+ validate=None):
+ """Parse `cssText` as :class:`~cssutils.css.CSSStyleSheet`.
+ Errors may be raised (e.g. UnicodeDecodeError).
+
+ :param cssText:
+ CSS string to parse
+ :param encoding:
+ If ``None`` the encoding will be read from BOM or an @charset
+ rule or defaults to UTF-8.
+ If given overrides any found encoding including the ones for
+ imported sheets.
+ It also will be used to decode `cssText` if given as a (byte)
+ string.
+ :param href:
+ The ``href`` attribute to assign to the parsed style sheet.
+ Used to resolve other urls in the parsed sheet like @import hrefs.
+ :param media:
+ The ``media`` attribute to assign to the parsed style sheet
+ (may be a MediaList, list or a string).
+ :param title:
+ The ``title`` attribute to assign to the parsed style sheet.
+ :param validate:
+ If given defines if validation is used. Uses CSSParser settings as
+ fallback
+ :returns:
+ :class:`~cssutils.css.CSSStyleSheet`.
+ """
+ self.__parseSetting(True)
+ # TODO: py3 needs bytes here!
+ if isinstance(cssText, bytes):
+ cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0]
+
+ if validate is None:
+ validate = self._validate
+
+ sheet = cssutils.css.CSSStyleSheet(href=href,
+ media=cssutils.stylesheets.MediaList(media),
+ title=title,
+ validating=validate)
+ sheet._setFetcher(self.__fetcher)
+ # tokenizing this ways closes open constructs and adds EOF
+ sheet._setCssTextWithEncodingOverride(self.__tokenizer.tokenize(cssText,
+ fullsheet=True),
+ encodingOverride=encoding)
+ self.__parseSetting(False)
+ return sheet
+
+ def parseFile(self, filename, encoding=None,
+ href=None, media=None, title=None,
+ validate=None):
+ """Retrieve content from `filename` and parse it. Errors may be raised
+ (e.g. IOError).
+
+ :param filename:
+ of the CSS file to parse, if no `href` is given filename is
+ converted to a (file:) URL and set as ``href`` of resulting
+ stylesheet.
+ If `href` is given it is set as ``sheet.href``. Either way
+ ``sheet.href`` is used to resolve e.g. stylesheet imports via
+ @import rules.
+ :param encoding:
+ Value ``None`` defaults to encoding detection via BOM or an
+ @charset rule.
+ Other values override detected encoding for the sheet at
+ `filename` including any imported sheets.
+ :returns:
+ :class:`~cssutils.css.CSSStyleSheet`.
+ """
+ if not href:
+ # prepend // for file URL, urllib does not do this?
+ #href = u'file:' + urllib.pathname2url(os.path.abspath(filename))
+ href = path2url(filename)
+
+ return self.parseString(open(filename, 'rb').read(),
+ encoding=encoding, # read returns a str
+ href=href, media=media, title=title,
+ validate=validate)
+
+ def parseUrl(self, href, encoding=None, media=None, title=None,
+ validate=None):
+ """Retrieve content from URL `href` and parse it. Errors may be raised
+ (e.g. URLError).
+
+ :param href:
+ URL of the CSS file to parse, will also be set as ``href`` of
+ resulting stylesheet
+ :param encoding:
+ Value ``None`` defaults to encoding detection via HTTP, BOM or an
+ @charset rule.
+ A value overrides detected encoding for the sheet at ``href``
+ including any imported sheets.
+ :returns:
+ :class:`~cssutils.css.CSSStyleSheet`.
+ """
+ encoding, enctype, text = cssutils.util._readUrl(href,
+ fetcher=self.__fetcher,
+ overrideEncoding=encoding)
+ if enctype == 5:
+ # do not use if defaulting to UTF-8
+ encoding = None
+
+ if text is not None:
+ return self.parseString(text, encoding=encoding,
+ href=href, media=media, title=title,
+ validate=validate)
+
+ def setFetcher(self, fetcher=None):
+ """Replace the default URL fetch function with a custom one.
+
+ :param fetcher:
+ A function which gets a single parameter
+
+ ``url``
+ the URL to read
+
+ and must return ``(encoding, content)`` where ``encoding`` is the
+ HTTP charset normally given via the Content-Type header (which may
+ simply omit the charset in which case ``encoding`` would be
+ ``None``) and ``content`` being the string (or unicode) content.
+
+ The Mimetype should be 'text/css' but this has to be checked by the
+ fetcher itself (the default fetcher emits a warning if encountering
+ a different mimetype).
+
+ Calling ``setFetcher`` with ``fetcher=None`` resets cssutils
+ to use its default function.
+ """
+ self.__fetcher = fetcher
diff --git a/libs/cssutils/prodparser.py b/libs/cssutils/prodparser.py
new file mode 100755
index 00000000..ac7825cc
--- /dev/null
+++ b/libs/cssutils/prodparser.py
@@ -0,0 +1,733 @@
+# -*- coding: utf-8 -*-
+"""Productions parser used by css and stylesheets classes to parse
+test into a cssutils.util.Seq and at the same time retrieving
+additional specific cssutils.util.Item objects for later use.
+
+TODO:
+ - ProdsParser
+ - handle EOF or STOP?
+ - handle unknown @rules
+ - handle S: maybe save to Seq? parameterized?
+ - store['_raw']: always?
+
+ - Sequence:
+ - opt first(), naive impl for now
+
+"""
+__all__ = ['ProdParser', 'Sequence', 'Choice', 'Prod', 'PreDef']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: parse.py 1418 2008-08-09 19:27:50Z cthedot $'
+
+from helper import pushtoken
+import cssutils
+import re
+import string
+import sys
+
+
+class ParseError(Exception):
+ """Base Exception class for ProdParser (used internally)."""
+ pass
+
+class Done(ParseError):
+ """Raised if Sequence or Choice is finished and no more Prods left."""
+ pass
+
+class Exhausted(ParseError):
+ """Raised if Sequence or Choice is finished but token is given."""
+ pass
+
+class Missing(ParseError):
+ """Raised if Sequence or Choice is not finished but no matching token given."""
+ pass
+
+class NoMatch(ParseError):
+ """Raised if nothing in Sequence or Choice does match."""
+ pass
+
+
+class Choice(object):
+ """A Choice of productions (Sequence or single Prod)."""
+
+ def __init__(self, *prods, **options):
+ """
+ *prods
+ Prod or Sequence objects
+ options:
+ optional=False
+ """
+ self._prods = prods
+
+ try:
+ self.optional = options['optional']
+ except KeyError, e:
+ for p in self._prods:
+ if p.optional:
+ self.optional = True
+ break
+ else:
+ self.optional = False
+
+ self.reset()
+
+ def reset(self):
+ """Start Choice from zero"""
+ self._exhausted = False
+
+ def matches(self, token):
+ """Check if token matches"""
+ for prod in self._prods:
+ if prod.matches(token):
+ return True
+ return False
+
+ def nextProd(self, token):
+ """
+ Return:
+
+ - next matching Prod or Sequence
+ - ``None`` if any Prod or Sequence is optional and no token matched
+ - raise ParseError if nothing matches and all are mandatory
+ - raise Exhausted if choice already done
+
+ ``token`` may be None but this occurs when no tokens left."""
+ if not self._exhausted:
+ optional = False
+ for x in self._prods:
+ if x.matches(token):
+ self._exhausted = True
+ x.reset()
+ return x
+ elif x.optional:
+ optional = True
+ else:
+ if not optional:
+ # None matched but also None is optional
+ raise ParseError(u'No match in %s' % self)
+ elif token:
+ raise Exhausted(u'Extra token')
+
+ def __str__(self):
+ return u'Choice(%s)' % u', '.join([str(x) for x in self._prods])
+
+
+class Sequence(object):
+ """A Sequence of productions (Choice or single Prod)."""
+ def __init__(self, *prods, **options):
+ """
+ *prods
+ Prod or Sequence objects
+ **options:
+ minmax = lambda: (1, 1)
+ callback returning number of times this sequence may run
+ """
+ self._prods = prods
+ try:
+ minmax = options['minmax']
+ except KeyError:
+ minmax = lambda: (1, 1)
+
+ self._min, self._max = minmax()
+ if self._max is None:
+ # unlimited
+ try:
+ # py2.6/3
+ self._max = sys.maxsize
+ except AttributeError:
+ # py<2.6
+ self._max = sys.maxint
+
+ self._prodcount = len(self._prods)
+ self.reset()
+
+ def matches(self, token):
+ """Called by Choice to try to find if Sequence matches."""
+ for prod in self._prods:
+ if prod.matches(token):
+ return True
+ try:
+ if not prod.optional:
+ break
+ except AttributeError:
+ pass
+ return False
+
+ def reset(self):
+ """Reset this Sequence if it is nested."""
+ self._roundstarted = False
+ self._i = 0
+ self._round = 0
+
+ def _currentName(self):
+ """Return current element of Sequence, used by name"""
+ # TODO: current impl first only if 1st if an prod!
+ for prod in self._prods[self._i:]:
+ if not prod.optional:
+ return str(prod)
+ else:
+ return 'Sequence'
+
+ optional = property(lambda self: self._min == 0)
+
+ def nextProd(self, token):
+ """Return
+
+ - next matching Prod or Choice
+ - raises ParseError if nothing matches
+ - raises Exhausted if sequence already done
+ """
+ while self._round < self._max:
+ # for this round
+ i = self._i
+ round = self._round
+ p = self._prods[i]
+ if i == 0:
+ self._roundstarted = False
+
+ # for next round
+ self._i += 1
+ if self._i == self._prodcount:
+ self._round += 1
+ self._i = 0
+
+ if p.matches(token):
+ self._roundstarted = True
+ # reset nested Choice or Prod to use from start
+ p.reset()
+ return p
+
+ elif p.optional:
+ continue
+
+ elif round < self._min:
+ raise Missing(u'Missing token for production %s' % p)
+
+ elif not token:
+ if self._roundstarted:
+ raise Missing(u'Missing token for production %s' % p)
+ else:
+ raise Done()
+
+ else:
+ raise NoMatch(u'No matching production for token')
+
+ if token:
+ raise Exhausted(u'Extra token')
+
+ def __str__(self):
+ return u'Sequence(%s)' % u', '.join([str(x) for x in self._prods])
+
+
+class Prod(object):
+ """Single Prod in Sequence or Choice."""
+ def __init__(self, name, match, optional=False,
+ toSeq=None, toStore=None,
+ stop=False, stopAndKeep=False,
+ nextSor=False, mayEnd=False,
+ storeToken=None,
+ exception=None):
+ """
+ name
+ name used for error reporting
+ match callback
+ function called with parameters tokentype and tokenvalue
+ returning True, False or raising ParseError
+ toSeq callback (optional) or False
+ calling toSeq(token, tokens) returns (type_, val) == (token[0], token[1])
+ to be appended to seq else simply unaltered (type_, val)
+
+ if False nothing is added
+
+ toStore (optional)
+ key to save util.Item to store or callback(store, util.Item)
+ optional = False
+ wether Prod is optional or not
+ stop = False
+ if True stop parsing of tokens here
+ stopAndKeep
+ if True stop parsing of tokens here but return stopping
+ token in unused tokens
+ nextSor=False
+ next is S or other like , or / (CSSValue)
+ mayEnd = False
+ no token must follow even defined by Sequence.
+ Used for operator ',/ ' currently only
+
+ storeToken = None
+ if True toStore saves simple token tuple and not and Item object
+ to store. Old style processing, TODO: resolve
+
+ exception = None
+ exception to be raised in case of error, normaly SyntaxErr
+ """
+ self._name = name
+ self.match = match
+ self.optional = optional
+ self.stop = stop
+ self.stopAndKeep = stopAndKeep
+ self.nextSor = nextSor
+ self.mayEnd = mayEnd
+ self.storeToken = storeToken
+ self.exception = exception
+
+ def makeToStore(key):
+ "Return a function used by toStore."
+ def toStore(store, item):
+ "Set or append store item."
+ if key in store:
+ _v = store[key]
+ if not isinstance(_v, list):
+ store[key] = [_v]
+ store[key].append(item)
+ else:
+ store[key] = item
+ return toStore
+
+ if toSeq or toSeq is False:
+ # called: seq.append(toSeq(value))
+ self.toSeq = toSeq
+ else:
+ self.toSeq = lambda t, tokens: (t[0], t[1])
+
+ if hasattr(toStore, '__call__'):
+ self.toStore = toStore
+ elif toStore:
+ self.toStore = makeToStore(toStore)
+ else:
+ # always set!
+ self.toStore = None
+
+ def matches(self, token):
+ """Return if token matches."""
+ if not token:
+ return False
+ type_, val, line, col = token
+ return self.match(type_, val)
+
+ def reset(self):
+ pass
+
+ def __str__(self):
+ return self._name
+
+ def __repr__(self):
+ return "" % (
+ self.__class__.__name__, self._name, id(self))
+
+
+# global tokenizer as there is only one!
+tokenizer = cssutils.tokenize2.Tokenizer()
+
+class ProdParser(object):
+ """Productions parser."""
+ def __init__(self, clear=True):
+ self.types = cssutils.cssproductions.CSSProductions
+ self._log = cssutils.log
+ if clear:
+ tokenizer.clear()
+
+ def _texttotokens(self, text):
+ """Build a generator which is the only thing that is parsed!
+ old classes may use lists etc
+ """
+ if isinstance(text, basestring):
+ # DEFAULT, to tokenize strip space
+ return tokenizer.tokenize(text.strip())
+
+ elif isinstance(text, tuple):
+ # OLD: (token, tokens) or a single token
+ if len(text) == 2:
+ # (token, tokens)
+ chain([token], tokens)
+ else:
+ # single token
+ return iter([text])
+
+ elif isinstance(text, list):
+ # OLD: generator from list
+ return iter(text)
+
+ else:
+ # DEFAULT, already tokenized, assume generator
+ return text
+
+ def _SorTokens(self, tokens, until=',/'):
+ """New tokens generator which has S tokens removed,
+ if followed by anything in ``until``, normally a ``,``."""
+ for token in tokens:
+ if token[0] == self.types.S:
+ try:
+ next_ = tokens.next()
+ except StopIteration:
+ yield token
+ else:
+ if next_[1] in until:
+ # omit S as e.g. ``,`` has been found
+ yield next_
+ elif next_[0] == self.types.COMMENT:
+ # pass COMMENT
+ yield next_
+ else:
+ yield token
+ yield next_
+
+ elif token[0] == self.types.COMMENT:
+ # pass COMMENT
+ yield token
+ else:
+ yield token
+ break
+ # normal mode again
+ for token in tokens:
+ yield token
+
+
+ def parse(self, text, name, productions, keepS=False, store=None):
+ """
+ text (or token generator)
+ to parse, will be tokenized if not a generator yet
+
+ may be:
+ - a string to be tokenized
+ - a single token, a tuple
+ - a tuple of (token, tokensGenerator)
+ - already tokenized so a tokens generator
+
+ name
+ used for logging
+ productions
+ used to parse tokens
+ keepS
+ if WS should be added to Seq or just be ignored
+ store UPDATED
+ If a Prod defines ``toStore`` the key defined there
+ is a key in store to be set or if store[key] is a list
+ the next Item is appended here.
+
+ TODO: NEEDED? :
+ Key ``raw`` is always added and holds all unprocessed
+ values found
+
+ returns
+ :wellformed: True or False
+ :seq: a filled cssutils.util.Seq object which is NOT readonly yet
+ :store: filled keys defined by Prod.toStore
+ :unusedtokens: token generator containing tokens not used yet
+ """
+ tokens = self._texttotokens(text)
+ if not tokens:
+ self._log.error(u'No content to parse.')
+ # TODO: return???
+
+ seq = cssutils.util.Seq(readonly=False)
+ if not store: # store for specific values
+ store = {}
+ prods = [productions] # stack of productions
+ wellformed = True
+
+ # while no real token is found any S are ignored
+ started = False
+ stopall = False
+ prod = None
+ # flag if default S handling should be done
+ defaultS = True
+ while True:
+ try:
+ token = tokens.next()
+ except StopIteration:
+ break
+ type_, val, line, col = token
+
+ # default productions
+ if type_ == self.types.COMMENT:
+ # always append COMMENT
+ seq.append(cssutils.css.CSSComment(val),
+ cssutils.css.CSSComment, line, col)
+ elif defaultS and type_ == self.types.S:
+ # append S (but ignore starting ones)
+ if not keepS or not started:
+ continue
+ else:
+ seq.append(val, type_, line, col)
+# elif type_ == self.types.ATKEYWORD:
+# # @rule
+# r = cssutils.css.CSSUnknownRule(cssText=val)
+# seq.append(r, type(r), line, col)
+ elif type_ == self.types.INVALID:
+ # invalidate parse
+ wellformed = False
+ self._log.error(u'Invalid token: %r' % (token,))
+ break
+ elif type_ == 'EOF':
+ # do nothing? (self.types.EOF == True!)
+ pass
+ else:
+ started = True # check S now
+ nextSor = False # reset
+
+ try:
+ while True:
+ # find next matching production
+ try:
+ prod = prods[-1].nextProd(token)
+ except (Exhausted, NoMatch), e:
+ # try next
+ prod = None
+ if isinstance(prod, Prod):
+ # found actual Prod, not a Choice or Sequence
+ break
+ elif prod:
+ # nested Sequence, Choice
+ prods.append(prod)
+ else:
+ # nested exhausted, try in parent
+ if len(prods) > 1:
+ prods.pop()
+ else:
+ raise ParseError('No match')
+ except ParseError, e:
+ wellformed = False
+ self._log.error(u'%s: %s: %r' % (name, e, token))
+ break
+ else:
+ # process prod
+ if prod.toSeq and not prod.stopAndKeep:
+ type_, val = prod.toSeq(token, tokens)
+ if val is not None:
+ seq.append(val, type_, line, col)
+ if prod.toStore:
+ if not prod.storeToken:
+ prod.toStore(store, seq[-1])
+ else:
+ # workaround for now for old style token
+ # parsing!
+ # TODO: remove when all new style
+ prod.toStore(store, token)
+
+ if prod.stop: # EOF?
+ # stop here and ignore following tokens
+ break
+
+ if prod.stopAndKeep: # e.g. ;
+ # stop here and ignore following tokens
+ # but keep this token for next run
+ tokenizer.push(token)
+ stopall = True
+ break
+
+ if prod.nextSor:
+ # following is S or other token (e.g. ",")?
+ # remove S if
+ tokens = self._SorTokens(tokens, ',/')
+ defaultS = False
+ else:
+ defaultS = True
+
+ lastprod = prod
+
+ if not stopall:
+ # stop immediately
+ while True:
+ # all productions exhausted?
+ try:
+ prod = prods[-1].nextProd(token=None)
+ except Done, e:
+ # ok
+ prod = None
+
+ except Missing, e:
+ prod = None
+ # last was a S operator which may End a Sequence, then ok
+ if hasattr(lastprod, 'mayEnd') and not lastprod.mayEnd:
+ wellformed = False
+ self._log.error(u'%s: %s' % (name, e))
+
+ except ParseError, e:
+ prod = None
+ wellformed = False
+ self._log.error(u'%s: %s' % (name, e))
+
+ else:
+ if prods[-1].optional:
+ prod = None
+ elif prod and prod.optional:
+ # ignore optional
+ continue
+
+ if prod and not prod.optional:
+ wellformed = False
+ self._log.error(u'%s: Missing token for production %r'
+ % (name, str(prod)))
+ break
+ elif len(prods) > 1:
+ # nested exhausted, next in parent
+ prods.pop()
+ else:
+ break
+
+ # trim S from end
+ seq.rstrip()
+ return wellformed, seq, store, tokens
+
+
+class PreDef(object):
+ """Predefined Prod definition for use in productions definition
+ for ProdParser instances.
+ """
+ types = cssutils.cssproductions.CSSProductions
+ reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$')
+
+ @staticmethod
+ def calc(toSeq=None, nextSor=False):
+ return Prod(name=u'calcfunction',
+ match=lambda t, v: u'calc(' == cssutils.helper.normalize(v),
+ toSeq=toSeq,
+ nextSor=nextSor)
+
+ @staticmethod
+ def char(name='char', char=u',', toSeq=None,
+ stop=False, stopAndKeep=False,
+ optional=True, nextSor=False):
+ "any CHAR"
+ return Prod(name=name, match=lambda t, v: v == char, toSeq=toSeq,
+ stop=stop, stopAndKeep=stopAndKeep, optional=optional,
+ nextSor=nextSor)
+
+ @staticmethod
+ def comma():
+ return PreDef.char(u'comma', u',')
+
+ @staticmethod
+ def dimension(nextSor=False, stop=False):
+ return Prod(name=u'dimension',
+ match=lambda t, v: t == PreDef.types.DIMENSION,
+ toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1])),
+ stop=stop,
+ nextSor=nextSor)
+
+ @staticmethod
+ def function(toSeq=None, nextSor=False):
+ return Prod(name=u'function',
+ match=lambda t, v: t == PreDef.types.FUNCTION,
+ toSeq=toSeq,
+ nextSor=nextSor)
+
+ @staticmethod
+ def funcEnd(stop=False):
+ ")"
+ return PreDef.char(u'end FUNC ")"', u')',
+ stop=stop)
+
+ @staticmethod
+ def hexcolor(stop=False, nextSor=False):
+ "#123 or #123456"
+ return Prod(name='HEX color',
+ match=lambda t, v: (
+ t == PreDef.types.HASH and
+ PreDef.reHexcolor.match(v)
+ ),
+ stop=stop,
+ nextSor=nextSor)
+
+ @staticmethod
+ def ident(stop=False, toStore=None, nextSor=False):
+ return Prod(name=u'ident',
+ match=lambda t, v: t == PreDef.types.IDENT,
+ stop=stop,
+ toStore=toStore,
+ nextSor=nextSor)
+
+ @staticmethod
+ def number(stop=False, toSeq=None, nextSor=False):
+ return Prod(name=u'number',
+ match=lambda t, v: t == PreDef.types.NUMBER,
+ stop=stop,
+ toSeq=toSeq,
+ nextSor=nextSor)
+
+ @staticmethod
+ def percentage(stop=False, toSeq=None, nextSor=False):
+ return Prod(name=u'percentage',
+ match=lambda t, v: t == PreDef.types.PERCENTAGE,
+ stop=stop,
+ toSeq=toSeq,
+ nextSor=nextSor)
+
+ @staticmethod
+ def string(stop=False, nextSor=False):
+ "string delimiters are removed by default"
+ return Prod(name=u'string',
+ match=lambda t, v: t == PreDef.types.STRING,
+ toSeq=lambda t, tokens: (t[0], cssutils.helper.stringvalue(t[1])),
+ stop=stop,
+ nextSor=nextSor)
+
+ @staticmethod
+ def S(name=u'whitespace', toSeq=None, optional=False):
+ return Prod(name=name,
+ match=lambda t, v: t == PreDef.types.S,
+ toSeq=toSeq,
+ optional=optional,
+ mayEnd=True)
+
+ @staticmethod
+ def unary(stop=False, toSeq=None, nextSor=False):
+ "+ or -"
+ return Prod(name=u'unary +-', match=lambda t, v: v in (u'+', u'-'),
+ optional=True,
+ stop=stop,
+ toSeq=toSeq,
+ nextSor=nextSor)
+
+ @staticmethod
+ def uri(stop=False, nextSor=False):
+ "'url(' and ')' are removed and URI is stripped"
+ return Prod(name=u'URI',
+ match=lambda t, v: t == PreDef.types.URI,
+ toSeq=lambda t, tokens: (t[0], cssutils.helper.urivalue(t[1])),
+ stop=stop,
+ nextSor=nextSor)
+
+ @staticmethod
+ def unicode_range(stop=False, nextSor=False):
+ "u+123456-abc normalized to lower `u`"
+ return Prod(name='unicode-range',
+ match=lambda t, v: t == PreDef.types.UNICODE_RANGE,
+ toSeq=lambda t, tokens: (t[0], t[1].lower()),
+ stop=stop,
+ nextSor=nextSor
+ )
+
+ @staticmethod
+ def variable(toSeq=None, stop=False, nextSor=False):
+ return Prod(name=u'variable',
+ match=lambda t, v: u'var(' == cssutils.helper.normalize(v),
+ toSeq=toSeq,
+ stop=stop,
+ nextSor=nextSor)
+
+ # used for MarginRule for now:
+ @staticmethod
+ def unknownrule(name=u'@', toStore=None):
+ """@rule dummy (matches ATKEYWORD to remove unknown rule tokens from
+ stream::
+
+ @x;
+ @x {...}
+
+ no nested yet!
+ """
+ def rule(tokens):
+ saved = []
+ for t in tokens:
+ saved.append(t)
+ if (t[1] == u'}' or t[1] == u';'):
+ return cssutils.css.CSSUnknownRule(saved)
+
+ return Prod(name=name,
+ match=lambda t, v: t == u'ATKEYWORD',
+ toSeq=lambda t, tokens: (u'CSSUnknownRule',
+ rule(pushtoken(t, tokens))
+ ),
+ toStore=toStore
+ )
diff --git a/libs/cssutils/profiles.py b/libs/cssutils/profiles.py
new file mode 100755
index 00000000..57c63bd4
--- /dev/null
+++ b/libs/cssutils/profiles.py
@@ -0,0 +1,791 @@
+"""CSS profiles.
+
+Profiles is based on code by Kevin D. Smith, orginally used as cssvalues,
+thanks!
+"""
+__all__ = ['Profiles']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: cssproperties.py 1116 2008-03-05 13:52:23Z cthedot $'
+
+import re
+import types
+
+class NoSuchProfileException(Exception):
+ """Raised if no profile with given name is found"""
+ pass
+
+
+# dummies, replaced in Profiles.addProfile
+_fontRegexReplacements = {
+ '__FONT_FAMILY_SINGLE': lambda f: False,
+ '__FONT_WITH_1_FAMILY': lambda f: False
+ }
+
+def _fontFamilyValidator(families):
+ """Check if ``font-family`` value is valid, regex is too slow.
+
+ Splits on ``,`` and checks each family separately.
+ Somehow naive as font-family name could contain a "," but this is unlikely.
+ Still should be a TODO.
+ """
+ match = _fontRegexReplacements['__FONT_FAMILY_SINGLE']
+
+ for f in families.split(u','):
+ if not match(f.strip()):
+ return False
+ return True
+
+def _fontValidator(font):
+ """Check if font value is valid, regex is too slow.
+
+ Checks everything before ``,`` on basic font value. Everything after should
+ be a valid font-family value.
+ """
+ if u',' in font:
+ # split off until 1st family
+ font1, families2 = font.split(u',', 1)
+ else:
+ font1, families2 = font, None
+
+ if not _fontRegexReplacements['__FONT_WITH_1_FAMILY'](font1.strip()):
+ return False
+
+ if families2 and not _fontFamilyValidator(families2):
+ return False
+
+ return True
+
+
+class Profiles(object):
+ """
+ All profiles used for validation. ``cssutils.profile`` is a
+ preset object of this class and used by all properties for validation.
+
+ Predefined profiles are (use
+ :meth:`~cssutils.profiles.Profiles.propertiesByProfile` to
+ get a list of defined properties):
+
+ :attr:`~cssutils.profiles.Profiles.CSS_LEVEL_2`
+ Properties defined by CSS2.1
+ :attr:`~cssutils.profiles.Profiles.CSS3_BASIC_USER_INTERFACE`
+ Currently resize and outline properties only
+ :attr:`~cssutils.profiles.Profiles.CSS3_BOX`
+ Currently overflow related properties only
+ :attr:`~cssutils.profiles.Profiles.CSS3_COLOR`
+ CSS 3 color properties
+ :attr:`~cssutils.profiles.Profiles.CSS3_PAGED_MEDIA`
+ As defined at http://www.w3.org/TR/css3-page/ (at 090307)
+
+ Predefined macros are:
+
+ :attr:`~cssutils.profiles.Profiles._TOKEN_MACROS`
+ Macros containing the token values as defined to CSS2
+ :attr:`~cssutils.profiles.Profiles._MACROS`
+ Additional general macros.
+
+ If you want to redefine any of these macros do this in your custom
+ macros.
+ """
+ CSS_LEVEL_2 = u'CSS Level 2.1'
+ CSS3_BACKGROUNDS_AND_BORDERS = u'CSS Backgrounds and Borders Module Level 3'
+ CSS3_BASIC_USER_INTERFACE = u'CSS3 Basic User Interface Module'
+ CSS3_BOX = CSS_BOX_LEVEL_3 = u'CSS Box Module Level 3'
+ CSS3_COLOR = CSS_COLOR_LEVEL_3 = u'CSS Color Module Level 3'
+ CSS3_FONTS = u'CSS Fonts Module Level 3'
+ CSS3_FONT_FACE = u'CSS Fonts Module Level 3 @font-face properties'
+ CSS3_PAGED_MEDIA = u'CSS3 Paged Media Module'
+ CSS3_TEXT = u'CSS Text Level 3'
+
+ _TOKEN_MACROS = {
+ 'ident': r'[-]?{nmstart}{nmchar}*',
+ 'name': r'{nmchar}+',
+ 'nmstart': r'[_a-z]|{nonascii}|{escape}',
+ 'nonascii': r'[^\0-\177]',
+ 'unicode': r'\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?',
+ 'escape': r'{unicode}|\\[ -~\200-\777]',
+ # 'escape': r'{unicode}|\\[ -~\200-\4177777]',
+ 'int': r'[-]?\d+',
+ 'nmchar': r'[\w-]|{nonascii}|{escape}',
+ 'num': r'[-]?\d+|[-]?\d*\.\d+',
+ 'positivenum': r'\d+|\d*\.\d+',
+ 'number': r'{num}',
+ 'string': r'{string1}|{string2}',
+ 'string1': r'"(\\\"|[^\"])*"',
+ 'uri': r'url\({w}({string}|(\\\)|[^\)])+){w}\)',
+ 'string2': r"'(\\\'|[^\'])*'",
+ 'nl': r'\n|\r\n|\r|\f',
+ 'w': r'\s*',
+ }
+ _MACROS = {
+ 'hexcolor': r'#[0-9a-f]{3}|#[0-9a-f]{6}',
+ 'rgbcolor': r'rgb\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\)|rgb\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\)',
+ 'namedcolor': r'(transparent|orange|maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray)',
+ 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)',
+ 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{uicolor}',
+ #'color': r'(maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray|ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)|#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\({w}{int}{w},{w}{int}{w},{w}{int}{w}\)|rgb\({w}{num}%{w},{w}{num}%{w},{w}{num}%{w}\)',
+ 'integer': r'{int}',
+ 'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)',
+ 'positivelength': r'0|{positivenum}(em|ex|px|in|cm|mm|pt|pc)',
+ 'angle': r'0|{num}(deg|grad|rad)',
+ 'time': r'0|{num}m?s',
+ 'frequency': r'0|{num}k?Hz',
+ 'percentage': r'{num}%',
+ 'shadow': '(inset)?{w}{length}{w}{length}{w}{length}?{w}{length}?{w}{color}?'
+ }
+
+ def __init__(self, log=None):
+ """A few profiles are predefined."""
+ self._log = log
+
+ # macro cache
+ self._usedMacros = Profiles._TOKEN_MACROS.copy()
+ self._usedMacros.update(Profiles._MACROS.copy())
+
+ # to keep order, REFACTOR!
+ self._profileNames = []
+ # for reset if macro changes
+ self._rawProfiles = {}
+ # already compiled profiles: {profile: {property: checkfunc, ...}, ...}
+ self._profilesProperties = {}
+
+ self._defaultProfiles = None
+
+ self.addProfiles([(self.CSS_LEVEL_2,
+ properties[self.CSS_LEVEL_2],
+ macros[self.CSS_LEVEL_2]
+ ),
+ (self.CSS3_BACKGROUNDS_AND_BORDERS,
+ properties[self.CSS3_BACKGROUNDS_AND_BORDERS],
+ macros[self.CSS3_BACKGROUNDS_AND_BORDERS]
+ ),
+ (self.CSS3_BASIC_USER_INTERFACE,
+ properties[self.CSS3_BASIC_USER_INTERFACE],
+ macros[self.CSS3_BASIC_USER_INTERFACE]
+ ),
+ (self.CSS3_BOX,
+ properties[self.CSS3_BOX],
+ macros[self.CSS3_BOX]
+ ),
+ (self.CSS3_COLOR,
+ properties[self.CSS3_COLOR],
+ macros[self.CSS3_COLOR]
+ ),
+ (self.CSS3_FONTS,
+ properties[self.CSS3_FONTS],
+ macros[self.CSS3_FONTS]
+ ),
+ # new object for font-face only?
+ (self.CSS3_FONT_FACE,
+ properties[self.CSS3_FONT_FACE],
+ macros[self.CSS3_FONTS]
+ ),
+ (self.CSS3_PAGED_MEDIA,
+ properties[self.CSS3_PAGED_MEDIA],
+ macros[self.CSS3_PAGED_MEDIA]
+ ),
+ (self.CSS3_TEXT,
+ properties[self.CSS3_TEXT],
+ macros[self.CSS3_TEXT]
+ )
+ ])
+
+ self.__update_knownNames()
+
+ def _expand_macros(self, dictionary, macros):
+ """Expand macros in token dictionary"""
+ def macro_value(m):
+ return '(?:%s)' % macros[m.groupdict()['macro']]
+
+ for key, value in dictionary.items():
+ if not hasattr(value, '__call__'):
+ while re.search(r'{[a-z][a-z0-9-]*}', value):
+ value = re.sub(r'{(?P[a-z][a-z0-9-]*)}',
+ macro_value, value)
+ dictionary[key] = value
+
+ return dictionary
+
+ def _compile_regexes(self, dictionary):
+ """Compile all regular expressions into callable objects"""
+ for key, value in dictionary.items():
+ # might be a function (font-family) as regex is too slow
+ if not hasattr(value, '__call__') and not isinstance(value,
+ types.FunctionType):
+ value = re.compile('^(?:%s)$' % value, re.I).match
+ dictionary[key] = value
+
+ return dictionary
+
+ def __update_knownNames(self):
+ self._knownNames = []
+ for properties in self._profilesProperties.values():
+ self._knownNames.extend(properties.keys())
+
+ def _getDefaultProfiles(self):
+ "If not explicitly set same as Profiles.profiles but in reverse order."
+ if not self._defaultProfiles:
+ return self.profiles
+ else:
+ return self._defaultProfiles
+
+ def _setDefaultProfiles(self, profiles):
+ "profiles may be a single or a list of profile names"
+ if isinstance(profiles, basestring):
+ self._defaultProfiles = (profiles,)
+ else:
+ self._defaultProfiles = profiles
+
+ defaultProfiles = property(_getDefaultProfiles,
+ _setDefaultProfiles,
+ doc=u"Names of profiles to use for validation."
+ u"To use e.g. the CSS2 profile set "
+ u"``cssutils.profile.defaultProfiles = "
+ u"cssutils.profile.CSS_LEVEL_2``")
+
+ profiles = property(lambda self: self._profileNames,
+ doc=u'Names of all profiles in order as defined.')
+
+ knownNames = property(lambda self: self._knownNames,
+ doc="All known property names of all profiles.")
+
+ def _resetProperties(self, newMacros=None):
+ "reset all props from raw values as changes in macros happened"
+ # base
+ macros = Profiles._TOKEN_MACROS.copy()
+ macros.update(Profiles._MACROS.copy())
+
+ # former
+ for profile in self._profileNames:
+ macros.update(self._rawProfiles[profile]['macros'])
+
+ # new
+ if newMacros:
+ macros.update(newMacros)
+
+ # reset properties
+ self._profilesProperties.clear()
+ for profile in self._profileNames:
+ properties = self._expand_macros(
+ # keep raw
+ self._rawProfiles[profile]['properties'].copy(),
+ macros)
+ self._profilesProperties[profile] = self._compile_regexes(properties)
+
+ # save
+ self._usedMacros = macros
+
+
+ def addProfiles(self, profiles):
+ """Add a list of profiles at once. Useful as if profiles define custom
+ macros these are used in one go. Using `addProfile` instead my be
+ **very** slow instead.
+ """
+ # add macros
+ for profile, properties, macros in profiles:
+ if macros:
+ self._usedMacros.update(macros)
+ self._rawProfiles[profile] = {'macros': macros.copy()}
+
+ # only add new properties
+ for profile, properties, macros in profiles:
+ self.addProfile(profile, properties.copy(), None)
+
+
+ def addProfile(self, profile, properties, macros=None):
+ """Add a new profile with name `profile` (e.g. 'CSS level 2')
+ and the given `properties`.
+
+ :param profile:
+ the new `profile`'s name
+ :param properties:
+ a dictionary of ``{ property-name: propery-value }`` items where
+ property-value is a regex which may use macros defined in given
+ ``macros`` or the standard macros Profiles.tokens and
+ Profiles.generalvalues.
+
+ ``propery-value`` may also be a function which takes a single
+ argument which is the value to validate and which should return
+ True or False.
+ Any exceptions which may be raised during this custom validation
+ are reported or raised as all other cssutils exceptions depending
+ on cssutils.log.raiseExceptions which e.g during parsing normally
+ is False so the exceptions would be logged only.
+ :param macros:
+ may be used in the given properties definitions. There are some
+ predefined basic macros which may always be used in
+ :attr:`Profiles._TOKEN_MACROS` and :attr:`Profiles._MACROS`.
+ """
+ if macros:
+ # check if known macros would change and if yes reset properties
+ if len(set(macros.keys()).intersection(self._usedMacros.keys())):
+ self._resetProperties(newMacros=macros)
+
+ else:
+ # no replacement, simply continue
+ self._usedMacros.update(macros)
+
+ else:
+ # might have been set by addProfiles before
+ try:
+ macros = self._rawProfiles[profile]['macros']
+ except KeyError, e:
+ macros = {}
+
+ # save name and raw props/macros if macros change to completely reset
+ self._profileNames.append(profile)
+ self._rawProfiles[profile] = {'properties': properties.copy(),
+ 'macros': macros.copy()}
+ # prepare and save properties
+ properties = self._expand_macros(properties, self._usedMacros)
+ self._profilesProperties[profile] = self._compile_regexes(properties)
+
+ self.__update_knownNames()
+
+ # hack for font and font-family which are too slow with regexes
+ if '__FONT_WITH_1_FAMILY' in properties:
+ _fontRegexReplacements['__FONT_WITH_1_FAMILY'] = properties['__FONT_WITH_1_FAMILY']
+ if '__FONT_FAMILY_SINGLE' in properties:
+ _fontRegexReplacements['__FONT_FAMILY_SINGLE'] = properties['__FONT_FAMILY_SINGLE']
+
+
+ def removeProfile(self, profile=None, all=False):
+ """Remove `profile` or remove `all` profiles.
+
+ If the removed profile used custom macros all remaining profiles
+ are reset to reflect the macro changes. This may be quite an expensive
+ operation!
+
+ :param profile:
+ profile name to remove
+ :param all:
+ if ``True`` removes all profiles to start with a clean state
+ :exceptions:
+ - :exc:`cssutils.profiles.NoSuchProfileException`:
+ If given `profile` cannot be found.
+ """
+ if all:
+ self._profilesProperties.clear()
+ self._rawProfiles.clear()
+ del self._profileNames[:]
+ else:
+ reset = False
+
+ try:
+ if (self._rawProfiles[profile]['macros']):
+ reset = True
+
+ del self._profilesProperties[profile]
+ del self._rawProfiles[profile]
+ del self._profileNames[self._profileNames.index(profile)]
+ except KeyError:
+ raise NoSuchProfileException(u'No profile %r.' % profile)
+
+ else:
+ if reset:
+ # reset properties as macros were removed
+ self._resetProperties()
+
+ self.__update_knownNames()
+
+ def propertiesByProfile(self, profiles=None):
+ """Generator: Yield property names, if no `profiles` is given all
+ profile's properties are used.
+
+ :param profiles:
+ a single profile name or a list of names.
+ """
+ if not profiles:
+ profiles = self.profiles
+ elif isinstance(profiles, basestring):
+ profiles = (profiles, )
+ try:
+ for profile in sorted(profiles):
+ for name in sorted(self._profilesProperties[profile].keys()):
+ yield name
+ except KeyError, e:
+ raise NoSuchProfileException(e)
+
+ def validate(self, name, value):
+ """Check if `value` is valid for given property `name` using **any**
+ profile.
+
+ :param name:
+ a property name
+ :param value:
+ a CSS value (string)
+ :returns:
+ if the `value` is valid for the given property `name` in any
+ profile
+ """
+ for profile in self.profiles:
+ if name in self._profilesProperties[profile]:
+ try:
+ # custom validation errors are caught
+ r = bool(self._profilesProperties[profile][name](value))
+ except Exception, e:
+ # TODO: more specific exception?
+ # Validate should not be fatal though!
+ self._log.error(e, error=Exception)
+ r = False
+ if r:
+ return r
+ return False
+
+ def validateWithProfile(self, name, value, profiles=None):
+ """Check if `value` is valid for given property `name` returning
+ ``(valid, profile)``.
+
+ :param name:
+ a property name
+ :param value:
+ a CSS value (string)
+ :param profiles:
+ internal parameter used by Property.validate only
+ :returns:
+ ``valid, matching, profiles`` where ``valid`` is if the `value`
+ is valid for the given property `name` in any profile,
+ ``matching==True`` if it is valid in the given `profiles`
+ and ``profiles`` the profile names for which the value is valid
+ (or ``[]`` if not valid at all)
+
+ Example::
+
+ >>> cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2
+ >>> print cssutils.profile.validateWithProfile('color', 'rgba(1,1,1,1)')
+ (True, False, Profiles.CSS3_COLOR)
+ """
+ if name not in self.knownNames:
+ return False, False, []
+ else:
+ if not profiles:
+ profiles = self.defaultProfiles
+ elif isinstance(profiles, basestring):
+ profiles = (profiles, )
+ for profilename in reversed(profiles):
+ # check given profiles
+ if name in self._profilesProperties[profilename]:
+ validate = self._profilesProperties[profilename][name]
+ try:
+ if validate(value):
+ return True, True, [profilename]
+ except Exception, e:
+ self._log.error(e, error=Exception)
+
+ for profilename in (p for p in self._profileNames
+ if p not in profiles):
+ # check remaining profiles as well
+ if name in self._profilesProperties[profilename]:
+ validate = self._profilesProperties[profilename][name]
+ try:
+ if validate(value):
+ return True, False, [profilename]
+ except Exception, e:
+ self._log.error(e, error=Exception)
+
+ names = []
+ for profilename, properties in self._profilesProperties.items():
+ # return profile to which name belongs
+ if name in properties.keys():
+ names.append(profilename)
+ names.sort()
+ return False, False, names
+
+
+properties = {}
+macros = {}
+
+
+"""
+Define some regular expression fragments that will be used as
+macros within the CSS property value regular expressions.
+"""
+macros[Profiles.CSS_LEVEL_2] = {
+ 'background-color': r'{color}|transparent|inherit',
+ 'background-image': r'{uri}|none|inherit',
+ #'background-position': r'({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit',
+ 'background-position': r'({percentage}|{length}|left|center|right)(\s*({percentage}|{length}|top|center|bottom))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit',
+ 'background-repeat': r'repeat|repeat-x|repeat-y|no-repeat|inherit',
+ 'background-attachment': r'scroll|fixed|inherit',
+ 'shape': r'rect\(({w}({length}|auto}){w},){3}{w}({length}|auto){w}\)',
+ 'counter': r'counter\({w}{ident}{w}(?:,{w}{list-style-type}{w})?\)',
+ 'identifier': r'{ident}',
+ 'family-name': r'{string}|{ident}({w}{ident})*',
+ 'generic-family': r'serif|sans-serif|cursive|fantasy|monospace',
+ 'absolute-size': r'(x?x-)?(small|large)|medium',
+ 'relative-size': r'smaller|larger',
+
+ #[[ | ] [, | ]* ] | inherit
+ #'font-family': r'(({family-name}|{generic-family})({w},{w}({family-name}|{generic-family}))*)|inherit',
+ # EXTREMELY SLOW REGEX
+ #'font-family': r'({family-name}({w},{w}{family-name})*)|inherit',
+
+ 'font-size': r'{absolute-size}|{relative-size}|{positivelength}|{percentage}|inherit',
+ 'font-style': r'normal|italic|oblique|inherit',
+ 'font-variant': r'normal|small-caps|inherit',
+ 'font-weight': r'normal|bold|bolder|lighter|[1-9]00|inherit',
+ 'line-height': r'normal|{number}|{length}|{percentage}|inherit',
+ 'list-style-image': r'{uri}|none|inherit',
+ 'list-style-position': r'inside|outside|inherit',
+ 'list-style-type': r'disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-(latin|alpha)|upper-(latin|alpha)|armenian|georgian|none|inherit',
+ 'margin-width': r'{length}|{percentage}|auto',
+ 'padding-width': r'{length}|{percentage}',
+ 'specific-voice': r'{ident}',
+ 'generic-voice': r'male|female|child',
+ 'content': r'{string}|{uri}|{counter}|attr\({w}{ident}{w}\)|open-quote|close-quote|no-open-quote|no-close-quote',
+ 'background-attrs': r'{background-color}|{background-image}|{background-repeat}|{background-attachment}|{background-position}',
+ 'list-attrs': r'{list-style-type}|{list-style-position}|{list-style-image}',
+ 'font-attrs': r'{font-style}|{font-variant}|{font-weight}',
+ 'text-attrs': r'underline|overline|line-through|blink',
+ 'overflow': r'visible|hidden|scroll|auto|inherit',
+}
+
+"""
+Define the regular expressions for validation all CSS values
+"""
+properties[Profiles.CSS_LEVEL_2] = {
+ 'azimuth': r'{angle}|(behind\s+)?(left-side|far-left|left|center-left|center|center-right|right|far-right|right-side)(\s+behind)?|behind|leftwards|rightwards|inherit',
+ 'background-attachment': r'{background-attachment}',
+ 'background-color': r'{background-color}',
+ 'background-image': r'{background-image}',
+ 'background-position': r'{background-position}',
+ 'background-repeat': r'{background-repeat}',
+ # Each piece should only be allowed one time
+ 'background': r'{background-attrs}(\s+{background-attrs})*|inherit',
+ 'border-collapse': r'collapse|separate|inherit',
+ 'border-spacing': r'{length}(\s+{length})?|inherit',
+ 'bottom': r'{length}|{percentage}|auto|inherit',
+ 'caption-side': r'top|bottom|inherit',
+ 'clear': r'none|left|right|both|inherit',
+ 'clip': r'{shape}|auto|inherit',
+ 'color': r'{color}|inherit',
+ 'content': r'none|normal|{content}(\s+{content})*|inherit',
+ 'counter-increment': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit',
+ 'counter-reset': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit',
+ 'cue-after': r'{uri}|none|inherit',
+ 'cue-before': r'{uri}|none|inherit',
+ 'cue': r'({uri}|none|inherit){1,2}|inherit',
+ #'cursor': r'((({uri}{w},{w})*)?(auto|crosshair|default|pointer|move|(e|ne|nw|n|se|sw|s|w)-resize|text|wait|help|progress))|inherit',
+ 'direction': r'ltr|rtl|inherit',
+ 'display': r'inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit',
+ 'elevation': r'{angle}|below|level|above|higher|lower|inherit',
+ 'empty-cells': r'show|hide|inherit',
+ 'float': r'left|right|none|inherit',
+
+ # regex too slow:
+ # 'font-family': r'{font-family}',
+ 'font-family': _fontFamilyValidator,
+ '__FONT_FAMILY_SINGLE': r'{family-name}',
+
+ 'font-size': r'{font-size}',
+ 'font-style': r'{font-style}',
+ 'font-variant': r'{font-variant}',
+ 'font-weight': r'{font-weight}',
+
+ # regex too slow and wrong too:
+ # 'font': r'({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{font-family}|caption|icon|menu|message-box|small-caption|status-bar|inherit',
+ 'font': _fontValidator,
+ '__FONT_WITH_1_FAMILY': r'(({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{family-name})|caption|icon|menu|message-box|small-caption|status-bar|inherit',
+
+ 'height': r'{length}|{percentage}|auto|inherit',
+ 'left': r'{length}|{percentage}|auto|inherit',
+ 'letter-spacing': r'normal|{length}|inherit',
+ 'line-height': r'{line-height}',
+ 'list-style-image': r'{list-style-image}',
+ 'list-style-position': r'{list-style-position}',
+ 'list-style-type': r'{list-style-type}',
+ 'list-style': r'{list-attrs}(\s+{list-attrs})*|inherit',
+ 'margin-right': r'{margin-width}|inherit',
+ 'margin-left': r'{margin-width}|inherit',
+ 'margin-top': r'{margin-width}|inherit',
+ 'margin-bottom': r'{margin-width}|inherit',
+ 'margin': r'{margin-width}(\s+{margin-width}){0,3}|inherit',
+ 'max-height': r'{length}|{percentage}|none|inherit',
+ 'max-width': r'{length}|{percentage}|none|inherit',
+ 'min-height': r'{length}|{percentage}|none|inherit',
+ 'min-width': r'{length}|{percentage}|none|inherit',
+ 'orphans': r'{integer}|inherit',
+ 'overflow': r'{overflow}',
+ 'padding-top': r'{padding-width}|inherit',
+ 'padding-right': r'{padding-width}|inherit',
+ 'padding-bottom': r'{padding-width}|inherit',
+ 'padding-left': r'{padding-width}|inherit',
+ 'padding': r'{padding-width}(\s+{padding-width}){0,3}|inherit',
+ 'page-break-after': r'auto|always|avoid|left|right|inherit',
+ 'page-break-before': r'auto|always|avoid|left|right|inherit',
+ 'page-break-inside': r'avoid|auto|inherit',
+ 'pause-after': r'{time}|{percentage}|inherit',
+ 'pause-before': r'{time}|{percentage}|inherit',
+ 'pause': r'({time}|{percentage}){1,2}|inherit',
+ 'pitch-range': r'{number}|inherit',
+ 'pitch': r'{frequency}|x-low|low|medium|high|x-high|inherit',
+ 'play-during': r'{uri}(\s+(mix|repeat))*|auto|none|inherit',
+ 'position': r'static|relative|absolute|fixed|inherit',
+ 'quotes': r'({string}\s+{string})(\s+{string}\s+{string})*|none|inherit',
+ 'richness': r'{number}|inherit',
+ 'right': r'{length}|{percentage}|auto|inherit',
+ 'speak-header': r'once|always|inherit',
+ 'speak-numeral': r'digits|continuous|inherit',
+ 'speak-punctuation': r'code|none|inherit',
+ 'speak': r'normal|none|spell-out|inherit',
+ 'speech-rate': r'{number}|x-slow|slow|medium|fast|x-fast|faster|slower|inherit',
+ 'stress': r'{number}|inherit',
+ 'table-layout': r'auto|fixed|inherit',
+ 'text-align': r'left|right|center|justify|inherit',
+ 'text-decoration': r'none|{text-attrs}(\s+{text-attrs})*|inherit',
+ 'text-indent': r'{length}|{percentage}|inherit',
+ 'text-transform': r'capitalize|uppercase|lowercase|none|inherit',
+ 'top': r'{length}|{percentage}|auto|inherit',
+ 'unicode-bidi': r'normal|embed|bidi-override|inherit',
+ 'vertical-align': r'baseline|sub|super|top|text-top|middle|bottom|text-bottom|{percentage}|{length}|inherit',
+ 'visibility': r'visible|hidden|collapse|inherit',
+ 'voice-family': r'({specific-voice}|{generic-voice}{w},{w})*({specific-voice}|{generic-voice})|inherit',
+ 'volume': r'{number}|{percentage}|silent|x-soft|soft|medium|loud|x-loud|inherit',
+ 'white-space': r'normal|pre|nowrap|pre-wrap|pre-line|inherit',
+ 'widows': r'{integer}|inherit',
+ 'width': r'{length}|{percentage}|auto|inherit',
+ 'word-spacing': r'normal|{length}|inherit',
+ 'z-index': r'auto|{integer}|inherit',
+}
+
+
+macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = {
+ 'border-style': 'none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset',
+ 'border-width': '{length}|thin|medium|thick',
+ 'b1': r'{border-width}?({w}{border-style})?({w}{color})?',
+ 'b2': r'{border-width}?({w}{color})?({w}{border-style})?',
+ 'b3': r'{border-style}?({w}{border-width})?({w}{color})?',
+ 'b4': r'{border-style}?({w}{color})?({w}{border-width})?',
+ 'b5': r'{color}?({w}{border-style})?({w}{border-width})?',
+ 'b6': r'{color}?({w}{border-width})?({w}{border-style})?',
+ 'border-attrs': r'{b1}|{b2}|{b3}|{b4}|{b5}|{b6}',
+ 'border-radius-part': '({length}|{percentage})(\s+({length}|{percentage}))?'
+ }
+properties[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = {
+ 'border-color': r'({color}|transparent)(\s+({color}|transparent)){0,3}|inherit',
+ 'border-style': r'{border-style}(\s+{border-style}){0,3}|inherit',
+ 'border-top': r'{border-attrs}|inherit',
+ 'border-right': r'{border-attrs}|inherit',
+ 'border-bottom': r'{border-attrs}|inherit',
+ 'border-left': r'{border-attrs}|inherit',
+ 'border-top-color': r'{color}|transparent|inherit',
+ 'border-right-color': r'{color}|transparent|inherit',
+ 'border-bottom-color': r'{color}|transparent|inherit',
+ 'border-left-color': r'{color}|transparent|inherit',
+ 'border-top-style': r'{border-style}|inherit',
+ 'border-right-style': r'{border-style}|inherit',
+ 'border-bottom-style': r'{border-style}|inherit',
+ 'border-left-style': r'{border-style}|inherit',
+ 'border-top-width': r'{border-width}|inherit',
+ 'border-right-width': r'{border-width}|inherit',
+ 'border-bottom-width': r'{border-width}|inherit',
+ 'border-left-width': r'{border-width}|inherit',
+ 'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit',
+ 'border': r'{border-attrs}|inherit',
+ 'border-top-right-radius': '{border-radius-part}',
+ 'border-bottom-right-radius': '{border-radius-part}',
+ 'border-bottom-left-radius': '{border-radius-part}',
+ 'border-top-left-radius': '{border-radius-part}',
+ 'border-radius': '({length}{w}|{percentage}{w}){1,4}(/{w}({length}{w}|{percentage}{w}){1,4})?',
+ 'box-shadow': 'none|{shadow}({w},{w}{shadow})*',
+ }
+
+# CSS3 Basic User Interface Module
+macros[Profiles.CSS3_BASIC_USER_INTERFACE] = {
+ 'border-style': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-style'],
+ 'border-width': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-width'],
+ 'outline-1': r'{outline-color}(\s+{outline-style})?(\s+{outline-width})?',
+ 'outline-2': r'{outline-color}(\s+{outline-width})?(\s+{outline-style})?',
+ 'outline-3': r'{outline-style}(\s+{outline-color})?(\s+{outline-width})?',
+ 'outline-4': r'{outline-style}(\s+{outline-width})?(\s+{outline-color})?',
+ 'outline-5': r'{outline-width}(\s+{outline-color})?(\s+{outline-style})?',
+ 'outline-6': r'{outline-width}(\s+{outline-style})?(\s+{outline-color})?',
+ 'outline-color': r'{color}|invert|inherit',
+ 'outline-style': r'auto|{border-style}|inherit',
+ 'outline-width': r'{border-width}|inherit',
+ }
+properties[Profiles.CSS3_BASIC_USER_INTERFACE] = {
+ 'box-sizing': r'content-box|border-box',
+ 'cursor': r'((({uri}{w}({number}{w}{number}{w})?,{w})*)?(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|(e|n|ne|nw|s|se|sw|w|ew|ns|nesw|nwse|col|row)-resize|all-scroll))|inherit',
+ 'nav-index': r'auto|{number}|inherit',
+ 'outline-color': r'{outline-color}',
+ 'outline-style': r'{outline-style}',
+ 'outline-width': r'{outline-width}',
+ 'outline-offset': r'{length}|inherit',
+ #'outline': r'{outline-attrs}(\s+{outline-attrs})*|inherit',
+ 'outline': r'{outline-1}|{outline-2}|{outline-3}|{outline-4}|{outline-5}|{outline-6}|inherit',
+ 'resize': 'none|both|horizontal|vertical|inherit',
+ }
+
+# CSS Box Module Level 3
+macros[Profiles.CSS3_BOX] = {
+ 'overflow': macros[Profiles.CSS_LEVEL_2]['overflow']
+ }
+properties[Profiles.CSS3_BOX] = {
+ 'overflow': '{overflow}{w}{overflow}?|inherit',
+ 'overflow-x': '{overflow}|inherit',
+ 'overflow-y': '{overflow}|inherit'
+ }
+
+# CSS Color Module Level 3
+macros[Profiles.CSS3_COLOR] = {
+ # orange and transparent in CSS 2.1
+ 'namedcolor': r'(currentcolor|transparent|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)',
+ # orange?
+ 'rgbacolor': r'rgba\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\,{w}{num}{w}\)|rgba\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)',
+ 'hslcolor': r'hsl\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\)|hsla\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)',
+ 'x11color': r'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen',
+ 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)',
+ 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{rgbacolor}|{hslcolor}|{x11color}|inherit',
+ }
+properties[Profiles.CSS3_COLOR] = {
+ 'opacity': r'{num}|inherit',
+ }
+
+# CSS Fonts Module Level 3 http://www.w3.org/TR/css3-fonts/
+macros[Profiles.CSS3_FONTS] = {
+ #'family-name': r'{string}|{ident}',
+ 'family-name': r'{string}|{ident}({w}{ident})*',
+ 'font-face-name': 'local\({w}{family-name}{w}\)',
+ 'font-stretch-names': r'(ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)',
+ 'unicode-range': r'[uU]\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?'
+ }
+properties[Profiles.CSS3_FONTS] = {
+ 'font-size-adjust': r'{number}|none|inherit',
+ 'font-stretch': r'normal|wider|narrower|{font-stretch-names}|inherit'
+ }
+properties[Profiles.CSS3_FONT_FACE] = {
+ 'font-family': '{family-name}',
+ 'font-stretch': r'{font-stretch-names}',
+ 'font-style': r'normal|italic|oblique',
+ 'font-weight': r'normal|bold|[1-9]00',
+ 'src': r'({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name})({w},{w}({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name}))*',
+ 'unicode-range': '{unicode-range}({w},{w}{unicode-range})*'
+ }
+
+# CSS3 Paged Media
+macros[Profiles.CSS3_PAGED_MEDIA] = {
+ 'page-size': 'a5|a4|a3|b5|b4|letter|legal|ledger',
+ 'page-orientation': 'portrait|landscape',
+ 'page-1': '{page-size}(?:{w}{page-orientation})?',
+ 'page-2': '{page-orientation}(?:{w}{page-size})?',
+ 'page-size-orientation': '{page-1}|{page-2}',
+ 'pagebreak': 'auto|always|avoid|left|right'
+ }
+properties[Profiles.CSS3_PAGED_MEDIA] = {
+ 'fit': 'fill|hidden|meet|slice',
+ 'fit-position': r'auto|(({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?))',
+ 'image-orientation': 'auto|{angle}',
+ 'orphans': r'{integer}|inherit',
+ 'page': 'auto|{ident}',
+ 'page-break-before': '{pagebreak}|inherit',
+ 'page-break-after': '{pagebreak}|inherit',
+ 'page-break-inside': 'auto|avoid|inherit',
+ 'size': '({length}{w}){1,2}|auto|{page-size-orientation}',
+ 'widows': r'{integer}|inherit'
+ }
+
+macros[Profiles.CSS3_TEXT] = {
+ }
+properties[Profiles.CSS3_TEXT] = {
+ 'text-shadow': 'none|{shadow}({w},{w}{shadow})*',
+ }
diff --git a/libs/cssutils/sac.py b/libs/cssutils/sac.py
new file mode 100755
index 00000000..d46a0b70
--- /dev/null
+++ b/libs/cssutils/sac.py
@@ -0,0 +1,428 @@
+#!/usr/bin/env python
+"""A validating CSSParser"""
+__all__ = ['CSSParser']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: parse.py 1754 2009-05-30 14:50:13Z cthedot $'
+
+import helper
+import codecs
+import errorhandler
+import os
+import tokenize2
+import urllib
+import sys
+
+
+class ErrorHandler(object):
+ """Basic class for CSS error handlers.
+
+ This class class provides a default implementation ignoring warnings and
+ recoverable errors and throwing a SAXParseException for fatal errors.
+
+ If a CSS application needs to implement customized error handling, it must
+ extend this class and then register an instance with the CSS parser
+ using the parser's setErrorHandler method. The parser will then report all
+ errors and warnings through this interface.
+
+ The parser shall use this class instead of throwing an exception: it is
+ up to the application whether to throw an exception for different types of
+ errors and warnings. Note, however, that there is no requirement that the
+ parser continue to provide useful information after a call to fatalError
+ (in other words, a CSS driver class could catch an exception and report a
+ fatalError).
+ """
+ def __init__(self):
+ self._log = errorhandler.ErrorHandler()
+
+ def error(self, exception, token=None):
+ self._log.error(exception, token, neverraise=True)
+
+ def fatal(self, exception, token=None):
+ self._log.fatal(exception, token)
+
+ def warn(self, exception, token=None):
+ self._log.warn(exception, token, neverraise=True)
+
+
+class DocumentHandler(object):
+ """
+ void endFontFace()
+ Receive notification of the end of a font face statement.
+ void endMedia(SACMediaList media)
+ Receive notification of the end of a media statement.
+ void endPage(java.lang.String name, java.lang.String pseudo_page)
+ Receive notification of the end of a media statement.
+ void importStyle(java.lang.String uri, SACMediaList media, java.lang.String defaultNamespaceURI)
+ Receive notification of a import statement in the style sheet.
+ void startFontFace()
+ Receive notification of the beginning of a font face statement.
+ void startMedia(SACMediaList media)
+ Receive notification of the beginning of a media statement.
+ void startPage(java.lang.String name, java.lang.String pseudo_page)
+ Receive notification of the beginning of a page statement.
+ """
+ def __init__(self):
+ def log(msg):
+ sys.stderr.write('INFO\t%s\n' % msg)
+ self._log = log
+
+ def comment(self, text, line=None, col=None):
+ "Receive notification of a comment."
+ self._log("comment %r at [%s, %s]" % (text, line, col))
+
+ def startDocument(self, encoding):
+ "Receive notification of the beginning of a style sheet."
+ # source
+ self._log("startDocument encoding=%s" % encoding)
+
+ def endDocument(self, source=None, line=None, col=None):
+ "Receive notification of the end of a document."
+ self._log("endDocument EOF")
+
+ def importStyle(self, uri, media, name, line=None, col=None):
+ "Receive notification of a import statement in the style sheet."
+ # defaultNamespaceURI???
+ self._log("importStyle at [%s, %s]" % (line, col))
+
+ def namespaceDeclaration(self, prefix, uri, line=None, col=None):
+ "Receive notification of an unknown rule t-rule not supported by this parser."
+ # prefix might be None!
+ self._log("namespaceDeclaration at [%s, %s]" % (line, col))
+
+ def startSelector(self, selectors=None, line=None, col=None):
+ "Receive notification of the beginning of a rule statement."
+ # TODO selectorList!
+ self._log("startSelector at [%s, %s]" % (line, col))
+
+ def endSelector(self, selectors=None, line=None, col=None):
+ "Receive notification of the end of a rule statement."
+ self._log("endSelector at [%s, %s]" % (line, col))
+
+ def property(self, name, value='TODO', important=False, line=None, col=None):
+ "Receive notification of a declaration."
+ # TODO: value is LexicalValue?
+ self._log("property %r at [%s, %s]" % (name, line, col))
+
+ def ignorableAtRule(self, atRule, line=None, col=None):
+ "Receive notification of an unknown rule t-rule not supported by this parser."
+ self._log("ignorableAtRule %r at [%s, %s]" % (atRule, line, col))
+
+
+
+class EchoHandler(DocumentHandler):
+ "Echos all input to property `out`"
+ def __init__(self):
+ super(EchoHandler, self).__init__()
+ self._out = []
+
+ out = property(lambda self: u''.join(self._out))
+
+ def startDocument(self, encoding):
+ super(EchoHandler, self).startDocument(encoding)
+ if u'utf-8' != encoding:
+ self._out.append(u'@charset "%s";\n' % encoding)
+
+# def comment(self, text, line=None, col=None):
+# self._out.append(u'/*%s*/' % text)
+
+ def importStyle(self, uri, media, name, line=None, col=None):
+ "Receive notification of a import statement in the style sheet."
+ # defaultNamespaceURI???
+ super(EchoHandler, self).importStyle(uri, media, name, line, col)
+ self._out.append(u'@import %s%s%s;\n' % (helper.string(uri),
+ u'%s ' % media if media else u'',
+ u'%s ' % name if name else u'')
+ )
+
+
+ def namespaceDeclaration(self, prefix, uri, line=None, col=None):
+ super(EchoHandler, self).namespaceDeclaration(prefix, uri, line, col)
+ self._out.append(u'@namespace %s%s;\n' % (u'%s ' % prefix if prefix else u'',
+ helper.string(uri)))
+
+ def startSelector(self, selectors=None, line=None, col=None):
+ super(EchoHandler, self).startSelector(selectors, line, col)
+ if selectors:
+ self._out.append(u', '.join(selectors))
+ self._out.append(u' {\n')
+
+ def endSelector(self, selectors=None, line=None, col=None):
+ self._out.append(u' }')
+
+ def property(self, name, value, important=False, line=None, col=None):
+ super(EchoHandler, self).property(name, value, line, col)
+ self._out.append(u' %s: %s%s;\n' % (name, value,
+ u' !important' if important else u''))
+
+
+class Parser(object):
+ """
+ java.lang.String getParserVersion()
+ Returns a string about which CSS language is supported by this parser.
+ boolean parsePriority(InputSource source)
+ Parse a CSS priority value (e.g.
+ LexicalUnit parsePropertyValue(InputSource source)
+ Parse a CSS property value.
+ void parseRule(InputSource source)
+ Parse a CSS rule.
+ SelectorList parseSelectors(InputSource source)
+ Parse a comma separated list of selectors.
+ void parseStyleDeclaration(InputSource source)
+ Parse a CSS style declaration (without '{' and '}').
+ void parseStyleSheet(InputSource source)
+ Parse a CSS document.
+ void parseStyleSheet(java.lang.String uri)
+ Parse a CSS document from a URI.
+ void setConditionFactory(ConditionFactory conditionFactory)
+
+ void setDocumentHandler(DocumentHandler handler)
+ Allow an application to register a document event handler.
+ void setErrorHandler(ErrorHandler handler)
+ Allow an application to register an error event handler.
+ void setLocale(java.util.Locale locale)
+ Allow an application to request a locale for errors and warnings.
+ void setSelectorFactory(SelectorFactory selectorFactory)
+ """
+ def __init__(self, documentHandler=None, errorHandler=None):
+ self._tokenizer = tokenize2.Tokenizer()
+ if documentHandler:
+ self.setDocumentHandler(documentHandler)
+ else:
+ self.setDocumentHandler(DocumentHandler())
+
+ if errorHandler:
+ self.setErrorHandler(errorHandler)
+ else:
+ self.setErrorHandler(ErrorHandler())
+
+ def parseString(self, cssText, encoding=None):
+ if isinstance(cssText, str):
+ cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0]
+
+ tokens = self._tokenizer.tokenize(cssText, fullsheet=True)
+
+ def COMMENT(val, line, col):
+ self._handler.comment(val[2:-2], line, col)
+
+ def EOF(val, line, col):
+ self._handler.endDocument(val, line, col)
+
+ def simple(t):
+ map = {'COMMENT': COMMENT,
+ 'S': lambda val, line, col: None,
+ 'EOF': EOF}
+ type_, val, line, col = t
+ if type_ in map:
+ map[type_](val, line, col)
+ return True
+ else:
+ return False
+
+ # START PARSING
+ t = tokens.next()
+ type_, val, line, col = t
+
+ encoding = 'utf-8'
+ if 'CHARSET_SYM' == type_:
+ # @charset "encoding";
+ # S
+ encodingtoken = tokens.next()
+ semicolontoken = tokens.next()
+ if 'STRING' == type_:
+ encoding = helper.stringvalue(val)
+ # ;
+ if 'STRING' == encodingtoken[0] and semicolontoken:
+ encoding = helper.stringvalue(encodingtoken[1])
+ else:
+ self._errorHandler.fatal(u'Invalid @charset')
+
+ t = tokens.next()
+ type_, val, line, col = t
+
+ self._handler.startDocument(encoding)
+
+ while True:
+ start = (line, col)
+ try:
+ if simple(t):
+ pass
+
+ elif 'ATKEYWORD' == type_ or type_ in ('PAGE_SYM', 'MEDIA_SYM', 'FONT_FACE_SYM'):
+ atRule = [val]
+ braces = 0
+ while True:
+ # read till end ;
+ # TODO: or {}
+ t = tokens.next()
+ type_, val, line, col = t
+ atRule.append(val)
+ if u';' == val and not braces:
+ break
+ elif u'{' == val:
+ braces += 1
+ elif u'}' == val:
+ braces -= 1
+ if braces == 0:
+ break
+
+ self._handler.ignorableAtRule(u''.join(atRule), *start)
+
+ elif 'IMPORT_SYM' == type_:
+ # import URI or STRING media? name?
+ uri, media, name = None, None, None
+ while True:
+ t = tokens.next()
+ type_, val, line, col = t
+ if 'STRING' == type_:
+ uri = helper.stringvalue(val)
+ elif 'URI' == type_:
+ uri = helper.urivalue(val)
+ elif u';' == val:
+ break
+
+ if uri:
+ self._handler.importStyle(uri, media, name)
+ else:
+ self._errorHandler.error(u'Invalid @import'
+ u' declaration at %r'
+ % (start,))
+
+ elif 'NAMESPACE_SYM' == type_:
+ prefix, uri = None, None
+ while True:
+ t = tokens.next()
+ type_, val, line, col = t
+ if 'IDENT' == type_:
+ prefix = val
+ elif 'STRING' == type_:
+ uri = helper.stringvalue(val)
+ elif 'URI' == type_:
+ uri = helper.urivalue(val)
+ elif u';' == val:
+ break
+ if uri:
+ self._handler.namespaceDeclaration(prefix, uri, *start)
+ else:
+ self._errorHandler.error(u'Invalid @namespace'
+ u' declaration at %r'
+ % (start,))
+
+ else:
+ # CSSSTYLERULE
+ selector = []
+ selectors = []
+ while True:
+ # selectors[, selector]* {
+ if 'S' == type_:
+ selector.append(u' ')
+ elif simple(t):
+ pass
+ elif u',' == val:
+ selectors.append(u''.join(selector).strip())
+ selector = []
+ elif u'{' == val:
+ selectors.append(u''.join(selector).strip())
+ self._handler.startSelector(selectors, *start)
+ break
+ else:
+ selector.append(val)
+
+ t = tokens.next()
+ type_, val, line, col = t
+
+ end = None
+ while True:
+ # name: value [!important][;name: value [!important]]*;?
+ name, value, important = None, [], False
+
+ while True:
+ # name:
+ t = tokens.next()
+ type_, val, line, col = t
+ if 'S' == type_:
+ pass
+ elif simple(t):
+ pass
+ elif 'IDENT' == type_:
+ if name:
+ self._errorHandler.error('more than one property name', t)
+ else:
+ name = val
+ elif u':' == val:
+ if not name:
+ self._errorHandler.error('no property name', t)
+ break
+ elif u';' == val:
+ self._errorHandler.error('premature end of property', t)
+ end = val
+ break
+ elif u'}' == val:
+ if name:
+ self._errorHandler.error('premature end of property', t)
+ end = val
+ break
+ else:
+ self._errorHandler.error('unexpected property name token %r' % val, t)
+
+ while not u';' == end and not u'}' == end:
+ # value !;}
+ t = tokens.next()
+ type_, val, line, col = t
+
+ if 'S' == type_:
+ value.append(u' ')
+ elif simple(t):
+ pass
+ elif u'!' == val or u';' == val or u'}' == val:
+ value = ''.join(value).strip()
+ if not value:
+ self._errorHandler.error('premature end of property (no value)', t)
+ end = val
+ break
+ else:
+ value.append(val)
+
+ while u'!' == end:
+ # !important
+ t = tokens.next()
+ type_, val, line, col = t
+
+ if simple(t):
+ pass
+ elif u'IDENT' == type_ and not important:
+ important = True
+ elif u';' == val or u'}' == val:
+ end = val
+ break
+ else:
+ self._errorHandler.error('unexpected priority token %r' % val)
+
+ if name and value:
+ self._handler.property(name, value, important)
+
+ if u'}' == end:
+ self._handler.endSelector(selectors, line=line, col=col)
+ break
+ else:
+ # reset
+ end = None
+
+ else:
+ self._handler.endSelector(selectors, line=line, col=col)
+
+ t = tokens.next()
+ type_, val, line, col = t
+
+ except StopIteration:
+ break
+
+
+
+ def setDocumentHandler(self, handler):
+ "Allow an application to register a document event `handler`."
+ self._handler = handler
+
+ def setErrorHandler(self, handler):
+ "TODO"
+ self._errorHandler = handler
+
\ No newline at end of file
diff --git a/libs/cssutils/script.py b/libs/cssutils/script.py
new file mode 100755
index 00000000..56c1c6e2
--- /dev/null
+++ b/libs/cssutils/script.py
@@ -0,0 +1,362 @@
+"""classes and functions used by cssutils scripts
+"""
+__all__ = ['CSSCapture', 'csscombine']
+__docformat__ = 'restructuredtext'
+__version__ = '$Id: parse.py 1323 2008-07-06 18:13:57Z cthedot $'
+
+import HTMLParser
+import codecs
+import cssutils
+import errno
+import logging
+import os
+import sys
+import urllib2
+import urlparse
+
+try:
+ import cssutils.encutils as encutils
+except ImportError:
+ try:
+ import encutils
+ except ImportError:
+ sys.exit("You need encutils from http://cthedot.de/encutils/")
+
+# types of sheets in HTML
+LINK = 0 #
+STYLE = 1 #
+
+class CSSCaptureHTMLParser(HTMLParser.HTMLParser):
+ """CSSCapture helper: Parse given data for link and style elements"""
+ curtag = u''
+ sheets = [] # (type, [atts, cssText])
+
+ def _loweratts(self, atts):
+ return dict([(a.lower(), v.lower()) for a, v in atts])
+
+ def handle_starttag(self, tag, atts):
+ if tag == u'link':
+ atts = self._loweratts(atts)
+ if u'text/css' == atts.get(u'type', u''):
+ self.sheets.append((LINK, atts))
+ elif tag == u'style':
+ # also get content of style
+ atts = self._loweratts(atts)
+ if u'text/css' == atts.get(u'type', u''):
+ self.sheets.append((STYLE, [atts, u'']))
+ self.curtag = tag
+ else:
+ # close as only intersting