Repository: Kozea/WeasyPrint

Issue:
Half of word-spacing to a space at the end of a TextBox
Hello, I get different results from Chrome and Weasyprint when trying to use word-spacing in text that ends with a space.  Weasyprint only adds half of word-spacing to these cases.

The following code illustrates the problem.

```html
<style type="text/css">
	@page { size: 200px; margin: 0 }
	body { margin: 0; font: 15pt monospace }
	.ws { word-spacing: 50px }
	.ws2 { word-spacing: 100px }
</style>
<div class="ws">a b</div>
<div class="ws">c <b></b>d</div>
<div class="ws2">e <b></b>f</div>
```

WeasyPrint:
![image](https://user-images.githubusercontent.com/70984536/222994077-57ef0cff-df03-4a6e-b10c-10c5998ce7cc.png)

Chrome:
![image](https://user-images.githubusercontent.com/70984536/222994042-83bc5c23-6304-4f95-86ed-3be188eeb69d.png)

I found a reference to this problem in this TODO:
https://github.com/Kozea/WeasyPrint/blob/4377c38b2971a3a3c6c7f3a6de2c49a585d65f68/tests/test_text.py#L473-L474

Currently, I am fixing this problem changing this code
https://github.com/Kozea/WeasyPrint/blob/4377c38b2971a3a3c6c7f3a6de2c49a585d65f68/weasyprint/text/line_break.py#L184-L190

to this
```python
if word_spacing:
    space_spacing = (
        units_from_double(word_spacing) + letter_spacing)
    position = bytestring.find(b' ')
    last_position = len(bytestring) - 1
    while position != -1:
        factor = 1 + (position == last_position)
        add_attr(position, position + 1, factor * space_spacing)
        position = bytestring.find(b' ', position + 1)
```

Discussion:

Patch:
diff --git a/weasyprint/text/line_break.py b/weasyprint/text/line_break.py
index f537a033..85e8a2e3 100644
--- a/weasyprint/text/line_break.py
+++ b/weasyprint/text/line_break.py
@@ -182,11 +182,20 @@ class Layout:
                 add_attr(0, len(bytestring), letter_spacing)
 
             if word_spacing:
+                if bytestring == b' ':
+                    # We need more than one space to set word spacing
+                    self.text = ' ​'  # Space + zero-width space
+                    text, bytestring = unicode_to_char_p(self.text)
+                    pango.pango_layout_set_text(self.layout, text, -1)
+
                 space_spacing = (
                     units_from_double(word_spacing) + letter_spacing)
                 position = bytestring.find(b' ')
+                # Pango gives only half of word-spacing on boundaries
+                boundary_positions = (0, len(bytestring) - 1)
                 while position != -1:
-                    add_attr(position, position + 1, space_spacing)
+                    factor = 1 + (position in boundary_positions)
+                    add_attr(position, position + 1, factor * space_spacing)
                     position = bytestring.find(b' ', position + 1)
 
             if word_breaking:


Test patch:
diff --git a/tests/test_text.py b/tests/test_text.py
index bbfd0f7f..f471c8f9 100644
--- a/tests/test_text.py
+++ b/tests/test_text.py
@@ -458,8 +458,16 @@ def test_text_align_justify_no_break_between_children():
     assert span_3.position_x == 5 * 16  # (3 + 1) characters + 1 space
 
 
+@pytest.mark.parametrize('text', (
+    'Lorem ipsum dolor<em>sit amet</em>',
+    'Lorem ipsum <em>dolorsit</em> amet',
+    'Lorem ipsum <em></em>dolorsit amet',
+    'Lorem ipsum<em> </em>dolorsit amet',
+    'Lorem ipsum<em> dolorsit</em> amet',
+    'Lorem ipsum <em>dolorsit </em>amet',
+))
 @assert_no_logs
-def test_word_spacing():
+def test_word_spacing(text):
     # keep the empty <style> as a regression test: element.text is None
     # (Not a string.)
     page, = render_pages('''
@@ -470,15 +478,14 @@ def test_word_spacing():
     line, = body.children
     strong_1, = line.children
 
-    # TODO: Pango gives only half of word-spacing to a space at the end
-    # of a TextBox. Is this what we want?
     page, = render_pages('''
       <style>strong { word-spacing: 11px }</style>
-      <body><strong>Lorem ipsum dolor<em>sit amet</em></strong>''')
+      <body><strong>%s</strong>''' % text)
     html, = page.children
     body, = html.children
     line, = body.children
     strong_2, = line.children
+
     assert strong_2.width - strong_1.width == 33