Skip to content

Commit

Permalink
Merge pull request #843 from Kozea/variables
Browse files Browse the repository at this point in the history
Add CSS variables
  • Loading branch information
liZe authored Apr 10, 2019
2 parents 5a2553e + bd77dce commit 5ceddcb
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ WeasyPrint |version| depends on:
* CFFI_ ≥ 0.6
* html5lib_ ≥ 0.999999999
* cairocffi_ ≥ 0.9.0
* tinycss2_ ≥ 0.5
* tinycss2_ ≥ 1.0.0
* cssselect2_ ≥ 0.1
* CairoSVG_ ≥ 1.0.20
* Pyphen_ ≥ 0.8
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ install_requires =
cffi>=0.6
html5lib>=0.999999999
cairocffi>=0.9.0
tinycss2>=0.5
tinycss2>=1.0.0
cssselect2>=0.1
CairoSVG>=1.0.20
Pyphen>=0.8
Expand Down
14 changes: 12 additions & 2 deletions weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,9 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
# Fast path for anonymous boxes:
# no cascaded style, only implicitly initial or inherited values.
computed = dict(INITIAL_VALUES)
for name in INHERITED:
computed[name] = parent_style[name]
for name in parent_style:
if name in INHERITED or name.startswith('__'):
computed[name] = parent_style[name]
# page is not inherited but taken from the ancestor if 'auto'
computed['page'] = parent_style['page']
# border-*-style is none, so border-width computes to zero.
Expand All @@ -609,6 +610,15 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
# Handle inheritance and initial values
specified = {}
computed = {}

if parent_style:
for name in parent_style:
if name.startswith('__'):
specified[name] = parent_style[name]
for name in cascaded:
if name.startswith('__'):
specified[name] = cascaded[name][0]

for name, initial in INITIAL_VALUES.items():
if name in cascaded:
value, _precedence = cascaded[name]
Expand Down
64 changes: 62 additions & 2 deletions weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
from .. import text
from ..logger import LOGGER
from ..urls import get_link_attribute
from .properties import INITIAL_VALUES, Dimension
from .properties import INHERITED, INITIAL_VALUES, Dimension
from .utils import (
ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, safe_urljoin)
ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function,
safe_urljoin)

ZERO_PIXELS = Dimension(0, 'px')

Expand Down Expand Up @@ -139,6 +140,33 @@ def _computing_order():
COMPUTER_FUNCTIONS = {}


def _resolve_var(computed, variable_name, default):
known_variable_names = [variable_name]

computed_value = computed.get(variable_name)
if computed_value and len(computed_value) == 1:
value = computed_value[0]
if value.type == 'ident' and value.value == 'initial':
return default

computed_value = computed.get(variable_name, default)
while (computed_value and
isinstance(computed_value, tuple)
and len(computed_value) == 1):
var_function = check_var_function(computed_value[0])
if var_function:
new_variable_name, new_default = var_function[1]
if new_variable_name in known_variable_names:
computed_value = default
break
known_variable_names.append(new_variable_name)
computed_value = computed.get(new_variable_name, new_default)
default = new_default
else:
break
return computed_value


def register_computer(name):
"""Decorator registering a property ``name`` for a function."""
name = name.replace('-', '_')
Expand Down Expand Up @@ -167,6 +195,7 @@ def compute(element, pseudo_type, specified, computed, parent_style,
:param target_collector: A target collector used to get computed targets.
"""
from .validation.properties import PROPERTIES

def computer():
"""Dummy object that holds attributes."""
Expand All @@ -187,13 +216,44 @@ def computer():

getter = COMPUTER_FUNCTIONS.get

for name in specified:
if name.startswith('__'):
computed[name] = specified[name]

for name in COMPUTING_ORDER:
if name in computed:
# Already computed
continue

value = specified[name]
function = getter(name)

if value and isinstance(value, tuple) and value[0] == 'var()':
variable_name, default = value[1]
computed_value = _resolve_var(computed, variable_name, default)
if computed_value is None:
new_value = None
else:
new_value = PROPERTIES[name.replace('_', '-')](computed_value)

# See https://drafts.csswg.org/css-variables/#invalid-variables
if new_value is None:
try:
computed_value = ''.join(
token.serialize() for token in computed_value)
except BaseException:
pass
LOGGER.warning(
'Unsupported computed value `%s` set in variable `%s` '
'for property `%s`.', computed_value,
variable_name.replace('_', '-'), name.replace('_', '-'))
if name in INHERITED and parent_style:
value = parent_style[name]
else:
value = INITIAL_VALUES[name]
else:
value = new_value

if function is not None:
value = function(computer, name, value)
# else: same as specified
Expand Down
19 changes: 18 additions & 1 deletion weasyprint/css/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def parse_function(function_token):
space-separated arguments. Return ``None`` otherwise.
"""
if not function_token.type == 'function':
if not getattr(function_token, 'type', None) == 'function':
return

content = list(remove_whitespace(function_token.arguments))
Expand All @@ -397,6 +397,8 @@ def parse_function(function_token):
if argument_function is None:
return
arguments.append(token)
if last_is_comma:
return
return function_token.lower_name, arguments


Expand Down Expand Up @@ -499,6 +501,21 @@ def check_string_function(token):
return ('string()', (custom_ident, ident))


def check_var_function(token):
function = parse_function(token)
if function is None:
return
name, args = function
if name == 'var' and args:
ident = args.pop(0)
if ident.type != 'ident' or not ident.value.startswith('--'):
return

# TODO: we should check authorized tokens
# https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
return ('var()', (ident.value.replace('-', '_'), args or None))


def get_string(token):
"""Parse a <string> token."""
if token.type == 'string':
Expand Down
6 changes: 4 additions & 2 deletions weasyprint/css/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def preprocess_declarations(base_url, declarations):
if declaration.type != 'declaration':
continue

name = declaration.lower_name
name = declaration.name
if not name.startswith('--'):
name = declaration.lower_name

def validation_error(level, reason):
getattr(LOGGER, level)(
Expand Down Expand Up @@ -114,7 +116,7 @@ def validation_error(level, reason):
unprefixed_name)
continue

if name.startswith('-'):
if name.startswith('-') and not name.startswith('--'):
validation_error('debug', 'prefixed selectors are ignored')
continue

Expand Down
18 changes: 14 additions & 4 deletions weasyprint/css/validation/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
from .. import computed_values
from ..properties import KNOWN_PROPERTIES, Dimension
from ..utils import (
InvalidValues, comma_separated_list, get_angle, get_content_list,
get_content_list_token, get_custom_ident, get_image, get_keyword,
get_length, get_resolution, get_single_keyword, get_url, parse_2d_position,
parse_background_position, parse_function, single_keyword, single_token)
InvalidValues, check_var_function, comma_separated_list, get_angle,
get_content_list, get_content_list_token, get_custom_ident, get_image,
get_keyword, get_length, get_resolution, get_single_keyword, get_url,
parse_2d_position, parse_background_position, parse_function,
single_keyword, single_token)

PREFIX = '-weasy-'
PROPRIETARY = set()
Expand Down Expand Up @@ -80,6 +81,10 @@ def decorator(function):

def validate_non_shorthand(base_url, name, tokens, required=False):
"""Default validator for non-shorthand properties."""
if name.startswith('--'):
# TODO: validate content
return ((name, tokens),)

if not required and name not in KNOWN_PROPERTIES:
hyphens_name = name.replace('_', '-')
if hyphens_name in KNOWN_PROPERTIES:
Expand All @@ -90,6 +95,11 @@ def validate_non_shorthand(base_url, name, tokens, required=False):
if not required and name not in PROPERTIES:
raise InvalidValues('property not supported yet')

for token in tokens:
var_function = check_var_function(token)
if var_function:
return ((name, var_function),)

keyword = get_single_keyword(tokens)
if keyword in ('initial', 'inherit'):
value = keyword
Expand Down
97 changes: 97 additions & 0 deletions weasyprint/tests/test_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
weasyprint.tests.test_variables
-------------------------------
Test CSS custom proproperties, also known as CSS variables.
:copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""

from .test_boxes import render_pages as parse


def test_variable_simple():
page, = parse('''
<style>
p { --var: 10px; width: var(--var) }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10


def test_variable_inherit():
page, = parse('''
<style>
html { --var: 10px }
p { width: var(--var) }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10


def test_variable_inherit_override():
page, = parse('''
<style>
html { --var: 20px }
p { width: var(--var); --var: 10px }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10


def test_variable_case_sensitive():
page, = parse('''
<style>
html { --var: 20px }
body { --VAR: 10px }
p { width: var(--VAR) }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10


def test_variable_chain():
page, = parse('''
<style>
html { --foo: 10px }
body { --var: var(--foo) }
p { width: var(--var) }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10


def test_variable_initial():
page, = parse('''
<style>
html { --var: initial }
p { width: var(--var, 10px) }
</style>
<p></p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert paragraph.width == 10

0 comments on commit 5ceddcb

Please sign in to comment.