(Old) hypertext in (new) hypertext

The Talmud is an early form of hypertext, with multiple texts and links between them. A daf (page) of Talmud has a unique layout. The Mishna and Gemara are in the center, with further discussion and commentary around it.

A daf of Talmud. Source: https://kottke.org/18/09/putting-the-talmud-online

A couple weeks ago, @emilyaviva@tech.lgbt posed the following challenge on Mastodon:

All right, tech folks, listen up. I want to make a web page that has a central div of text where two divs flow around it from top, sides, and bottom, just like @esther_alter@mastodon.social’s brilliant Talmudifier, but in CSS only. You’d think this would be possible, but after several hours of screwing around with floats and flexbox, I’m not sure it is. Any brilliant ideas?

Challenge accepted.

Challenge solved — almost. (It required a tiny bit of JavaScript.)

See the Pen Talmud daf layout by Noah (@noleli) on CodePen.

Let’s dig into how.

Semantics

First things first: semantics. The way I thought about it, the whole daf is an <article>. The Mishna/Gemara are the main body of the article (but gets wrapped in a <div> with a class to be unambiguous). Then, the commentary around it is an <aside>. Within the commentary, there are two distinct sections: the Rashi (set here in a sans serif font, following Talmudifier’s convention) is first. (Remember that while this text is faux Latin, Hebrew and Aramaic are written from right to left.) Then, the tosafot wrap around it. As they’re both part of the <aside>, the Rashi is also in a <div>.

So, the whole markup looks like this:

<article class="daf">
<div class="mishna"><p>The Mishna/Gemara text</p></div>
<aside>
<div class="rashi"><p>The Rashi text</p></div>
<p>The tosafot text</p>
</aside>
</article>

Since the original challenge referenced Talmudifier, I pasted in the lorem ipsum from there.

Layout

Now, let’s put things where they need to be. I used a simple grid layout with 3 rows and 3 columns. That might be a little counterintuitive because it looks at first like the commentary is in two columns, but it’s really not. We’ll handle that in a moment.

The center/Mishna text is in the center of the 3 × 3 grid, so the height of first row of the grid layout determines how high or low on the page it starts. Since the center text’s width is set by the width of the .mishna element and its height is intrinsic based on the length of the text, the grid looks like this:

.daf {
width: 80ch;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: 6rem auto 1fr;
}

Then the Mishna is placed in the center grid area, and the aside (commentary and Rashi) are put in the entire grid area (plus a bit of styling).

.mishna {
grid-area: 2 / 2 / 3 / 3;
font-weight: bold;
width: 25ch;
}

aside {
grid-area: 1 / 1 / -1 / -1;
}

.rashi {
font-family: sans-serif;
}

That leaves us with this delightful mess, in which the Rashi and tosafot are regular paragraphs and the Mishna is thrown on top:

Two paragraphs of text with a third paragraph stuck on top, narrow and in the center

Now let’s ignore the Mishna and shift gears to the <aside>. While it initially looks like a two-column layout at the top, if you look down below you can see that the tosafot wraps around the Rashi. So, this is really a straightforward floating layout in which the Rashi is floated to the right, with the tosafot wrapping around it. The main thing here is that the two “columns” are of equal width, and there’s a gap between the columns.

We’ll need to use the column gap value a bunch of times, so let’s add a custom property to the .daf:

.daf {
/* ... */
--column-gap: 1.125rem;
}

The Rashi needs to be approximately 50% of the width of its parent, but not exactly. To keep the two (apparent) columns the same width, the column gap needs to be split between the width of the tosafot and the width of the Rashi. Therefore, the width of the Rashi is 50% the width of its parent minus half of the column gap. The gap itself is applied as a left margin on the Rashi[1].

.rashi {
float: right;
font-family: sans-serif;
width: calc(50% - var(--column-gap)/2);
margin-left: var(--column-gap);
}

Hiding the Mishna/Gemara, we get this:

Rashi floated right in tosafot

And with the Mishna added back in,

layout without wrapping around mishna

Perfect! Except for that one big problem….

Wrapping around the Mishna

There’s no good way to wrap text around other things using CSS if you need anything more complicated than a simple float. This is a known shortcoming, and several attempts at solving it (and related problems) have been proposed over the years. Regions, Shapes Level 2, and — most promisingly here — Exclusions. Alas, we need to hack something now.

Floats are generally positioned inline with some text. That means that in order to have a float not be at the top of the text, but at a specific point along the block axis, you need to know exactly where to insert that float inline into your text’s markup. That’s not going to work here. You also can’t float something to the middle, or have multiple elements wrap around a single float.

So here’s what we’re going to do: we’re going to use JavaScript to insert two empty floating divs, size them appropriately, and “place” them by messing with their shape-outside. Why JavaScript? Two reasons, one required and one aesthetic: first, we need to measure the size of the Mishna container; and second, it lets us keep the input markup semantically clean.

Each of the two floated divs takes care of half of the mishna text. Let’s deal with the righthand side first. Here, the Rashi is our main block of text, so let’s add a div to the top of the Rashi element, give it a fixed size, and float it left.

const rashi = aside.querySelector(".rashi");
const rashiFloater = document.createElement("div");

rashiFloater.style = `
float: left;
width: 110px;
height: 130px;
`
;

rashi.prepend(rashiFloater);

At this arbitrary size, we get this (giving it a background color and again hiding the Mishna):

An empty div floated left in the Rashi

Now we need to set it to the right size. The width needs to be the distance from the center of the column break to the edge of the Mishna. In other words, it’s half the width of the Mishna minus half the width of the column gap. The height needs to take our floater from where it starts at the top of the daf to the bottom of the Mishna.

// get the mishna measurements
const mishnaRect = document.querySelector(".mishna").getBoundingClientRect();
// but we'll need to shift it into local rather than viewport coordinates
const { top: dafTop } = document.querySelector(".daf").getBoundingClientRect();

rashiFloater.style = `
float: left;
width: calc((
${mishnaRect.width}px - var(--column-gap))/2);
height:
${mishnaRect.height + mishnaRect.y - dafTop}px;
margin-right: var(--column-gap);
`
;

Add in a right margin (column gap), and now you can really see where this is going.

An empty div floated left in the Rashi that's sized to start at the top and perfectly clear the Mishna

So close! But we still need to not have the float punching out the Rashi all the way to the top of the daf. For that, we’ll use shape-outside. shape-outside lets you give the browser an effective shape (for wrapping purposes) for a floating element. Using the inset() shape function, we can inset the top starting point of the wrapping to the distance between the top of the daf and the top of the Mishna.

rashiFloater.style = `
float: left;
width: calc((
${mishnaRect.width}px - var(--column-gap))/2);
height:
${mishnaRect.height + mishnaRect.y - dafTop}px;
shape-outside: inset(
${mishnaRect.y - dafTop}px 0 0 0);
margin-right: var(--column-gap);
`
;

And that’s it for the right side! Remember that shape-outside doesn’t actually move or clip the floated element, as you can see with the background color, but the wrapping works exactly the way we need it to.

An empty div floated left in the Rashi where the Rashi wraps around the Mishna, both above and below, but the color of the div goes all the way to the top

The left side is basically the same. The main differences are where to put the floater in the DOM, and which direction to float it. The <aside> already has something floated right (the Rashi itself), so the empty floater needs to go after the Rashi. Remember from the bad old days before flex layouts that if you have multiple items floated in the same direction, they “pile up” so that the first floated item goes the farthest, and the next floated item runs into that. By putting the floater right after the Rashi, the Rashi is farthest right, and the floater is to the left of that.

const aside = document.querySelector("aside");

const tosafotFloater = document.createElement("div");
tosafotFloater.style = `
float: right;
width: calc((
${mishnaRect.width}px - var(--column-gap))/2);
height:
${mishnaRect.height + mishnaRect.y - dafTop}px;
shape-outside: inset(
${mishnaRect.y - dafTop}px 0 0 0);
margin-left: var(--column-gap);
`
;

rashi.after(tosafotFloater);

Both left and right floats with the Rashi and Tosafot wrapping around them with background colors

That’s the whole thing! Get rid of the background colors from the floats and here’s what we’ve got. Success!

A completed page of lorem ipsum layed out like a daf of talmud


  1. You’d think this would be a perfect place to use logical properties, what with Hebrew and Aramaic being right-to-left languages. However, the reference image from Talmudifier has English text laid out in the same orientation as the original RTL pages, so using absolute directions seemed to be the way to go. ↩︎