Today, I’d like to talk a little about how we push down inline styles to a range of nodes.
In rich text editing, we often want to apply certain inline styles such as bolding on a selection of nodes.
More specifically, bold, italic, underline, and other
execCommand all apply inline styles
to the current selection.
For example, the following code bolds “hello world”:
In WebKit and many other implementations, the innerHTML of the
test element after the script ran will look like this:
Ok, this was simple.
We split the text node that contains “hello world” into two text nodes each containing “hello” and “ world”,
and wrapped the second node by the
Now let’s make things a little more complicated.
What if we had
<strong>hello world WebKit</strong> all in bold
and wanted to unbold just “world” to get
<strong>hello </strong>world<strong> WebKit</strong>?
A simple solution using CSS is the following:
<b> hello <span class="Apple-style-span" style="font-weight: normal;">world</span> WebKit </b>
This will certainly unbold “world” but the markup isn’t so clean
class="Apple-style-span" to the span to signify the fact
it’s generated by WebKit for styling purposes).
Also, we’re hopeless if we’re removing underline because underline and line-through cannot be cleared in CSS:
<u> hello <span style="text-decoration: none;">world</span> WebKit </u>
Will be rendered same as
<span style="text-decoration: underline;">hello world WebKit</span>
because CSS’s text-decoration properly
does not provide a way to cancel text decorations propagated from ancestors.
This was the motivation behind pushing down inline styles in WebKit.
By pushing down, we mean to prune ancestors that propagate inline styles that is being negated,
and apply the style back to siblings of ancestors.
For example, in the previous example, WebKit removes
u element around “hello world WebKit”
and adds it back to “hello ” and “ WebKit” to produce a very clean markup:
<u>hello</u> world <u>WebKit</u>
The style push down is implemented by
removeInlineStyle calls this function at the start and the end of the current selection
to push down inline styles that are propagated from ancestors and conflicting with the style being applied.
We have to call it on both ends because either end can be inside a styled element
s, or any other element with the
Once styles are pushed down,
removeInlineStyle will iterate through each node
within the selection to remove appropriate nodes.
pushDownInlineStyleAroundNode first finds the highest ancestor that propagates
the style conflicting with new style we’re applying.
It then extracts (obtains the value of and removes) the conflicting style.
If this ancestor is a presentational element such as
strong to be compatible with Internet Explorer)
or becomes unnecessary (
font without any attributes) after removing attributes,
pushDownInlineStyleAroundNode removes the element altogether.
Once the conflicting style is extracted,
it visits every child of the ancestor from which we extracted the style,
and applies the style back to each one of them to preserve the style outside the selection.
Without this reapplication of conflicting styles at the end, we end up losing styles outside of the current selection. For example, suppose we’re removing the underline from “webkit” (current selection) in:
<u> hello <b> world <s>webkit</s> </b> </u>
The highest ancestor that has a conflicting style at the start of selection is
whose children are “hello” and the
Thus, we remove this presentational element to obtain:
hello <b> world <s>webkit</s> </b>
Then we re-apply underline to children “hello” and the
b element to obtain:
<u>hello</u> <b style="text-decoration: underline;"> world <s>webkit</s> </b>
pushDownInlineStyleAroundNode then repeats above steps on the children
that contained the start or the end of selection.
In other words, we keep extracting the style from the highest node
with a conflicting style and reapplying it to its children until we hit the start or the end of selection.
In the above example, the next step will be to extract the underline
b’s inline style declaration and reapply it to its children to obtain:
<u>hello</u> <b> <u>world</u> <s style="text-decoration: underline;">webkit</s> </b>
At this point, the highest ancestor with a conflicting style is the
s element with
its pushed-down inline style declaration.
We remove this element, and stop.
pushDownInlineStyleAroundNode falls into an infinite loop if we applied the extracted style to “webkit”).
At the end of the day, we get:
<u>hello</u> <b> <u>world</u> <s>webkit</s> </b>
There are many details that I skipped over
but this is a rough overview of how
I hope you enjoyed reading it.