w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-text] What does the `white-space-collapse` apply to when white-space trimming/positioning #9724

Open andreubotella opened 10 months ago

andreubotella commented 10 months ago

CSS-TEXT section 4.1.2 talks about the rendering and hanging of spaces in each line, and step 4 talks about whether a sequence of trailing spaces at the end of the line hangs, which depends on the values of white-space-collapse (and text-wrap-mode).

However, when you look at it in detail, it's not clear what value should be used here. This whole section applies to the line as a whole, and a sequence of trailing spaces can span inlines. Should then each preserved space in the sequence of the line's trailing spaces hang or not depending on its white-space-collapse value? This doesn't seem to make sense, since this would allow for some spaces to hang when not at the end of the line, which doesn't seem to make sense, and is not allowed by the definition of hanging.

<div style="white-space-collapse: continue">
foo&x#3000;&x#3000;&x#3000;<span style="white-space-collapse: break-spaces">        </span>
</div>

In this example, if the entirety of the text fits within the line, the ideographic spaces would hang (since they're not collapsed in phase I, so they count as preserved, but they're still white-space-collapse: continue), but the spaces inside the span wouldn't.

Because of this, when fixing hanging in Blink to align with the spec, I understood the white-space-collapse (and text-wrap-mode) values as applying to the line, rather than the spaces. Blink has a concept of a line box's style, which apparently is not in the spec (although I was not aware of that at the time of implementing this), which seems to be the same as the containing block box's computed style (except maybe including ::first-line styles) – and that is what I used in my implementation. This led to differences with other browsers in cases like this:

<div style="white-space-collapse: collapse; text-align: center">
Something.<span style="white-space-collapse: preserve; text-wrap-mode: wrap;">          </span>
</div>

If the relevant white-space-collapse (and text-wrap-mode) values should be those corresponding to each preserved space character, then the trailing spaces inside the span conditionally hang (since this is the last line), and therefore the "Something." text will be off-center (assuming that the text fully fits within the line, whether the spaces do or not). However, if the relevant values should be the containing block box's, then the spaces should hang (unconditionally) and the "Something." text will appear centered.

cc @kojiishi @jfkthame @fantasai @frivoal

frivoal commented 10 months ago

It does apply to the spaces, not the the line or the whole block. white-space-collapse and text-wrap-mode are explicitly defined as applied to text, not to blocks, and that's deliberate.

If towards the end of a line you've got some spaces that cannot hang followed by some spaces that can, then only the ones that can hang get to hang, and the others do not. Similarly, if you've got some non collapsible spaces followed by some collapsible spaces at the end of the line, then step 3 gets rids of the collapsible spaces, and those other non collapsible spaces remain.

If it is the other way around, and you've got a sequence of collapsible spaces followed by a sequence of non collapsible spaces, then the sequence of collapsible spaces aren't at the end of the line (the non-collapsible ones are), and so step 3 doesn't discard them.

Similarly, if you've got some spaces that can hang, followed by some that cannot, then none of them hang, because the spaces at the end of the line aren't hangable, and those that are hangable aren't at the end of the line.

I feel like the case with collapsible and non collapsible isn't even that hard to construct:

<style>
code {
  white-space: pre;
  font-family: monospace; /* not important, just to make it realistic */
  background: lightgray; /* not important, just to make it realistic */
}
</style>
<p>
  In Makefiles, do no confuse sequences of spaces like <code>        </code>
  with tabs like <code>&#9;</code> as they have a different behavior
  even if they look the same.

If the line ends so that there's room for the code element and a little more, but not enough for the next word, then you've got a collapsible space followed by the non collapsible sequence of spaces of the code element, followed by the collapsible space after the code element, at the end of the line. In this case, you remove the collapsible space after the code element, and keep both the non collapsible spaces in the code element (they're not collapsible) and the collapsible space before it (it's not at the end of the line).

I feel like realistic examples with hangable / non hangable spaces are a tad more difficult to construct (at least in English), but the logic is the same.

andreubotella commented 10 months ago

If that is the intended reading, the spec should probably clarify that, I think. I'll work on a PR to do that.

andreubotella commented 10 months ago

What would happen if a sequence of whitespace that hangs unconditionally is followed by a sequence that hangs conditionally?

frivoal commented 10 months ago

What would happen if a sequence of whitespace that hangs unconditionally is followed by a sequence that hangs conditionally?

Same logic? In cases where the condition is met and (all) the conditionally hangable spaces do hang because they didn't fit the line prior to justification, then condition part is fullfilled. The (conditionally) hanging spaces are just hanging spaces, as are the unconditionally hanging spaces at the end of the line, so it's just one big sequence of hanging spaces at the end of the line, and it can hang.

However, if there remains at least one conditionally hangable space at the end of the line which is not hanging because it did fit the line prior to justification, then the sequence of unconditionally hangable spaces aren't actually at the end of the line since there's something in between (a non-hanging space), and thus they don't hang.

andreubotella commented 10 months ago
<style>
  div {
    border: 1px solid black;
    text-align: center;
    animation: 5s linear infinite alternate width-animation;
  }
  .unconditional {
    white-space: normal;
    background-color: purple;
    opacity: 0.6;
  }
  .conditional {
    white-space: pre-wrap;
    background-color: orange;
    opacity: 0.6;
  }
</style>

<div><!--
  -->qwertyuiop<!--
  --><span class="unconditional">&#x3000;&#x3000;&#x3000;&#x3000;&#x3000;</span><!--
  --><span class="conditional">             </span><!--
--></div>

I understand that a correct implementation should render this animation, then.

frivoal commented 2 months ago

I agree with the above logic, as well as with the tests introduced in https://github.com/web-platform-tests/wpt/pull/44384.

@fantasai, can you have a look too and see if you agree?

As for the spec

An alternative interpretation for the last case could be that any sequence of unconditionally hangable glyphs followed conditionally hangable glyphs becomes conditionally hangable as well. Honestly, while I can construct cases where this happens, I'm having a hard time thinking realistic cases where this would matter.