Ruby is marked up as such: “<ruby><rb>Left Below</rb><rp>(</rp><rt>Left Above</rt><rp>)</rp><rb>Right Below</rb><rp>(</rp><rt>Right Above</rt><rp>)</rp></ruby>”

That ends up looking like this:

Let’s break that down. We have two pieces which are almost identical to: “<rb>Left Below</rb><rp>(</rp><rt>Left Above</rt><rp>)</rp>”. Each of these is called a “ruby run”. A ruby tag’s contents is a sequence of ruby runs. These are the ruby runs:

<rp> is (usually) display: none; so this is the same as “<rb>Left Below</rb><rt>Left Above</rt>”. It is used for browsers who don’t recognize the ruby tag (and will instead simply show the contents of unrecognized tags inline). This is so that the ruby text has some sort of context around it that shows that it is not part of the normal stream of text. The tag stands for “ruby parenthesis”.

The <rb> tag is automatically inserted around whatever content comes before the <rt> tag, so this is the same as simply “Left Below<rt>Left Above</rt>”. “rb” stands for “ruby base” and “rt” stands for “ruby text”.

There are two different renderer classes which might represent a <ruby> tag, RenderRubyAsInline and RenderRubyAsBlock. They are used for display: inline and display: block, respectively. A ruby run is represented by a RenderRubyRun. Ruby text is represented by a RenderRubyText, and a ruby base is represented by a RenderRubyBase. In the following picture, blue is ruby text and green is ruby base:

RenderRubyRun, RenderRubyText, and RenderRubyBase are all subclasses of RenderBlockFlow. That way, the base and text block layout is reused as any other progression of <div>s. This means that a RenderRubyText is the first child of a RenderRubyRun (if it is present), and a RenderRubyBase is the last child of a RenderRubyRun (if it is present). This also means that a RenderRubyRun has at most two children. In addition, there can be a line break between two ruby runs, but not inside a single ruby run. This also means that if the contents of a ruby text are too wide for a line, the ruby text will line wrap inside the element. The same goes for the ruby base. In the following diagram, a red arrow is a RenderObject() child relationship and a green arrow is an InlineFlowBox child relationship (however, the relationship between RenderRubyText and RootInlineBox is a has-a relationship, by way of the RenderLineBoxList in RenderBlockFlow) .

The width of a ruby run is simply the maximum width of its two children: the ruby text and the ruby base (which is true of all inline-block elements). However, it’s important to realize that if the ruby text is significantly longer than the ruby base, there is some amount of overhang where the ruby text is allowed to encroach upon the space of the elements which surround it. However, this amount of overhang is capped at a multiple of the font size. This overhang is implemented by setting negative margins on the RenderRubyRun.

In this image, the red box represents the bounds of the ruby run. Note how it intersects the bounds of the text on either side of the ruby!

There are two conceptual pieces to the layout of ruby runs: how its containing line treats the run as a whole, and how the ruby run treats its children. Because layout is a preorder traversal, layout of the ruby text and ruby base occurs before the layout of the line containing the ruby run, which means that the outer line layout has access to information regarding the size of the ruby base and ruby text.

RenderRubyRun overrides RenderElement::layout() so that it can perform some post processing on the ruby text and ruby base. The first thing this override does, however, is to call RenderBlockFlow’s layout() function, which goes through the normal block layout process. One of the steps of laying out a RenderBlockFlow is to lay out its children. However; some RenderBlockFlows (such as RenderRubyRun) have a “special excluded child” which gets laid out before the rest of its children, and then skipped in the normal child layout loop. In this case, the special excluded child is the ruby text, so it always gets laid out first. Because it gets positioned later inside RenderRubyRun::layout anyway, there’s no reason for it to be a part of normal block layout. It also shouldn’t contribute to the line height the way a regular inline-block element would.

Laying out a RenderBlockFlow allows subclasses to override the text alignment for the line, and RenderRubyText does so. In general, it is overridden to be “justify” in RenderRubyText::textAlignmentForLine(). Another thing that subclasses can override is, if the line’s text-align is justify, the starting position and width of the available space to lay the line out in. This is where RenderRubyText calculates its own inset amount, relative to its parent (the RenderRubyRun). This is so that it doesn’t stretch from edge to edge if the RenderRubyRun ends up being slightly wider than the space allocated to it in its containing line (because of negative margin, see above). It does this by looking at its own maxPreferredLogicalWidth(), which is the natural width of the element, and comparing that to the line’s available width that is passed in from RenderBlockFlow::computeInlineDirectionPositionsForSegment(). RenderRubyBase does both of these things similarly.

Then we pop back up to RenderRubyRun::layout() just after it calls RenderBlockFlow::layout(), where we place the ruby text to make it match up with the ruby base.

Once we’re done layout out the ruby run itself, the line containing the ruby run lays out. RenderRubyRun acts as a replaced inline element (see its constructor), which means that the constructBidiRunsForSegment() call inside layoutRunsAndFloatsInRange() will return a bidi run whose renderer() is the RenderRubyRun. Then, when computeInlineDirectionPositionsForSegment() iterates over the bidi runs, it calls RenderRubyRun::getOverhang(), and applies margins equal to the starting and ending overhang. We can calculate how much overhang there should be by looking at the size of the RootInlineBoxes inside the RenderRubyBase, and comparing that to the size of the RenderRubyRun. The RootInlineBoxes will hug the size of the actual text inside the base, while the RenderRubyRun’s logicalWidth() will grow to encompass the ruby text as well. Note that we look at the ruby base (not the ruby text) when performing this calculation. In the following diagram of the original example's ruby's containing line, the green arrows are the InlineFlowBox child relationships, and the purple arrows are InlineBox::renderer() relationships.

If you have a ruby run on a justified line, our search for expansion opportunities will descend into ruby bases inside RenderBlockFlow::computeInlineDirectionPositionsForSegment(). Then, when it comes time to grow the InlineBoxes by expanding the expansion opportunities, we also descend into ruby runs in RenderBlockFlow::updateRubyForJustifiedText. In that function, we calculate the size that the expanded base should have, then tell the RenderRubyBase to set its “initial offset” to a value that would center its contents. We then override the width of the RenderRubyRun and re-lay it out. This time, however, RenderRubyBase::adjustInlineDirectionLineBounds remembers the initial offset that we set earlier, and adjust the bounds of its inner line accordingly. Then, because ruby bases are justified (see above), the ruby base will expand to take up the entire space of the line so the expansions will be re-calculated and will happen to end up being the same as the expansions for the line that contains the ruby.

Last modified 2 years ago Last modified on Dec 17, 2014 12:26:05 PM

Attachments (8)

Download all attachments as: .zip