sud0nick: Resolve merge conflicts for PR #50.

This commit is contained in:
Foxtrot
2019-06-19 18:09:12 +01:00
23 changed files with 10355 additions and 1674 deletions

View File

@@ -153,23 +153,26 @@ class PortalCloner:
# Search for CSS files linked with the @import statement and remove
for style in self.soup.find_all("style"):
parser = tinycss.make_parser('page3')
stylesheet = parser.parse_stylesheet(style.string)
for rule in stylesheet.rules:
if rule.at_keyword == "@import":
# Get the name of the resource
fname = str(rule.uri).split("/")[-1]
try:
stylesheet = parser.parse_stylesheet(style.string)
for rule in stylesheet.rules:
if rule.at_keyword == "@import":
# Download the resource
resourceURLs.append([rul.uri, fname])
# Parse the CSS to get image links
_key = "resources/" + fname
self.css_urls[_key] = self.parseCSS(urlparse.urljoin(self.url, rule.uri))
# Replace the old link of the CSS with the new one
modStyle = style.string
style.string.replace_with(modStyle.replace(rule.uri, "resources/" + fname))
# Get the name of the resource
fname = str(rule.uri).split("/")[-1]
# Download the resource
resourceURLs.append([rule.uri, fname])
# Parse the CSS to get image links
_key = "resources/" + fname
self.css_urls[_key] = self.parseCSS(urlparse.urljoin(self.url, rule.uri))
# Replace the old link of the CSS with the new one
modStyle = style.string
style.string.replace_with(modStyle.replace(rule.uri, "resources/" + fname))
except:
pass
# Find and download all images and CSS files linked with <link>

File diff suppressed because it is too large Load Diff

View File

@@ -1,310 +1,310 @@
# Copyright 2001-2005 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Vinay Sajip
# not be used in advertising or publicity pertaining to distribution
# of the software without specific, written prior permission.
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Configuration functions for the logging package for Python. The core package
is based on PEP 282 and comments thereto in comp.lang.python, and influenced
by Apache's log4j system.
Should work under Python versions >= 1.5.2, except that source line
information is not available unless 'sys._getframe()' is.
Copyright (C) 2001-2004 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging' and log away!
"""
import sys, logging, logging.handlers, string, socket, struct, os
try:
import thread
import threading
except ImportError:
thread = None
from SocketServer import ThreadingTCPServer, StreamRequestHandler
DEFAULT_LOGGING_CONFIG_PORT = 9030
if sys.platform == "win32":
RESET_ERROR = 10054 #WSAECONNRESET
else:
RESET_ERROR = 104 #ECONNRESET
#
# The following code implements a socket listener for on-the-fly
# reconfiguration of logging.
#
# _listener holds the server object doing the listening
_listener = None
def fileConfig(fname, defaults=None):
"""
Read the logging configuration from a ConfigParser-format file.
This can be called several times from an application, allowing an end user
the ability to select from various pre-canned configurations (if the
developer provides a mechanism to present the choices and load the chosen
configuration).
In versions of ConfigParser which have the readfp method [typically
shipped in 2.x versions of Python], you can pass in a file-like object
rather than a filename, in which case the file-like object will be read
using readfp.
"""
import ConfigParser
cp = ConfigParser.ConfigParser(defaults)
if hasattr(cp, 'readfp') and hasattr(fname, 'readline'):
cp.readfp(fname)
else:
cp.read(fname)
#first, do the formatters...
flist = cp.get("formatters", "keys")
if len(flist):
flist = string.split(flist, ",")
formatters = {}
for form in flist:
sectname = "formatter_%s" % form
opts = cp.options(sectname)
if "format" in opts:
fs = cp.get(sectname, "format", 1)
else:
fs = None
if "datefmt" in opts:
dfs = cp.get(sectname, "datefmt", 1)
else:
dfs = None
f = logging.Formatter(fs, dfs)
formatters[form] = f
#next, do the handlers...
#critical section...
logging._acquireLock()
try:
try:
#first, lose the existing handlers...
logging._handlers.clear()
#now set up the new ones...
hlist = cp.get("handlers", "keys")
if len(hlist):
hlist = string.split(hlist, ",")
handlers = {}
fixups = [] #for inter-handler references
for hand in hlist:
try:
sectname = "handler_%s" % hand
klass = cp.get(sectname, "class")
opts = cp.options(sectname)
if "formatter" in opts:
fmt = cp.get(sectname, "formatter")
else:
fmt = ""
klass = eval(klass, vars(logging))
args = cp.get(sectname, "args")
args = eval(args, vars(logging))
h = apply(klass, args)
if "level" in opts:
level = cp.get(sectname, "level")
h.setLevel(logging._levelNames[level])
if len(fmt):
h.setFormatter(formatters[fmt])
#temporary hack for FileHandler and MemoryHandler.
if klass == logging.handlers.MemoryHandler:
if "target" in opts:
target = cp.get(sectname,"target")
else:
target = ""
if len(target): #the target handler may not be loaded yet, so keep for later...
fixups.append((h, target))
handlers[hand] = h
except: #if an error occurs when instantiating a handler, too bad
pass #this could happen e.g. because of lack of privileges
#now all handlers are loaded, fixup inter-handler references...
for fixup in fixups:
h = fixup[0]
t = fixup[1]
h.setTarget(handlers[t])
#at last, the loggers...first the root...
llist = cp.get("loggers", "keys")
llist = string.split(llist, ",")
llist.remove("root")
sectname = "logger_root"
root = logging.root
log = root
opts = cp.options(sectname)
if "level" in opts:
level = cp.get(sectname, "level")
log.setLevel(logging._levelNames[level])
for h in root.handlers[:]:
root.removeHandler(h)
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
log.addHandler(handlers[hand])
#and now the others...
#we don't want to lose the existing loggers,
#since other threads may have pointers to them.
#existing is set to contain all existing loggers,
#and as we go through the new configuration we
#remove any which are configured. At the end,
#what's left in existing is the set of loggers
#which were in the previous configuration but
#which are not in the new configuration.
existing = root.manager.loggerDict.keys()
#now set up the new ones...
for log in llist:
sectname = "logger_%s" % log
qn = cp.get(sectname, "qualname")
opts = cp.options(sectname)
if "propagate" in opts:
propagate = cp.getint(sectname, "propagate")
else:
propagate = 1
logger = logging.getLogger(qn)
if qn in existing:
existing.remove(qn)
if "level" in opts:
level = cp.get(sectname, "level")
logger.setLevel(logging._levelNames[level])
for h in logger.handlers[:]:
logger.removeHandler(h)
logger.propagate = propagate
logger.disabled = 0
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
logger.addHandler(handlers[hand])
#Disable any old loggers. There's no point deleting
#them as other threads may continue to hold references
#and by disabling them, you stop them doing any logging.
for log in existing:
root.manager.loggerDict[log].disabled = 1
except:
import traceback
ei = sys.exc_info()
traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
del ei
finally:
logging._releaseLock()
def listen(port=DEFAULT_LOGGING_CONFIG_PORT):
"""
Start up a socket server on the specified port, and listen for new
configurations.
These will be sent as a file suitable for processing by fileConfig().
Returns a Thread object on which you can call start() to start the server,
and which you can join() when appropriate. To stop the server, call
stopListening().
"""
if not thread:
raise NotImplementedError, "listen() needs threading to work"
class ConfigStreamHandler(StreamRequestHandler):
"""
Handler for a logging configuration request.
It expects a completely new logging configuration and uses fileConfig
to install it.
"""
def handle(self):
"""
Handle a request.
Each request is expected to be a 4-byte length,
followed by the config file. Uses fileConfig() to do the
grunt work.
"""
import tempfile
try:
conn = self.connection
chunk = conn.recv(4)
if len(chunk) == 4:
slen = struct.unpack(">L", chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + conn.recv(slen - len(chunk))
#Apply new configuration. We'd like to be able to
#create a StringIO and pass that in, but unfortunately
#1.5.2 ConfigParser does not support reading file
#objects, only actual files. So we create a temporary
#file and remove it later.
file = tempfile.mktemp(".ini")
f = open(file, "w")
f.write(chunk)
f.close()
fileConfig(file)
os.remove(file)
except socket.error, e:
if type(e.args) != types.TupleType:
raise
else:
errcode = e.args[0]
if errcode != RESET_ERROR:
raise
class ConfigSocketReceiver(ThreadingTCPServer):
"""
A simple TCP socket-based logging config receiver.
"""
allow_reuse_address = 1
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
handler=None):
ThreadingTCPServer.__init__(self, (host, port), handler)
logging._acquireLock()
self.abort = 0
logging._releaseLock()
self.timeout = 1
def serve_until_stopped(self):
import select
abort = 0
while not abort:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
self.timeout)
if rd:
self.handle_request()
logging._acquireLock()
abort = self.abort
logging._releaseLock()
def serve(rcvr, hdlr, port):
server = rcvr(port=port, handler=hdlr)
global _listener
logging._acquireLock()
_listener = server
logging._releaseLock()
server.serve_until_stopped()
return threading.Thread(target=serve,
args=(ConfigSocketReceiver,
ConfigStreamHandler, port))
def stopListening():
"""
Stop the listening server which was created with a call to listen().
"""
global _listener
if _listener:
logging._acquireLock()
_listener.abort = 1
_listener = None
logging._releaseLock()
# Copyright 2001-2005 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Vinay Sajip
# not be used in advertising or publicity pertaining to distribution
# of the software without specific, written prior permission.
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Configuration functions for the logging package for Python. The core package
is based on PEP 282 and comments thereto in comp.lang.python, and influenced
by Apache's log4j system.
Should work under Python versions >= 1.5.2, except that source line
information is not available unless 'sys._getframe()' is.
Copyright (C) 2001-2004 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging' and log away!
"""
import sys, logging, logging.handlers, string, socket, struct, os
try:
import thread
import threading
except ImportError:
thread = None
from SocketServer import ThreadingTCPServer, StreamRequestHandler
DEFAULT_LOGGING_CONFIG_PORT = 9030
if sys.platform == "win32":
RESET_ERROR = 10054 #WSAECONNRESET
else:
RESET_ERROR = 104 #ECONNRESET
#
# The following code implements a socket listener for on-the-fly
# reconfiguration of logging.
#
# _listener holds the server object doing the listening
_listener = None
def fileConfig(fname, defaults=None):
"""
Read the logging configuration from a ConfigParser-format file.
This can be called several times from an application, allowing an end user
the ability to select from various pre-canned configurations (if the
developer provides a mechanism to present the choices and load the chosen
configuration).
In versions of ConfigParser which have the readfp method [typically
shipped in 2.x versions of Python], you can pass in a file-like object
rather than a filename, in which case the file-like object will be read
using readfp.
"""
import ConfigParser
cp = ConfigParser.ConfigParser(defaults)
if hasattr(cp, 'readfp') and hasattr(fname, 'readline'):
cp.readfp(fname)
else:
cp.read(fname)
#first, do the formatters...
flist = cp.get("formatters", "keys")
if len(flist):
flist = string.split(flist, ",")
formatters = {}
for form in flist:
sectname = "formatter_%s" % form
opts = cp.options(sectname)
if "format" in opts:
fs = cp.get(sectname, "format", 1)
else:
fs = None
if "datefmt" in opts:
dfs = cp.get(sectname, "datefmt", 1)
else:
dfs = None
f = logging.Formatter(fs, dfs)
formatters[form] = f
#next, do the handlers...
#critical section...
logging._acquireLock()
try:
try:
#first, lose the existing handlers...
logging._handlers.clear()
#now set up the new ones...
hlist = cp.get("handlers", "keys")
if len(hlist):
hlist = string.split(hlist, ",")
handlers = {}
fixups = [] #for inter-handler references
for hand in hlist:
try:
sectname = "handler_%s" % hand
klass = cp.get(sectname, "class")
opts = cp.options(sectname)
if "formatter" in opts:
fmt = cp.get(sectname, "formatter")
else:
fmt = ""
klass = eval(klass, vars(logging))
args = cp.get(sectname, "args")
args = eval(args, vars(logging))
h = apply(klass, args)
if "level" in opts:
level = cp.get(sectname, "level")
h.setLevel(logging._levelNames[level])
if len(fmt):
h.setFormatter(formatters[fmt])
#temporary hack for FileHandler and MemoryHandler.
if klass == logging.handlers.MemoryHandler:
if "target" in opts:
target = cp.get(sectname,"target")
else:
target = ""
if len(target): #the target handler may not be loaded yet, so keep for later...
fixups.append((h, target))
handlers[hand] = h
except: #if an error occurs when instantiating a handler, too bad
pass #this could happen e.g. because of lack of privileges
#now all handlers are loaded, fixup inter-handler references...
for fixup in fixups:
h = fixup[0]
t = fixup[1]
h.setTarget(handlers[t])
#at last, the loggers...first the root...
llist = cp.get("loggers", "keys")
llist = string.split(llist, ",")
llist.remove("root")
sectname = "logger_root"
root = logging.root
log = root
opts = cp.options(sectname)
if "level" in opts:
level = cp.get(sectname, "level")
log.setLevel(logging._levelNames[level])
for h in root.handlers[:]:
root.removeHandler(h)
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
log.addHandler(handlers[hand])
#and now the others...
#we don't want to lose the existing loggers,
#since other threads may have pointers to them.
#existing is set to contain all existing loggers,
#and as we go through the new configuration we
#remove any which are configured. At the end,
#what's left in existing is the set of loggers
#which were in the previous configuration but
#which are not in the new configuration.
existing = root.manager.loggerDict.keys()
#now set up the new ones...
for log in llist:
sectname = "logger_%s" % log
qn = cp.get(sectname, "qualname")
opts = cp.options(sectname)
if "propagate" in opts:
propagate = cp.getint(sectname, "propagate")
else:
propagate = 1
logger = logging.getLogger(qn)
if qn in existing:
existing.remove(qn)
if "level" in opts:
level = cp.get(sectname, "level")
logger.setLevel(logging._levelNames[level])
for h in logger.handlers[:]:
logger.removeHandler(h)
logger.propagate = propagate
logger.disabled = 0
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
logger.addHandler(handlers[hand])
#Disable any old loggers. There's no point deleting
#them as other threads may continue to hold references
#and by disabling them, you stop them doing any logging.
for log in existing:
root.manager.loggerDict[log].disabled = 1
except:
import traceback
ei = sys.exc_info()
traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
del ei
finally:
logging._releaseLock()
def listen(port=DEFAULT_LOGGING_CONFIG_PORT):
"""
Start up a socket server on the specified port, and listen for new
configurations.
These will be sent as a file suitable for processing by fileConfig().
Returns a Thread object on which you can call start() to start the server,
and which you can join() when appropriate. To stop the server, call
stopListening().
"""
if not thread:
raise NotImplementedError, "listen() needs threading to work"
class ConfigStreamHandler(StreamRequestHandler):
"""
Handler for a logging configuration request.
It expects a completely new logging configuration and uses fileConfig
to install it.
"""
def handle(self):
"""
Handle a request.
Each request is expected to be a 4-byte length,
followed by the config file. Uses fileConfig() to do the
grunt work.
"""
import tempfile
try:
conn = self.connection
chunk = conn.recv(4)
if len(chunk) == 4:
slen = struct.unpack(">L", chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + conn.recv(slen - len(chunk))
#Apply new configuration. We'd like to be able to
#create a StringIO and pass that in, but unfortunately
#1.5.2 ConfigParser does not support reading file
#objects, only actual files. So we create a temporary
#file and remove it later.
file = tempfile.mktemp(".ini")
f = open(file, "w")
f.write(chunk)
f.close()
fileConfig(file)
os.remove(file)
except socket.error, e:
if type(e.args) != types.TupleType:
raise
else:
errcode = e.args[0]
if errcode != RESET_ERROR:
raise
class ConfigSocketReceiver(ThreadingTCPServer):
"""
A simple TCP socket-based logging config receiver.
"""
allow_reuse_address = 1
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
handler=None):
ThreadingTCPServer.__init__(self, (host, port), handler)
logging._acquireLock()
self.abort = 0
logging._releaseLock()
self.timeout = 1
def serve_until_stopped(self):
import select
abort = 0
while not abort:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
self.timeout)
if rd:
self.handle_request()
logging._acquireLock()
abort = self.abort
logging._releaseLock()
def serve(rcvr, hdlr, port):
server = rcvr(port=port, handler=hdlr)
global _listener
logging._acquireLock()
_listener = server
logging._releaseLock()
server.serve_until_stopped()
return threading.Thread(target=serve,
args=(ConfigSocketReceiver,
ConfigStreamHandler, port))
def stopListening():
"""
Stop the listening server which was created with a call to listen().
"""
global _listener
if _listener:
logging._acquireLock()
_listener.abort = 1
_listener = None
logging._releaseLock()

View File

@@ -0,0 +1,200 @@
# coding: utf-8
"""
tinycss.colors3
---------------
Parser for CSS 3 Fonts syntax:
https://www.w3.org/TR/css-fonts-3/
Adds support for font-face and font-feature-values rules.
:copyright: (c) 2016 by Kozea.
:license: BSD, see LICENSE for more details.
"""
from __future__ import division, unicode_literals
from .css21 import CSS21Parser, ParseError
class FontFaceRule(object):
"""A parsed at-rule for font faces.
.. attribute:: at_keyword
Always ``'@font-face'``.
.. attribute:: declarations
A list of :class:`~.css21.Declaration` objects.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
"""
def __init__(self, at_keyword, declarations, line, column):
assert at_keyword == '@font-face'
self.at_keyword = at_keyword
self.declarations = declarations
self.line = line
self.column = column
class FontFeatureValuesRule(object):
"""A parsed at-rule for font feature values.
.. attribute:: at_keyword
Always ``'@font-feature-values'``.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
.. attribute:: at_rules
The list of parsed at-rules inside the @font-feature-values block, in
source order.
.. attribute:: family_names
A list of strings representing font families.
"""
def __init__(self, at_keyword, at_rules, family_names, line, column):
assert at_keyword == '@font-feature-values'
self.at_keyword = at_keyword
self.family_names = family_names
self.at_rules = at_rules
self.line = line
self.column = column
class FontFeatureRule(object):
"""A parsed at-rule for font features.
.. attribute:: at_keyword
One of the 16 following strings:
* ``@stylistic``
* ``@styleset``
* ``@character-variant``
* ``@swash``
* ``@ornaments``
* ``@annotation``
.. attribute:: declarations
A list of :class:`~.css21.Declaration` objects.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
"""
def __init__(self, at_keyword, declarations, line, column):
self.at_keyword = at_keyword
self.declarations = declarations
self.line = line
self.column = column
class CSSFonts3Parser(CSS21Parser):
"""Extend :class:`~.css21.CSS21Parser` for `CSS 3 Fonts`_ syntax.
.. _CSS 3 Fonts: https://www.w3.org/TR/css-fonts-3/
"""
FONT_FEATURE_VALUES_AT_KEYWORDS = [
'@stylistic',
'@styleset',
'@character-variant',
'@swash',
'@ornaments',
'@annotation',
]
def parse_at_rule(self, rule, previous_rules, errors, context):
if rule.at_keyword == '@font-face':
if rule.head:
raise ParseError(
rule.head[0],
'unexpected {0} token in {1} rule header'.format(
rule.head[0].type, rule.at_keyword))
declarations, body_errors = self.parse_declaration_list(rule.body)
errors.extend(body_errors)
return FontFaceRule(
rule.at_keyword, declarations, rule.line, rule.column)
elif rule.at_keyword == '@font-feature-values':
family_names = tuple(
self.parse_font_feature_values_family_names(rule.head))
at_rules, body_errors = (
self.parse_rules(rule.body or [], '@font-feature-values'))
errors.extend(body_errors)
return FontFeatureValuesRule(
rule.at_keyword, at_rules, family_names,
rule.line, rule.column)
elif rule.at_keyword in self.FONT_FEATURE_VALUES_AT_KEYWORDS:
if context != '@font-feature-values':
raise ParseError(
rule, '{0} rule not allowed in {1}'.format(
rule.at_keyword, context))
declarations, body_errors = self.parse_declaration_list(rule.body)
errors.extend(body_errors)
return FontFeatureRule(
rule.at_keyword, declarations, rule.line, rule.column)
return super(CSSFonts3Parser, self).parse_at_rule(
rule, previous_rules, errors, context)
def parse_font_feature_values_family_names(self, tokens):
"""Parse an @font-feature-values selector.
:param tokens:
An iterable of token, typically from the ``head`` attribute of
an unparsed :class:`AtRule`.
:returns:
A generator of strings representing font families.
:raises:
:class:`~.parsing.ParseError` on invalid selectors
"""
family = ''
current_string = False
for token in tokens:
if token.type == 'DELIM' and token.value == ',' and family:
yield family
family = ''
current_string = False
elif token.type == 'STRING' and not family and (
current_string is False):
family = token.value
current_string = True
elif token.type == 'IDENT' and not current_string:
if family:
family += ' '
family += token.value
elif token.type != 'S':
family = ''
break
if family:
yield family
else:
raise ParseError(token, 'invalid @font-feature-values selector')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
# coding: utf-8
"""
Tests for the Fonts 3 parser
----------------------------
:copyright: (c) 2016 by Kozea.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import pytest
from tinycss.fonts3 import CSSFonts3Parser
from . import assert_errors
from .test_tokenizer import jsonify
@pytest.mark.parametrize(('css', 'expected_family_names', 'expected_errors'), [
('@font-feature-values foo {}', ('foo',), []),
('@font-feature-values Foo Test {}', ('Foo Test',), []),
('@font-feature-values \'Foo Test\' {}', ('Foo Test',), []),
('@font-feature-values Foo Test, Foo Lol, "Foo tooo"', (
'Foo Test', 'Foo Lol', 'Foo tooo'), []),
('@font-feature-values Foo , Foo lol {}', ('Foo', 'Foo lol'), []),
('@font-feature-values Foo , "Foobar" , Lol {}', (
'Foo', 'Foobar', 'Lol'), []),
('@font-feature-values Foo, {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values ,Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test,"Foo", {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test "Foo" {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test Foo, Test "bar", "foo" {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test/Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values /Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values #Foo {}', None, [
'invalid @font-feature-values selector']),
# TODO: this currently works but should not work
# ('@font-feature-values test@foo {}', None, [
# 'invalid @font-feature-values selector']),
('@font-feature-values Hawaii 5-0 {}', None, [
'invalid @font-feature-values selector']),
])
def test_font_feature_values_selectors(css, expected_family_names,
expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
if stylesheet.rules:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-feature-values'
assert rule.family_names == expected_family_names
@pytest.mark.parametrize(('css', 'expected_declarations', 'expected_errors'), [
('@font-face {}', [], []),
('@font-face test { src: "lol"; font-family: "bar" }', None, [
'unexpected IDENT token in @font-face rule header']),
('@font-face { src: "lol"; font-family: "bar" }', [
('src', [('STRING', 'lol')]),
('font-family', [('STRING', 'bar')])], []),
('@font-face { src: "lol"; font-family: "bar"; src: "baz" }', [
('src', [('STRING', 'lol')]),
('font-family', [('STRING', 'bar')]),
('src', [('STRING', 'baz')])], []),
])
def test_font_face_content(css, expected_declarations, expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
def declarations(rule):
return [(decl.name, list(jsonify(decl.value)))
for decl in rule.declarations]
if expected_declarations is None:
assert stylesheet.rules == []
assert expected_errors
else:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-face'
assert declarations(rule) == expected_declarations
@pytest.mark.parametrize(
('css', 'expected_rules', 'expected_errors'), [
('''@annotation{}''', None, [
'@annotation rule not allowed in stylesheet']),
('''@font-feature-values foo {}''', None, []),
('''@font-feature-values foo {
@swash { ornate: 1; }
@styleset { double-W: 14; sharp-terminals: 16 1; }
}''', [
('@swash', [('ornate', [('INTEGER', 1)])]),
('@styleset', [
('double-w', [('INTEGER', 14)]),
('sharp-terminals', [
('INTEGER', 16), ('S', ' '), ('INTEGER', 1)])])], []),
('''@font-feature-values foo {
@swash { ornate: 14; }
@unknown { test: 1; }
}''', [('@swash', [('ornate', [('INTEGER', 14)])])], [
'unknown at-rule in @font-feature-values context: @unknown']),
('''@font-feature-values foo {
@annotation{boxed:1}
bad: 2;
@brokenstylesetbecauseofbadabove { sharp: 1}
@styleset { sharp-terminals: 16 1; @bad {}}
@styleset { @bad {} top-ignored: 3; top: 9000}
really-bad
}''', [
('@annotation', [('boxed', [('INTEGER', 1)])]),
('@styleset', [
('sharp-terminals', [
('INTEGER', 16), ('S', ' '), ('INTEGER', 1)])]),
('@styleset', [('top', [('INTEGER', 9000)])])], [
'unexpected ; token in selector',
'expected a property name, got ATKEYWORD',
'expected a property name, got ATKEYWORD',
'no declaration block found for ruleset']),
])
def test_font_feature_values_content(css, expected_rules, expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
if expected_rules is not None:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-feature-values'
rules = [
(at_rule.at_keyword, [
(decl.name, list(jsonify(decl.value)))
for decl in at_rule.declarations])
for at_rule in rule.at_rules] if rule.at_rules else None
assert rules == expected_rules

View File

@@ -0,0 +1,302 @@
# coding: utf-8
"""
Tests for the tokenizer
-----------------------
:copyright: (c) 2012 by Simon Sapin.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import os
import sys
import pytest
from tinycss.tokenizer import (
cython_tokenize_flat, python_tokenize_flat, regroup)
def test_speedups():
is_pypy = hasattr(sys, 'pypy_translation_info')
env_skip_tests = os.environ.get('TINYCSS_SKIP_SPEEDUPS_TESTS')
# pragma: no cover
if is_pypy or env_skip_tests:
return
assert cython_tokenize_flat is not None, (
'Cython speedups are not installed, related tests will '
'be skipped. Set the TINYCSS_SKIP_SPEEDUPS_TESTS environment '
'variable if this is expected.')
@pytest.mark.parametrize(('tokenize', 'css_source', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
('', []),
('red -->', [('IDENT', 'red'), ('S', ' '), ('CDC', '-->')]),
# Longest match rule: no CDC
('red-->', [('IDENT', 'red--'), ('DELIM', '>')]),
(r'p[example="foo(int x) { this.x = x;}"]', [
('IDENT', 'p'),
('[', '['),
('IDENT', 'example'),
('DELIM', '='),
('STRING', 'foo(int x) { this.x = x;}'),
(']', ']')]),
# Numbers are parsed
('42 .5 -4pX 1.25em 30%', [
('INTEGER', 42), ('S', ' '),
('NUMBER', .5), ('S', ' '),
# units are normalized to lower-case:
('DIMENSION', -4, 'px'), ('S', ' '),
('DIMENSION', 1.25, 'em'), ('S', ' '),
('PERCENTAGE', 30, '%')]),
# URLs are extracted
('url(foo.png)', [('URI', 'foo.png')]),
('url("foo.png")', [('URI', 'foo.png')]),
# Escaping
(r'/* Comment with a \ backslash */', [
('COMMENT', '/* Comment with a \ backslash */')]), # Unchanged
# backslash followed by a newline in a string: ignored
('"Lorem\\\nIpsum"', [('STRING', 'LoremIpsum')]),
# backslash followed by a newline outside a string: stands for itself
('Lorem\\\nIpsum', [
('IDENT', 'Lorem'), ('DELIM', '\\'),
('S', '\n'), ('IDENT', 'Ipsum')]),
# Cancel the meaning of special characters
(r'"Lore\m Ipsum"', [('STRING', 'Lorem Ipsum')]), # or not specal
(r'"Lorem \49psum"', [('STRING', 'Lorem Ipsum')]),
(r'"Lorem \49 psum"', [('STRING', 'Lorem Ipsum')]),
(r'"Lorem\"Ipsum"', [('STRING', 'Lorem"Ipsum')]),
(r'"Lorem\\Ipsum"', [('STRING', r'Lorem\Ipsum')]),
(r'"Lorem\5c Ipsum"', [('STRING', r'Lorem\Ipsum')]),
(r'Lorem\+Ipsum', [('IDENT', 'Lorem+Ipsum')]),
(r'Lorem+Ipsum', [
('IDENT', 'Lorem'), ('DELIM', '+'), ('IDENT', 'Ipsum')]),
(r'url(foo\).png)', [('URI', 'foo).png')]),
# Unicode and backslash escaping
('\\26 B', [('IDENT', '&B')]),
('\\&B', [('IDENT', '&B')]),
('@\\26\tB', [('ATKEYWORD', '@&B')]),
('@\\&B', [('ATKEYWORD', '@&B')]),
('#\\26\nB', [('HASH', '#&B')]),
('#\\&B', [('HASH', '#&B')]),
('\\26\r\nB(', [('FUNCTION', '&B(')]),
('\\&B(', [('FUNCTION', '&B(')]),
(r'12.5\000026B', [('DIMENSION', 12.5, '&b')]),
(r'12.5\0000263B', [('DIMENSION', 12.5, '&3b')]), # max 6 digits
(r'12.5\&B', [('DIMENSION', 12.5, '&b')]),
(r'"\26 B"', [('STRING', '&B')]),
(r"'\000026B'", [('STRING', '&B')]),
(r'"\&B"', [('STRING', '&B')]),
(r'url("\26 B")', [('URI', '&B')]),
(r'url(\26 B)', [('URI', '&B')]),
(r'url("\&B")', [('URI', '&B')]),
(r'url(\&B)', [('URI', '&B')]),
(r'Lorem\110000Ipsum', [('IDENT', 'Lorem\uFFFDIpsum')]),
# Bad strings
# String ends at EOF without closing: no error, parsed
('"Lorem\\26Ipsum', [('STRING', 'Lorem&Ipsum')]),
# Unescaped newline: ends the string, error, unparsed
('"Lorem\\26Ipsum\n', [
('BAD_STRING', r'"Lorem\26Ipsum'), ('S', '\n')]),
# Tokenization restarts after the newline, so the second " starts
# a new string (which ends at EOF without errors, as above.)
('"Lorem\\26Ipsum\ndolor" sit', [
('BAD_STRING', r'"Lorem\26Ipsum'), ('S', '\n'),
('IDENT', 'dolor'), ('STRING', ' sit')]),
]])
def test_tokens(tokenize, css_source, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
sources = [css_source]
if sys.version_info[0] < 3:
# On Python 2.x, ASCII-only bytestrings can be used
# where Unicode is expected.
sources.append(css_source.encode('ascii'))
for css_source in sources:
tokens = tokenize(css_source, ignore_comments=False)
result = [
(token.type, token.value) + (
() if token.unit is None else (token.unit,))
for token in tokens
]
assert result == expected_tokens
@pytest.mark.parametrize('tokenize', [
python_tokenize_flat, cython_tokenize_flat])
def test_positions(tokenize):
"""Test the reported line/column position of each token."""
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
css = '/* Lorem\nipsum */\fa {\n color: red;\tcontent: "dolor\\\fsit" }'
tokens = tokenize(css, ignore_comments=False)
result = [(token.type, token.line, token.column) for token in tokens]
assert result == [
('COMMENT', 1, 1), ('S', 2, 9),
('IDENT', 3, 1), ('S', 3, 2), ('{', 3, 3),
('S', 3, 4), ('IDENT', 4, 5), (':', 4, 10),
('S', 4, 11), ('IDENT', 4, 12), (';', 4, 15), ('S', 4, 16),
('IDENT', 4, 17), (':', 4, 24), ('S', 4, 25), ('STRING', 4, 26),
('S', 5, 5), ('}', 5, 6)]
@pytest.mark.parametrize(('tokenize', 'css_source', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
('', []),
(r'Lorem\26 "i\psum"4px', [
('IDENT', 'Lorem&'), ('STRING', 'ipsum'), ('DIMENSION', 4)]),
('not([[lorem]]{ipsum (42)})', [
('FUNCTION', 'not', [
('[', [
('[', [
('IDENT', 'lorem'),
]),
]),
('{', [
('IDENT', 'ipsum'),
('S', ' '),
('(', [
('INTEGER', 42),
])
])
])]),
# Close everything at EOF, no error
('a[b{"d', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('STRING', 'd'),
]),
]),
]),
# Any remaining ), ] or } token is a nesting error
('a[b{d]e}', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('IDENT', 'd'),
(']', ']'), # The error is visible here
('IDENT', 'e'),
]),
]),
]),
# ref:
('a[b{d}e]', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('IDENT', 'd'),
]),
('IDENT', 'e'),
]),
]),
]])
def test_token_grouping(tokenize, css_source, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
tokens = regroup(tokenize(css_source, ignore_comments=False))
result = list(jsonify(tokens))
assert result == expected_tokens
def jsonify(tokens):
"""Turn tokens into "JSON-compatible" data structures."""
for token in tokens:
if token.type == 'FUNCTION':
yield (token.type, token.function_name,
list(jsonify(token.content)))
elif token.is_container:
yield token.type, list(jsonify(token.content))
else:
yield token.type, token.value
@pytest.mark.parametrize(('tokenize', 'ignore_comments', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
(False, [
('COMMENT', '/* lorem */'),
('S', ' '),
('IDENT', 'ipsum'),
('[', [
('IDENT', 'dolor'),
('COMMENT', '/* sit */'),
]),
('BAD_COMMENT', '/* amet')
]),
(True, [
('S', ' '),
('IDENT', 'ipsum'),
('[', [
('IDENT', 'dolor'),
]),
]),
]])
def test_comments(tokenize, ignore_comments, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
css_source = '/* lorem */ ipsum[dolor/* sit */]/* amet'
tokens = regroup(tokenize(css_source, ignore_comments))
result = list(jsonify(tokens))
assert result == expected_tokens
@pytest.mark.parametrize(('tokenize', 'css_source'), [
(tokenize, test_data)
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
r'p[example="foo(int x) { this.x = x;}"]',
'"Lorem\\26Ipsum\ndolor" sit',
'/* Lorem\nipsum */\fa {\n color: red;\tcontent: "dolor\\\fsit" }',
'not([[lorem]]{ipsum (42)})',
'a[b{d]e}',
'a[b{"d',
]])
def test_token_serialize_css(tokenize, css_source):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
for _regroup in [regroup, lambda x: x]:
tokens = _regroup(tokenize(css_source, ignore_comments=False))
result = ''.join(token.as_css() for token in tokens)
assert result == css_source
@pytest.mark.parametrize(('tokenize', 'css_source'), [
(tokenize, test_data)
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
'(8, foo, [z])', '[8, foo, (z)]', '{8, foo, [z]}', 'func(8, foo, [z])'
]
])
def test_token_api(tokenize, css_source):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
tokens = list(regroup(tokenize(css_source)))
assert len(tokens) == 1
token = tokens[0]
expected_len = 7 # 2 spaces, 2 commas, 3 others.
assert len(token.content) == expected_len

View File

@@ -0,0 +1 @@
VERSION = '0.4'