Repository: Kozea/WeasyPrint
Pull request #855 (2019-04-23 22:15:08)

Add support for full CSS3 text-decoration [RFC]
* parse text-decoration as shorthand
* draw different colors
* basic text-decoration-style
* no 'wavy' support for now

One problem of the way it's currently done is that it makes weird glitches with dashed/dotted style when elements are found in the decorated text:

```html
<p style="text-decoration: underline green dashed">Lorem <span>ipsum</span>
dolor sit amet, <span>consectetur</span> adipiscing elit, sed do eiusmod tempor 
ncididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.</p>
```


randomstuff: This is trying to address #571.

Patch:
diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py
index 55e20ca0..3e762693 100644
--- a/weasyprint/css/properties.py
+++ b/weasyprint/css/properties.py
@@ -53,7 +53,9 @@ INITIAL_VALUES = {
     'position': 'static',
     'right': 'auto',
     'table_layout': 'auto',
-    'text_decoration': 'none',
+    'text_decoration_line': 'none',
+    'text_decoration_color': 'currentColor',
+    'text_decoration_style': 'solid',
     'top': 'auto',
     'unicode_bidi': 'normal',
     'vertical_align': 'baseline',
@@ -246,7 +248,9 @@ INHERITED = {
     'quotes',
     'tab_size',
     'text_align',
-    'text_decoration',
+    'text_decoration_line',
+    'text_decoration_color',
+    'text_decoration_style',
     'text_indent',
     'text_transform',
     'visibility',
diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py
index 42ccd70a..40ede132 100644
--- a/weasyprint/css/validation/expanders.py
+++ b/weasyprint/css/validation/expanders.py
@@ -345,6 +345,46 @@ def expand_background(base_url, name, tokens):
     yield 'background-color', color
 
 
+@expander('text-decoration')
+def expand_text_decoration(base_url, name, tokens):
+
+    text_decoration_line = set()
+    text_decoration_color = None
+    text_decoration_style = None
+
+    for token in tokens:
+        keyword = get_keyword(token)
+        if keyword in ('none', 'underline', 'overline', 'line-through',
+                       'blink'):
+            text_decoration_line.add(keyword)
+        elif keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
+            if text_decoration_style is not None:
+                raise InvalidValues
+            else:
+                text_decoration_style = keyword
+        else:
+            color = parse_color(token)
+            if color is None:
+                raise InvalidValues
+            elif text_decoration_color is not None:
+                raise InvalidValues
+            else:
+                text_decoration_color = color
+
+    if 'none' in text_decoration_line:
+        if len(text_decoration_line) != 1:
+            raise InvalidValues
+        text_decoration_line = 'none'
+    elif not(text_decoration_line):
+        text_decoration_line = 'none'
+    else:
+        text_decoration_line = frozenset(text_decoration_line - set(['blink']))
+
+    yield 'text_decoration_line', text_decoration_line
+    yield 'text_decoration_color', text_decoration_color or 'currentColor'
+    yield 'text_decoration_style', text_decoration_style or 'solid'
+
+
 @expander('page-break-after')
 @expander('page-break-before')
 def expand_page_break_before_after(base_url, name, tokens):
diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py
index 436bfa17..0b8e8c5c 100644
--- a/weasyprint/css/validation/properties.py
+++ b/weasyprint/css/validation/properties.py
@@ -128,6 +128,7 @@ def background_attachment(keyword):
 @property('border-bottom-color')
 @property('border-left-color')
 @property('column-rule-color', unstable=True)
+@property('text-decoration-color')
 @single_token
 def other_colors(token):
     return parse_color(token)
@@ -977,8 +978,8 @@ def text_align(keyword):
 
 
 @property()
-def text_decoration(tokens):
-    """``text-decoration`` property validation."""
+def text_decoration_line(tokens):
+    """``text-decoration-line`` property validation."""
     keywords = [get_keyword(v) for v in tokens]
     if keywords == ['none']:
         return 'none'
@@ -992,6 +993,14 @@ def text_decoration(tokens):
             return frozenset(unique - set(['blink']))
 
 
+@property()
+@single_keyword
+def text_decoration_style(keyword):
+    """``text-decoration-style`` property validation."""
+    if keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
+        return keyword
+
+
 @property()
 @single_token
 def text_indent(token):
diff --git a/weasyprint/draw.py b/weasyprint/draw.py
index 2b31afac..bc7086f3 100644
--- a/weasyprint/draw.py
+++ b/weasyprint/draw.py
@@ -1019,12 +1019,15 @@ def draw_text(context, textbox, enable_hinting):
     textbox.pango_layout.reactivate(textbox.style)
     show_first_line(context, textbox)
 
-    values = textbox.style['text_decoration']
+    values = textbox.style['text_decoration_line']
 
     thickness = textbox.style['font_size'] / 18  # Like other browsers do
     if enable_hinting and thickness < 1:
         thickness = 1
 
+    color = textbox.style['text_decoration_color']
+    if color == 'currentColor':
+        color = textbox.style['color']
     if ('overline' in values or
             'line-through' in values or
             'underline' in values):
@@ -1033,29 +1036,43 @@ def draw_text(context, textbox, enable_hinting):
         draw_text_decoration(
             context, textbox,
             textbox.baseline - metrics.ascent + thickness / 2,
-            thickness, enable_hinting)
+            thickness, enable_hinting, color)
     if 'underline' in values:
         draw_text_decoration(
             context, textbox,
             textbox.baseline - metrics.underline_position + thickness / 2,
-            thickness, enable_hinting)
+            thickness, enable_hinting, color)
     if 'line-through' in values:
         draw_text_decoration(
             context, textbox,
             textbox.baseline - metrics.strikethrough_position,
-            thickness, enable_hinting)
+            thickness, enable_hinting, color)
 
     textbox.pango_layout.deactivate()
 
 
 def draw_text_decoration(context, textbox, offset_y, thickness,
-                         enable_hinting):
+                         enable_hinting, color):
     """Draw text-decoration of ``textbox`` to a ``cairo.Context``."""
+    style = textbox.style['text_decoration_style']
     with stacked(context):
         if enable_hinting:
             context.set_antialias(cairo.ANTIALIAS_NONE)
-        context.set_source_rgba(*textbox.style['color'])
+        context.set_source_rgba(*color)
         context.set_line_width(thickness)
+
+        if style == "dashed":
+            context.set_dash([5 * thickness])
+        elif style == "dotted":
+            context.set_dash([thickness])
+
         context.move_to(textbox.position_x, textbox.position_y + offset_y)
         context.rel_line_to(textbox.width, 0)
         context.stroke()
+
+        if style == "double":
+            delta = 2 * thickness
+            context.move_to(textbox.position_x, textbox.position_y
+                            + offset_y + delta)
+            context.rel_line_to(textbox.width, 0)
+            context.stroke()


Test patch:
diff --git a/weasyprint/tests/test_css.py b/weasyprint/tests/test_css.py
index 50fa8bc2..c697af86 100644
--- a/weasyprint/tests/test_css.py
+++ b/weasyprint/tests/test_css.py
@@ -136,7 +136,7 @@ def test_annotate_document():
     assert li_0['margin_bottom'] == (16, 'px')
     assert li_0['margin_left'] == (32, 'px')  # 4em
 
-    assert a['text_decoration'] == frozenset(['underline'])
+    assert a['text_decoration_line'] == frozenset(['underline'])
     assert a['font_weight'] == 900
     assert a['font_size'] == 24  # 300% of 8px
     assert a['padding_top'] == (1, 'px')
diff --git a/weasyprint/tests/test_css_validation.py b/weasyprint/tests/test_css_validation.py
index 2bc4b985..e27f3266 100644
--- a/weasyprint/tests/test_css_validation.py
+++ b/weasyprint/tests/test_css_validation.py
@@ -131,14 +131,47 @@ def test_spacing_invalid(rule):
 
 @assert_no_logs
 @pytest.mark.parametrize('rule, result', (
-    ('text-decoration: none', {'text_decoration': 'none'}),
+    ('text-decoration-line: none', {'text_decoration_line': 'none'}),
+    ('text-decoration-line: overline', {
+        'text_decoration_line': frozenset(['overline'])}),
+    ('text-decoration-line: overline blink line-through', {
+        'text_decoration_line': frozenset(['line-through', 'overline'])}),
+))
+def test_decoration_line(rule, result):
+    assert expand_to_dict(rule) == result
+
+
+@assert_no_logs
+@pytest.mark.parametrize('rule, result', (
+    ('text-decoration-style: solid', {'text_decoration_style': 'solid'}),
+    ('text-decoration-style: double', {'text_decoration_style': 'double'}),
+    ('text-decoration-style: dotted', {'text_decoration_style': 'dotted'}),
+    ('text-decoration-style: dashed', {'text_decoration_style': 'dashed'}),
+    ('text-decoration-style: wavy', {'text_decoration_style': 'wavy'}),
+))
+def test_decoration_style(rule, result):
+    assert expand_to_dict(rule) == result
+
+
+TEXT_DECORATION_DEFAULT = {
+    'text_decoration_line': 'none',
+    'text_decoration_color': 'currentColor',
+    'text_decoration_style': 'solid',
+}
+
+
+@assert_no_logs
+@pytest.mark.parametrize('rule, result', (
+    ('text-decoration: none', {'text_decoration_line': 'none'}),
     ('text-decoration: overline', {
-        'text_decoration': frozenset(['overline'])}),
+        'text_decoration_line': frozenset(['overline'])}),
     ('text-decoration: overline blink line-through', {
-        'text_decoration': frozenset(['line-through', 'overline'])}),
+        'text_decoration_line': frozenset(['line-through', 'overline'])}),
+    ('text-decoration: red', {'text_decoration_color': (1, 0, 0, 1)}),
 ))
 def test_decoration(rule, result):
-    assert expand_to_dict(rule) == result
+    real_result = {**TEXT_DECORATION_DEFAULT, **result}
+    assert expand_to_dict(rule) == real_result
 
 
 @assert_no_logs