:hide-uri-scheme:
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:
endif::[]
:author: Jules Sam. Randolph
:revnumber: 2.1.0
:toc: left

= CSS Whitespace Collapsing

|===
|*{author} ― v{revnumber}*
|===

== Problem Frame

The problem addressed in this document relates to the implementation of CSS
whitespace rendering requirements in a React tree. The full requirements are
exposed in W3C's https://www.w3.org/TR/css-text-3/[CSS Text Module Level 3],
and more precisely chapters 3 and 4.

This document aims at a subset of these requirements. The following scope
limitations should apply:

* Only the behavior for CSS `white-space: normal;` rule is of interest.
Alternative values such as `pre-line`, `break-spaces`, and others will not be
addressed.
* Only LTR (Left to Right) layouts will be considered (no bidi reordering).
* Only horizontal text layouts will be considered.
* Ruby annotations will not be considered.

From now on, this subset of white-space rendering requirements will be referred to as the
_limited whitespace collapsing requirements_.

____
The problem can be defined as follows:

* Adapt the limited whitespace collapsing requirements to React Native Render HTML library.
* Define a transient structure to facilitate such implementation.
____

== Data Flow

A new structure is offered, the transient render tree, to formalize and
enhance the preexisting implementation. This structure is detailed in <<transient-render-tree>>.

The below flow is proposed to account for the new whitespace collapsing requirements:

. *Parsing*: the HTML is parsed into a document tree.
. *Render Tree Generation*: a transient render tree is build to account
for React-Native constraints and prepare a structure to be rendered in a React
tree.
.. *Translating*: each document node is translated to a transient node (see
<<translate-tree>> algorithm).
.. *Transforming*: the transient tree is mutated.
... *Hoisting*: the tree is reordered to account for transient node constraints,
which are derived from React Native constraints (see <<hoist-tree>> algorithm).
... *Collapsing*: the transient render tree is walked over in order to collapse
  white spaces according to the limited whitespace collapsing requirements (see
  <<collapse-tree>> algorithm).
. *Rendering*: the transient render tree is transformed into a React tree.


[[transient-render-tree]]
== Transient Render Tree Data Structure

A transient render tree is equal to its root <<tnode>> (transient node).
This intermediary structure offers multiple advantages:

- Easy style inheritance;
- Easing of whitespace collapsing implementation;
- Comprehensive rendering logic.

[[tnode]]
=== TNode

A transient node is a node from a transient render tree.

``` ts
interface TNode {
    type: 'block' | 'phrasing' | 'text' | 'empty';
    attributes: Record<string, string>;
    children: TNode[];
    isAnchor: boolean;
    isCollapsibleLeft(): boolean;
    isCollapsibleRight(): boolean;
    isWhitespace(): boolean;
    isEmpty(): boolean;
    trimLeft(): void;
    trimRight(): void;
    getFirstChild(): TNode | null;
    getLastChild(): TNode | null;
}
```

isCollapsibleLeft():: Returns the result of _firstChild.isCollapsibleLeft()_.
isCollapsibleRight():: Returns the result of _lastChild.isCollapsibleRight()_.
isWhitespace():: Default implementation returns `false`.
isEmpty():: Default implementation returns `false`.
trimLeft():: Invokes _firstChild.trimLeft_, and remove this child if empty.
trimRight():: Invokes _lastChild.trimRight_, and remove this child if empty.

[[tblock]]
=== TBlock

A transient block node represents block content. 

Constrains:: Its children must be any list of <<tphrasing>>, <<tblock>> or
<<tempty>> nodes.
Rendering::
It will generally be rendered as a React Native _collapsedChildren_<View />`, but could be
rendered as anything using custom renderers.
Notes::
A transient block node can be anonymous when no corresponding tag name is
specified.

```ts
interface TBlock extends TNode {
    type: 'block';
    tagName?: string;
}
```

[[tphrasing]]
=== TPhrasing

A transient phrasing node represents a mix of transient text and phrasing
nodes.

Constrains:: Its children must be any list of <<ttext>>, <<tphrasing>> or
<<tempty>> nodes.
Rendering::
It will be rendered as a React Native `<Text />` node, and thus creates
an inline formatting context for its children.
Notes::
A transient phrasing node can be anonymous when no corresponding tag name is
specified.

```ts
interface TPhrasing extends TNode {
    type: 'phrasing';
    tagName?: string;
}
```

isEmpty():: Returns _true_ if for every _child_ of_children_, _child.isEmpty()_
is _true_.
isWhitespace():: Return _true_ if for every _child_ of _children_,
_child.isWhitespace()_ is _true_.

[[ttext]]
=== TText

Constrains:: Must not have children.
Rendering::
A transient text node represents raw text, optionally its surrounding tag.

```ts
interface TText extends TNode {
    type: 'text';
    tagName?: string;
    data: string;
}
```

isCollapsibleLeft():: Returns _true_ if the first character of _data_ is a space character.
isCollapsibleRight():: Returns _true_ if the last character of _data_ is a
space character.
isWhitespace():: Returns _true_ if _data_ has length 1 and the first character of _data_ is a space
character.
isEmpty():: Returns _true_ if _data_ has length 0.
trimLeft():: Replace _data_ with the substring starting at index 1.
trimRight():: Replace _data_ with the substring ending at the penultimate index.

[[tempty]]
=== TEmpty

Constrains:: none.
Rendering::
Empty nodes are never rendered.
Notes::
Some empty nodes are generated during render tree transforming to indicate that
a specific node should not be rendered. Examples of tags which translate to empty nodes:
. script
. link

```ts
interface TEmpty extends TBlock {
  type: 'empty';
  tagName?: string;
}
```

== Translate Algorithms

=== Definitions

[[text-phrasing-el]] text phrasing element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/text-level-semantics.html[HTML Standard, Ch. 4.5,
Text-level semantics] or
https://html.spec.whatwg.org/multipage/edits.html[HTML Standard, Ch. 4.7,
Edits].
[[embedded-el]] embedded element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/embedded-content.html[HTML Standard, Ch. 4.8,
Embedded content].
[[sectioning-el]] sectioning element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/sections.html[HTML Standard, Ch. 4.3,
Sections].
[[tabular-el]] tabular element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/tables.html[HTML Standard, Ch. 4.9,
Tabular data].
[[grouping-el]] grouping element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/grouping-content.html[HTML Standard, Ch. 4.4,
Grouping content].
[[interactive-el]] interactive element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/interactive-elements.html[HTML Standard, Ch. 4.12,
Interactive elements] or https://html.spec.whatwg.org/multipage/forms.html[HTML Standard, Ch. 4.10,
Forms].
[[untranslatable-el]] untranslatable element:: Any of the elements presented in
https://html.spec.whatwg.org/multipage/scripting.html[HTML Standard,
Ch. 4.11, Scripting] or https://html.spec.whatwg.org/multipage/semantics.html[HTML Standard,
Ch. 4.2, Document metadata].

[[translate-tree]]
=== Translate Tree

. *Let* _body_ be the body of the document tree
. *Return* the result of applying <<translate-node>> to _body_

[[translate-node]]
=== Translate Node

[IMPORTANT]
Interactive content is not supported and will be translated to <<tempty>> nodes.

Given _node_ a document node:

. *If* _node_ is a Text node
.. *Let* _tnode_ be a <<ttext>>
.. *Let* _tnode.data_ be _node.data_
.. *Return* _tnode_
. *Else if* _node_ is a <<text-phrasing-el>>
.. *If* _node.children_ has length 0
... *Let* _tnode_ be a <<ttext>>
... *Set* _tnode.data_ to the empty string
... *Set* _tnode.tagName to _node.tagName_
... *Set* _tnode.attributes to _node.attributes_
... *Return* _tnode_
.. *Else if* _node.children_ has length 1 and its child is a text node
... *Let* _tnode_ be a <<ttext>>
... *Set* _tnode.data_ be _node.children[0].data_
... *Set* _tnode.tagName to _node.tagName_
... *Set* _tnode.attributes to _node.attributes_
... *Return* _tnode_
.. *Else*
... *Let* _tnode_ be a <<tphrasing>>
... *Set* _tnode.tagName to _node.tagName_
... *Set* _tnode.attributes to _node.attributes_
... *Set* _tnode.children_ to the mapping of each of _node.children_ to the
    result of <<translate-node>>
.. *End if*
. *Else if* _node_ is an <<embedded-el>>, <<sectioning-el>>, <<grouping-el>> or <<tabular-el>>, 
... *Let* _tnode_ be a <<tblock>>
... *Set* _tnode.tagName to _node.tagName_
... *Set* _tnode.attributes to _node.attributes_
... *Set* _tnode.children_ to the mapping of each of _node.children_ to <<translate-node>>
. *Else* +
_(node is an <<interactive-el>> or <<untranslatable-el>>. Notice that its
children won't be copied)_
.. *Let* _tnode_ be a <<tempty>>
.. *Set* _tnode.tagName to _node.tagName_
.. *Set* _tnode.attributes to _node.attributes_
.. *Return* _tnode_
. *End if*

== Hoist Algorithms

[[hoist-tree]]
=== Hoist Tree

. *Let* _troot_ be the root of the transient render tree
. *Return* the result of applying <<hoist-node>> to _troot_

[[hoist-node]]
=== Hoist Node

[NOTE]
<<tempty>> nodes are ignored in the bellow algorithm.

Given _tnode_ a <<tnode>>:

. *Set* _tnode.children_ to the mapping of <<hoist-node>>
. *If* _tnode_ is a <<tphrasing>> node
.. *For each* _cnode_ from _tnode.children_
... *If* _cnode_ is a <<tblock>> node
.... *Let* _newnode_ be a <<tblock>> node
.... *Copy* every attribute of _tnode_ to _newnode_
.... *Return* the result of applying <<group-text>> to _(newnode)_
... *End if*
.. *Done*
. *Else if* _tnode_ is a <<tblock>> node
.. *If* _tnode_ has children
... *Let* _wrapper_ be a <<tphrasing>>
... *Return* the result of applying <<group-text>> to _(tnode)_
.. *End if*
. *End if*
. *Return* _tnode_

[[group-text]]
=== Group Text

[NOTE]
<<tempty>> nodes are ignored in the bellow algorithm.

Given _tnode_ a <<tblock>> and 

. *Let* _wrappernode_ be a <<tphrasing>> anonymous node which parent styles are those of _tnode_.
. *Let* _newchildren_ be an empty list of <<tnode,TNodes>>.
. _(a marker copy is a new instance of the same class, in which only the attribute 'href' is
copied if present)_
. *Let* _wrapper_ be a marker copy of _wrappernode_
. *For each* _cnode_ of _tnode.children_
.. *Let* _newchild_ be the result of applying <<hoist-node>> to _cnode_
.. *If* _newchild_ is a <<ttext>> *or* _newchild_ is a <<tphrasing>>
... *Push* _newchild_ to _wrapper.children_
.. *Else*
... *Push* _wrapper_ to _newchildren_
... *Let* _wrapper_ be a copy of _wrappernode_
... *If* _wrappernode_ is a <<tphrasing>> node
.... *Let* _nextchild_ be a <<tblock>> node
.... *Copy* _newchild_ into _nextchild_
.... *Push* _nextchild_ to _newchildren_
... *Else*
.... *Push* _newchild_ to _newchildren_
... *End if*
.. *End if*
. *Done*
. *If* _wrapper.children_ is not empty
... *Push* _wrapper_ to _newchildren_
. *End if*
. *Set* _tnode.children_ to _newchildren_
. *Return* _tnode_

== Collapse Algorithms

=== Definitions

[[inter-element-whitespace]] inter-element whitespace::
ASCII whitespace is always allowed between elements. User agents represent these characters between elements in the source markup as Text nodes in the DOM. Empty Text nodes and Text nodes consisting of just sequences of those characters are considered inter-element whitespace.
Source:
https://html.spec.whatwg.org/multipage//dom.html#inter-element-whitespace[HTML
living standard, Ch. 3.2, Elements].
[[collapsible]] collapsible:: A collapsible whitespace (tabular, space...) is a
character which, in certain contexts depending on the
https://www.w3.org/TR/2020/WD-css-text-3-20200429/#white-space-property[white-space
CSS property], should be removed before paint when preceded by another
whitespace.
[[segment-break]] segment break:: For CSS processing, each document
language–defined segment break and each line feed (U+000A) in the text is
treated as a segment break. See
https://www.w3.org/TR/2020/WD-css-text-3-20200429/#segment-break[CSS Text
Module Level 3, Ch. 4]
[[space-discarding-set]] space discarding character set:: Any character from
the Space-Discarding Unicode Characters list. See https://www.w3.org/TR/css-text-3/#space-discard-set[CSS Text
Module Level 3, Appendix F]

[[collapse-tree]]
=== Collapse Tree

Given _root_ the root node of the transient render tree:

. *Let* _croot_ be the result of applying <<collapse-node>> to _root_
. *If* _croot.isCollapsibleLeft()_:
.. *Invoke* _root.trimLeft()_
. *End if*
. *If* _croot.isCollapsibleRight()_:
.. *Invoke* _root.trimRight()_
. *End if*
. *Return* _croot_

[[collapse-node]]
=== Collapse Node

Given _node_ a transient node:

. *If* _node_ is a <<ttext>> node
.. *Return* the result of applying <<collapse-text>> to _node_
. *Else if* _node_ is a <<tphrasing>> node
.. *Return* the result of applying <<collapse-phrasing>> to _node_
. *Else if* _node_ is a <<tblock>> node
.. *Return* the result of applying applying <<collapse-block>> to _node_
. *End if*
. *Return* _node_

[[collapse-block]]
=== Collapse Block

Given _node_ a <<tblock>> node:

. *Let* _newChildren_ be an empty <<tnode>> array.
. *For each* index _i_ of _node.children_, do:
.. *Let* _child_ be the result of applying <<collapse-node>> to _node.children[i]_
.. *If* *not* _child.isWhitespace()_ +
   _(child is not an <<inter-element-whitespace>>)_
... *If* _child.isCollapsibleLeft()_
.... *Do* _child.trimLeft()_
... *Else if* _child.isCollapsibleRight()_
.... *Do* _child.trimRight()_
... *End if*
... *If not* _child.isEmpty()_
.... *Push* _child_ in _newChildren_
... *End if*
.. *End if*
. *Done*
. *Set* _child.children_ to _newChildren_
. *Return* _node_

[[collapse-phrasing]]
=== Collapse Phrasing

Given _node_ a <<tphrasing>> node:

[arabic]
. *Let* _collapsedChildren_ be an empty list of <<tnode>>
. *Let* _trimmedChildren_ be an empty list of <<tnode>>
. *For each* index _i_ of _node.children_, do:
.. *Let* _child_ be the result of applying <<collapse-node>> to _node.children[i]_
.. *If* _child_ is a <<ttext>>
... *Push* _child_ to _collapsedChildren_
.. *Else if* _child_ is a <<tphrasing>>
... *Push* _child_ to _collapsedChildren_
. *Done*
. *For each* pair index (_i_, _k_) of consecutive items in _collapsedChildren_, do:
.. *If* _child[i].isCollapsibleRight()_ and _child[k].isCollapsibleLeft()_
... *Do* _child[i].trimRight()_
... *If* *not* _child[i].isEmpty()_
.... *Push* _child[i]_ to _trimmedChildren_
... *End if*
.. *Else*
... *Push* _child[i]_ to _trimmedChildren_
.. *End if*
. *Done*
. *Push* the last element of _node.children_ to _trimmedChildren_
. *Set* _node.children_ to _trimmedChildren_
. *Return* _node_

[[collapse-text]]
=== Collapse Text

Given _node_ a <<ttext>> node:

[arabic]
. *Let* _collapsedData_ be _node.data_
. *Do*: remove sequences of <<collapsible>>, spaces and
tabs immediately preceding or
following a <<segment-break>> *on* _collapsedData_
. *Do*: remove any <<collapsible>> <<segment-break>> immediately following another
<<collapsible>> <<segment-break>> *on* _collapsedData_
. *For each* remaining <<segment-break>> in _collapsedData_:
.. *If* the character immediately before or immediately after the <<segment-break>>
is the zero-width space character, remove the <<segment-break>>
.. *Else if* both the character before and after the <<segment-break>> belong to
the <<space-discarding-set>>, remove the <<segment-break>>
.. *Else* replace the <<segment-break>> with a space
. *Done*
. *Do*: replace any <<collapsible>> tab with a space *on* _collapsedData_
. *Do*: delete any <<collapsible>> space following another <<collapsible>> space *on* _collapsedData_
. *Let* _node.data_ be _collapsedData_
. *Return* _node_

== Example

In the below example, the transient render tree state will be laid out in xml.
The laid structure strictly represents data structure presented in
<<transient-render-tree>>, however the `attributes` and default fields will be omitted.

=== Source

[source,html]
----
<a href="https://domain.com">
This is
<span>phrasing content</span>
<img src="https://domain.com/logo.jpg" />
    and this is <strong>too</strong>.
</a>
----

=== Translating

The document tree is walked over to generate an initial transient render tree:

.After Translating
[source,xml]
----
<TPhrasing href="https://domain.com">
  <TText>\nThis is\n<TText/>
  <TText tagName="span">phrasing content</TText>
  <TText>\n</TText>
  <TBlock tagName="img"/>
  <TText>\n    and this is </TText>
  <TText tagName="strong">too</TText>
  <TText>.\n</TText>
</TPhrasing>
----

See <<translate-tree>> algorithm.

=== Hoisting

Hoisting is applied to enforce transient node constraints:

.After Hoisting
[source,xml]
----
<TBlock href="https://domain.com">
  <TPhrasing>
    <TText>\nThis is\n<TText/>
    <TText tagName="span">phrasing content</TText>
    <TText>\n</TText>
  <TPhrasing>
  <TBlock tagName="img"/>
  <TPhrasing href="https://domain.com">
    <TText>\n    and this is </TText>
    <TText tagName="strong">too</TText>
    <TText>.\n</TText>
  </TPhrasing>
</TBlock>
----

See <<hoist-tree>> algorithm.

=== Collapsing

Collapsing is applied to enforce limited whitespace collapsing requirements:

.After Collapsing
[source,xml]
----
<TBlock>
  <TPhrasing href="https://domain.com">
    <TText>This is <TText/>
    <TText tagName="span">phrasing content</TText>
  <TPhrasing>
  <TBlock href="https://domain.com" tagName="img"/>
  <TPhrasing href="https://domain.com">
    <TText>and this is </TText>
    <TText tagName="strong">too</TText>
    <TText>.</TText>
  </TPhrasing>
</TBlock>
----

See <<collapse-tree>> algorithm.