Skip to main content

Pushing Inline Styles in WebKit

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":

<div id="test" contenteditable>hello world</div>
<script type="text/javascript">
var test = document.getElementById('test');
getSelection().selectAllChildren(test);
document.execCommand('bold', false, null);
</script>

In WebKit and many other implementations, the innerHTML of the test element after the script ran will look like this:

<b>hello world</b>

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 b element. 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 (WebKit added 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 recent WebKit changesets r65208 and r66324 extended this idea of pushing down styles to all inline styles. Let's take a closer look at the push-down process now.

The style push down is implemented by pushDownInlineStyleAroundNode in ApplyStyleCommand. 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 such as b, u, s, or any other element with the style attribute. 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 u and b (also em and strong to be compatible with Internet Explorer) or becomes unnecessary (span or 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 u whose children are "hello" and the b element. 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 from 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 pushDownInlineStyleAroundNode works. I hope you enjoyed reading it.