Inlining a frameset

2010-11-29

Tradionally a frameset demands 1+f+x requests, where f is the number of frames in the frameset and x is the total number of additional resources inside the frames or frameset. (The frameset document itself also takes one request, obviously.)

For the js1k demo pages I wanted to reduce that to 1+x. In fact, since the js1k demo's did not allow external requests and anything else was inlined, the whole demo page was reduced to one request (plus whatever google analytics did, but that wasn't my concern). An important demand was that all the legacy demo's remained to work though. And since these demo's were relying on the most extreme edge case conditions, there wasn't a whole lot of room for errors. For more information on optimizing the js1k site see blog post.

First things first. There are currently two methods of inlining a page into the src attribute of a <frame>. One involves data uri's. This one can actually be pretty straightforward. You take the html of the frameset and forge a base64 data uri for it. This will work in all non-IE browsers. There's no additional encoding required since base64 takes care of that.

And then there's IE. Whilst for the first demo IE was not in the picture (IE9 wasn't really on the radar) I did want to be able to load the demo's in IE9 beta. IE8 has a strict policy for data uri's, basically restricting them to images and objects. They are explicitly not supported as the src attribute. So that's that, data uri's are out. I knew people would wonder whether it would work in IE9 and I didn't want to make them jump through elaborate hoops just to test that.

Then somebody told me about another method. I didn't even know it existed. And I still don't know why it works so well, but it does. It's embedding the html using src="javascript:'html...'". And that just works. It will take your html and form a proper (with doctype even standards!) html document in the frame! However, there is a catch. A few actually.

First of all, it seems the origin of these frames are unique. The frames may not talk to each other or their parent.

Secondly you need to apply a few weird combinations of encoding. The single and double quotes need to be hidden, and they can't use the same escape technique. Double quotes need to be escaped using &#34; to escape the html parser. Single quotes need to be escaped with a backslash (\') because they are part of the javascript string. Turning them into a html entity will not work. Furthermore returns need to be escaped with \n, as a js string may not contain them (fine, ok, they can). Next, because you're passing on a url, you need to escape url sensitive chars. So % becomes %25, ? becomes %23 and # becomes %35. Finally, to get all the demo's to work, white space had to be escaped as well as they can be considered significant by js. So all tabs become \t.

This brings us to the following php escape script:

Code: (php)
function convert($str) {
$str = str_replace("\\", "\\\\", $str); // escape all backslashes (again)
$str = str_replace("\n", "\\n", $str); // replace all line breaks
$str = str_replace("\t", "\\t", $str); // tabs are significant whitespace...
$str = str_replace("'", "\\'", $str); // backslash escape single quotes. they are not translated from html entities...

$str = str_replace('%', '%25', $str); // prevent accidental uri encoding (%25 => % when uri decoded)
$str = str_replace('?', '%3F', $str); // question marks are query strings
$str = str_replace('#', '%23', $str); // hashes in urls are bad, mkay (_after_ the percentage encoding)
$str = str_replace('"', '"', $str); // html escape double quotes (_after_ the hash encoding)

return "javascript:'".$str."'";
}

(Yes, I know you can easily combine the str_replace into a single call.)

The result of that function becomes the value for the src, quotes with double quotes. That method seems to work flawlessly in all non-IE browsers. In IE the js side of things seems to fail incoherently. It will execute but then somehow crash.

Code: (html+php)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
<head>
<title>frameset demo</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<frameset id="myNastyFrameSet" rows="30px,*">
<frame marginwidth="0" marginheight="0" frameborder="0" noresize="no" src="<?=convert($htmlA)?>" />
<frame marginwidth="0" marginheight="0" frameborder="0" noresize="no" src="<?=convert($htmlB)?>" />
</frameset>
</html>

Note that it's quite difficult to debug js in these things. You can't really get to see the source of the frame (because there isn't one). However, you can check out the DOM inspector and see the script tags you've included. That way you can still get to the decoded script literals used. Good luck though :)

Hope it helps you!