Three Quick Ways to Avoid Widows

A few months ago I threw together a quick redesign of the Learning jQuery site. It's nothing fancy, mind you, but I was itching to retire the thin veil covering the tired old WordPress Kubrick theme, so something had to be done.

Almost immediately upon changing the font-family and font-size of the blog post titles, I noticed a few unsightly widows (just to clarify, we're talking about typographical widows. My mother already suspects me of avoiding her; I don't want to add to her anxiety. ;) ).

Here is an example of one such widow:

See how the last word, "plugin," appears on its own line? According to a couple designerly friends of mine, that's a no-no. So, I considered for half a minute how to get that title to look more like this:

The lowly yet lovely non-breaking space ( ) would do the trick, but how to replace it for the regular, breaking space? I certainly wasn't about to manually edit all of the entries' titles. Not only would it take too long, but it would also pollute the markup with something that really shouldn't be there. No, what I needed was a little JavaScript.

Selecting the Titles

On this site, entry titles are wrapped in <h2><a href="foo"></a></h2>, which can be selected in jQuery with $('h2 a'). Easy. Now, because I want to manipulate the text of each title independently, I'll need to use the .each() method, which is basically a chainable for loop. Inside the .each() is where I substitute the last breaking space with the non-breaking variety. Here are three ways I came up with to achieve this.

Array

The first approach was to convert the title string into an array of words and then stitch the array items back together, dealing with the last one specially.

[js]$(document).ready(function() { var h2Text = ''; $('h2 a').each(function() { var h2Array = $(this).text().split(' '), h2Last = h2Array.pop(); h2Text = h2Array.join(' ') + ' ' + h2Last; $(this).html(h2Text); }); });[/js]

A couple things to note about line 5 above: (a) the variable is actually being declared (with a "var") because it's separated from the previous variable declaration by a comma rather than a statement-ending semicolon. (b) The JavaScript .pop() array method "pops" the last item off the array and returns it; so it's no longer part of h2Array, but its value is stored in the h2Last variable. This is especially handy for us, because we don't want the last word to appear twice.

Line 6 joins the remaining array items with a space between them and then appends the non-breaking space and the popped last item. Line 7 dumps that concatenated string back into the title, inside <h2><a></a></h2>.

String

The next approach involved working solely with strings, using the slice and lastIndexOf methods to split the the string into two pieces — one leading up to the last space, and one immediately following the last space.

[js]$(document).ready(function() { var h2all, h2a, h2b; $('h2 a').each(function() { h2all = $(this).text(); h2a = h2all.slice(0, h2all.lastIndexOf(' ')); h2b = ' ' + h2all.slice(h2all.lastIndexOf(' ')+1); $(this).html(h2a + h2b); }); });[/js]

As line 7 demonstrates, the two sliced strings are stitched back together to keep the last two words on the same line.

Regular Expression

The final technique is the one I ended up sticking with, partly because it's the tersest and partly because I have a fondness for regular expressions:

[js]$(document).ready(function() { var h2Text = ''; $('h2 a').each(function() { h2Text = $(this).text().replace(/ (\w+)$/,' $1'); $(this).html(h2Text); }); });[/js]

The distinguishing feature here is line 4, which uses the replace regular-expression method. This method takes two arguments, a pattern to match against and a replacement value. For the pattern, which appears between the two slashes, we first match a space and then match one or more "word characters" (letters, numerals, or underscores). The parentheses capture all but that initials space, and the "$" at the end ensures that the match appears at the end of the string. The replacement argument starts with the non-breaking space and follows with $1, which refers to the first (and in our case, only) parenthetical "capture group." (Please forgive me if I've provided too much detail about the regular expression. I'm never quite sure how much of this stuff is worth mentioning, but since this entry is targeting beginners, I suppose it's better to err on the side of too much.)

Update

A few people pointed out in the comments that my regular expression could be improved, and I agree. In particular, as noted by Art Lawry, the \w can be changed to \S (that's an uppercase S) to match any non-whitespace character. That way it'll match characters such as ö and ç as well.

By the way, all three of these code examples can be reduced in length quite a bit. For example, the regular expression example can be pared down from 7 to 5 lines if we don't bother with the h2Text variable and instead do something like this:

$(this).html($(this).text().replace(/ (\w+)$/,' $1'));

However, the code is usually easier to read and maintain (for me, at least) if the value is first stored in a variable.

Any suggestions for improvement here? Any other approaches that you would recommend instead? Leave a comment.