shithub: opossum

Download patch

ref: 5f020655d08a351cb5b74d0ba7f6a998eb5be528
parent: f8c9bd648275f2b20c6b2656dfcc5a1800cde026
author: Philip Silva <philip.silva@protonmail.com>
date: Fri Feb 5 14:44:24 EST 2021

Call document.close at the end

--- a/browser/experimental.go
+++ b/browser/experimental.go
@@ -132,6 +132,10 @@
 		initialized = true
 	}
 
+	if err = d.CloseDoc(); err != nil {
+		return "", fmt.Errorf("close doc: %w", err)
+	}
+
 	resHtm, changed, err := d.TrackChanges()
 	if err != nil {
 		return "", fmt.Errorf("track changes: %w", err)
--- a/domino/domino.go
+++ b/domino/domino.go
@@ -116,6 +116,7 @@
 		window.top = window;
 		window.self = window;
 		addEventListener = function() {};
+		removeEventListener = function() {};
 		window.location.href = 'http://example.com';
 		var ___fq;
 		___fq = function(pre, el) {
@@ -151,8 +152,6 @@
 			userAgent: 'opossum'
 		};
 		HTMLElement = domino.impl.HTMLElement;
-		// Fire DOMContentLoaded to trigger $(document).ready(..)
-		document.close();
 	` + script
 	if !initial {
 		SCRIPT = script
@@ -271,6 +270,12 @@
 	return d.Exec(string(buf), true)
 }
 
+// CloseDoc fires DOMContentLoaded to trigger $(document).ready(..)
+func (d *Domino) CloseDoc() (err error) {
+	_, err = d.Exec("document.close();", false)
+	return
+}
+
 // TriggerClick, and return the result html
 // ...then HTML5 parse it, diff the node tree
 // (probably faster and cleaner than anything else)
@@ -281,7 +286,10 @@
 
 		console.log('query ' + sel);
 
-		if (el._listeners && el._listeners.click) {
+		if (!el) {
+			console.log('el is null/undefined');
+			null;
+		} else if (el._listeners && el._listeners.click) {
 			var fn = el.click.bind(el);
 
 			if (fn) {
--- a/domino/domino_test.go
+++ b/domino/domino_test.go
@@ -109,6 +109,9 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
+	if err = d.CloseDoc(); err != nil {
+		t.Fatalf("%v", err)
+	}
 	res, err := d.Exec("$('h1').attr('style')", false)
 	t.Logf("res=%v", res)
 	if err != nil {
@@ -160,10 +163,6 @@
 	}
 	d := NewDomino(string(buf), nil)
 	d.Start()
-	script := `
-	Object.assign(this, window);
-	`
-	_ = script
 	for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "goversion.js", "godocs.js"} {
 		buf, err := ioutil.ReadFile("godoc/"+fn)
 		if err != nil {
@@ -177,6 +176,37 @@
 	d.Stop()
 }
 
+func TestGoplayground(t *testing.T) {
+	buf, err := ioutil.ReadFile("godoc/golang.html")
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	d := NewDomino(string(buf), nil)
+	d.Start()
+	for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "playground.js", "goversion.js", "godocs.js", "golang.js"} {
+		buf, err := ioutil.ReadFile("godoc/"+fn)
+		if err != nil {
+			t.Fatalf("%v", err)
+		}
+		_, err = d.Exec(string(buf) /*+ ";" + script*/, i == 0)
+		if err != nil {
+			t.Fatalf("%v", err)
+		}
+	}
+	res, err := d.Exec("window.playground", false)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	if !strings.Contains(res, "function playground(opts) {")  {
+		t.Fatalf("%v", res)
+	}
+	if err = d.CloseDoc(); err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	d.Stop()
+}
+
 func TestJqueryUI(t *testing.T) {
 	buf, err := ioutil.ReadFile("jqueryui/tabs.html")
 	if err != nil {
@@ -250,16 +280,10 @@
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
-	//t.Parallel()
 	SCRIPT := string(jQuery) + `
-    ;;;
-    Object.assign(this, window);
-	console.log("Started");
 	var clicked = false;
     $(document).ready(function() {
-    	console.log('READDDYYYYY!!!!!!!');
     	$('h1').click(function() {
-    		console.log('CLICKED!!!!');
     		clicked = true;
     	});
     });
@@ -269,6 +293,9 @@
 	_, err = d.Exec(SCRIPT, true)
 	if err != nil {
 		t.Fatalf(err.Error())
+	}
+	if err = d.CloseDoc(); err != nil {
+		t.Fatalf("%v", err)
 	}
 
 	res, err := d.Exec("$('h1').html()", false)
--- /dev/null
+++ b/domino/godoc/golang.html
@@ -1,0 +1,299 @@
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="utf-8">
+<meta name="description" content="Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="theme-color" content="#00ADD8">
+
+  <title>The Go Programming Language</title>
+
+<link href="https://fonts.googleapis.com/css?family=Work+Sans:600|Roboto:400,700" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css?family=Product+Sans&text=Supported%20by%20Google&display=swap" rel="stylesheet">
+<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
+
+<link rel="search" type="application/opensearchdescription+xml" title="godoc" href="/opensearch.xml" />
+
+<script>window.initFuncs = [];</script>
+
+<script>
+var _gaq = _gaq || [];
+_gaq.push(["_setAccount", "UA-11222381-2"]);
+window.trackPageview = function() {
+  _gaq.push(["_trackPageview", location.pathname+location.hash]);
+};
+window.trackPageview();
+window.trackEvent = function(category, action, opt_label, opt_value, opt_noninteraction) {
+  _gaq.push(["_trackEvent", category, action, opt_label, opt_value, opt_noninteraction]);
+};
+</script>
+
+<script src="/lib/godoc/jquery.js" defer></script>
+
+
+<script src="/lib/godoc/playground.js" defer></script>
+
+<script>var goVersion = "go1.15.8";</script>
+<script src="/lib/godoc/godocs.js" defer></script>
+
+<body class="Site">
+<header class="Header js-header">
+  <div class="Header-banner">
+    Black Lives Matter.
+    <a href="https://support.eji.org/give/153413/#!/donation/checkout"
+       target="_blank"
+       rel="noopener">Support the Equal Justice Initiative.</a>
+  </div>
+  <nav class="Header-nav ">
+    <a href="/"><img class="Header-logo" src="/lib/godoc/images/go-logo-blue.svg" alt="Go"></a>
+    <button class="Header-menuButton js-headerMenuButton" aria-label="Main menu" aria-expanded="false">
+      <div class="Header-menuButtonInner"></div>
+    </button>
+    <ul class="Header-menu">
+      <li class="Header-menuItem"><a href="/doc/">Documents</a></li>
+      <li class="Header-menuItem"><a href="/pkg/">Packages</a></li>
+      <li class="Header-menuItem"><a href="/project/">The Project</a></li>
+      <li class="Header-menuItem"><a href="/help/">Help</a></li>
+      
+        <li class="Header-menuItem"><a href="/blog/">Blog</a></li>
+        
+          <li class="Header-menuItem"><a href="https://play.golang.org/">Play</a></li>
+        
+      
+      <li class="Header-menuItem Header-menuItem--search">
+        <form class="HeaderSearch" role="search" action="/search">
+          <input class="HeaderSearch-input"
+                type="search"
+                name="q"
+                placeholder="Search"
+                aria-label="Search"
+                autocapitalize="off"
+                autocomplete="off"
+                autocorrect="off"
+                spellcheck="false"
+                required>
+          <button class="HeaderSearch-submit">
+            <!-- magnifying glass: --><svg class="HeaderSearch-icon" width="24" height="24" viewBox="0 0 24 24"><title>Search</title><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
+          </button>
+        </form>
+      </li>
+    </ul>
+  </nav>
+</header>
+
+<main id="page" class="Site-content">
+<div class="container">
+
+
+
+
+
+
+
+
+<div id="nav"></div>
+
+
+
+
+<div class="HomeContainer">
+  <section class="HomeSection Hero">
+    <h1 class="Hero-header">
+      Go is an open source programming language that makes it easy to build
+      <strong>simple</strong>, <strong>reliable</strong>, and <strong>efficient</strong> software.
+    </h1>
+    <i class="Hero-gopher"></i>
+    <a href="/dl/" class="Button Button--big HeroDownloadButton">
+      <img class="HeroDownloadButton-image" src="/lib/godoc/images/cloud-download.svg" alt="">
+      Download Go
+    </a>
+    <p class="Hero-description">
+      Binary distributions available for<br>
+      Linux, macOS, Windows, and more.
+    </p>
+  </section>
+
+  <section class="HomeSection Playground">
+    <div class="Playground-headerContainer">
+      <h2 class="HomeSection-header">Try Go</h2>
+      
+        <a class="Playground-popout js-playgroundShareEl">Open in Playground</a>
+      
+    </div>
+    <div class="Playground-inputContainer">
+      <textarea class="Playground-input js-playgroundCodeEl" spellcheck="false" aria-label="Try Go">// You can edit this code!
+// Click here and start typing.
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, 世界")
+}
+</textarea>
+    </div>
+    <div class="Playground-outputContainer js-playgroundOutputEl">
+      <pre class="Playground-output"><noscript>Hello, 世界</noscript></pre>
+    </div>
+    <div class="Playground-controls">
+      <select class="Playground-selectExample js-playgroundToysEl" aria-label="Code examples">
+        <option value="hello.go">Hello, World!</option>
+        <option value="life.go">Conway's Game of Life</option>
+        <option value="fib.go">Fibonacci Closure</option>
+        <option value="peano.go">Peano Integers</option>
+        <option value="pi.go">Concurrent pi</option>
+        <option value="sieve.go">Concurrent Prime Sieve</option>
+        <option value="solitaire.go">Peg Solitaire Solver</option>
+        <option value="tree.go">Tree Comparison</option>
+      </select>
+      <div class="Playground-buttons">
+        <button class="Button Button--primary js-playgroundRunEl" title="Run this code [shift-enter]">Run</button>
+        <div class="Playground-secondaryButtons">
+          
+            <button class="Button js-playgroundShareEl" title="Share this code">Share</button>
+            <a class="Button tour" href="https://tour.golang.org/" title="Playground Go from your browser">Tour</a>
+          
+        </div>
+      </div>
+    </div>
+  </section>
+
+  
+    <section class="HomeSection Blog js-blogContainerEl">
+      <h2 class="HomeSection-header">Featured articles</h2>
+      <div class="Blog-footer js-blogFooterEl"><a class="Button Button--primary" href="https://blog.golang.org/">Read more &gt;</a></div>
+    </section>
+
+    <section class="HomeSection">
+      <h2 class="HomeSection-header">Featured video</h2>
+      <div class="js-videoContainer" style="--aspect-ratio-padding: 58.07%;">
+        <iframe width="415" height="241" src="https://www.youtube.com/embed/rFejpH_tAHM" frameborder="0" allowfullscreen></iframe>
+      </div>
+    </section>
+  
+</div>
+<script>
+(function() {
+  'use strict';
+
+  window.initFuncs.push(function() {
+    // Set up playground if enabled.
+    if (window.playground) {
+      window.playground({
+        "codeEl":        ".js-playgroundCodeEl",
+        "outputEl":      ".js-playgroundOutputEl",
+        "runEl":         ".js-playgroundRunEl",
+        "shareEl":       ".js-playgroundShareEl",
+        "shareRedirect": "//play.golang.org/p/",
+        "toysEl":        ".js-playgroundToysEl"
+      });
+
+      // The pre matched below is added by the code above. Style it appropriately.
+      document.querySelector(".js-playgroundOutputEl pre").classList.add("Playground-output");
+    } else {
+      $(".Playground").hide();
+    }
+  });
+
+  
+    function readableTime(t) {
+      var m = ["January", "February", "March", "April", "May", "June", "July",
+        "August", "September", "October", "November", "December"];
+      var p = t.substring(0, t.indexOf("T")).split("-");
+      var d = new Date(p[0], p[1]-1, p[2]);
+      return d.getDate() + " " + m[d.getMonth()] + " " + d.getFullYear();
+    }
+
+    window.feedLoaded = function(result) {
+      var read = document.querySelector(".js-blogFooterEl");
+      for (var i = 0; i < result.length && i < 2; i++) {
+        var entry = result[i];
+        var header = document.createElement("h3");
+        header.className = "Blog-title";
+        var titleLink = document.createElement("a");
+        titleLink.href = entry.Link;
+        titleLink.rel = "noopener";
+        titleLink.textContent = entry.Title;
+        header.appendChild(titleLink);
+        read.parentNode.insertBefore(header, read);
+        var extract = document.createElement("div");
+        extract.className = "Blog-extract";
+        extract.innerHTML = entry.Summary;
+        // Ensure any cross-origin links have rel=noopener set.
+        var links = extract.querySelectorAll("a");
+        for (var j = 0; j < links.length; j++) {
+          links[j].rel = "noopener";
+          links[j].classList.add("Blog-link");
+        }
+        read.parentNode.insertBefore(extract, read);
+        var when = document.createElement("div");
+        when.className = "Blog-when";
+        when.textContent = "Published " + readableTime(entry.Time);
+        read.parentNode.insertBefore(when, read);
+      }
+    }
+
+    window.initFuncs.push(function() {
+      // Load blog feed.
+      $("<script/>")
+        .attr("src", "//blog.golang.org/.json?jsonp=feedLoaded")
+        .appendTo("body");
+
+      // Set the video at random.
+      var videos = [
+        {
+          s: "https://www.youtube.com/embed/rFejpH_tAHM",
+          title: "dotGo 2015 - Rob Pike - Simplicity is Complicated",
+        },
+        {
+          s: "https://www.youtube.com/embed/0ReKdcpNyQg",
+          title: "GopherCon 2015: Robert Griesemer - The Evolution of Go",
+        },
+        {
+          s: "https://www.youtube.com/embed/sX8r6zATHGU",
+          title: "Steve Francia - Go: building on the shoulders of giants and stepping on a few toes",
+        },
+        {
+          s: "https://www.youtube.com/embed/rWJHbh6qO_Y",
+          title: "Brad Fitzpatrick Go 1.11 and beyond",
+        },
+        {
+          s: "https://www.youtube.com/embed/bmZNaUcwBt4",
+          title: "The Why of Go",
+        },
+        {
+          s: "https://www.youtube.com/embed/0Zbh_vmAKvk",
+          title: "GopherCon 2017: Russ Cox - The Future of Go",
+        },
+      ];
+      var v = videos[Math.floor(Math.random()*videos.length)];
+      $(".js-videoContainer iframe").attr("src", v.s).attr("title", v.title);
+    });
+   
+})();
+</script>
+
+
+</div><!-- .container -->
+</main><!-- #page -->
+<footer>
+  <div class="Footer ">
+    <img class="Footer-gopher" src="/lib/godoc/images/footer-gopher.jpg" alt="The Go Gopher">
+    <ul class="Footer-links">
+      <li class="Footer-link"><a href="/doc/copyright.html">Copyright</a></li>
+      <li class="Footer-link"><a href="/doc/tos.html">Terms of Service</a></li>
+      <li class="Footer-link"><a href="http://www.google.com/intl/en/policies/privacy/">Privacy Policy</a></li>
+      <li class="Footer-link"><a href="http://golang.org/issues/new?title=x/website:" target="_blank" rel="noopener">Report a website issue</a></li>
+    </ul>
+    <a class="Footer-supportedBy" href="https://google.com">Supported by Google</a>
+  </div>
+</footer>
+
+<script>
+(function() {
+  var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;
+  ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") + ".google-analytics.com/ga.js";
+  var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ga, s);
+})();
+</script>
+
+
--- /dev/null
+++ b/domino/godoc/golang.js
@@ -1,0 +1,97 @@
+(function() {
+  'use strict';
+  window.initFuncs.push(function() {
+    // Set up playground if enabled.
+    if (window.playground) {
+      window.playground({
+        "codeEl":        ".js-playgroundCodeEl",
+        "outputEl":      ".js-playgroundOutputEl",
+        "runEl":         ".js-playgroundRunEl",
+        "shareEl":       ".js-playgroundShareEl",
+        "shareRedirect": "//play.golang.org/p/",
+        "toysEl":        ".js-playgroundToysEl"
+      });
+
+      // The pre matched below is added by the code above. Style it appropriately.
+      document.querySelector(".js-playgroundOutputEl pre").classList.add("Playground-output");
+    } else {
+      $(".Playground").hide();
+    }
+  });
+
+
+    function readableTime(t) {
+      var m = ["January", "February", "March", "April", "May", "June", "July",
+        "August", "September", "October", "November", "December"];
+      var p = t.substring(0, t.indexOf("T")).split("-");
+      var d = new Date(p[0], p[1]-1, p[2]);
+      return d.getDate() + " " + m[d.getMonth()] + " " + d.getFullYear();
+    }
+
+    window.feedLoaded = function(result) {
+      var read = document.querySelector(".js-blogFooterEl");
+      for (var i = 0; i < result.length && i < 2; i++) {
+        var entry = result[i];
+        var header = document.createElement("h3");
+        header.className = "Blog-title";
+        var titleLink = document.createElement("a");
+        titleLink.href = entry.Link;
+        titleLink.rel = "noopener";
+        titleLink.textContent = entry.Title;
+        header.appendChild(titleLink);
+        read.parentNode.insertBefore(header, read);
+        var extract = document.createElement("div");
+        extract.className = "Blog-extract";
+        extract.innerHTML = entry.Summary;
+        // Ensure any cross-origin links have rel=noopener set.
+        var links = extract.querySelectorAll("a");
+        for (var j = 0; j < links.length; j++) {
+          links[j].rel = "noopener";
+          links[j].classList.add("Blog-link");
+        }
+        read.parentNode.insertBefore(extract, read);
+        var when = document.createElement("div");
+        when.className = "Blog-when";
+        when.textContent = "Published " + readableTime(entry.Time);
+        read.parentNode.insertBefore(when, read);
+      }
+    }
+
+    window.initFuncs.push(function() {
+      // Load blog feed.
+      $("<script/>")
+        .attr("src", "//blog.golang.org/.json?jsonp=feedLoaded")
+        .appendTo("body");
+
+      // Set the video at random.
+      var videos = [
+        {
+          s: "https://www.youtube.com/embed/rFejpH_tAHM",
+          title: "dotGo 2015 - Rob Pike - Simplicity is Complicated",
+        },
+        {
+          s: "https://www.youtube.com/embed/0ReKdcpNyQg",
+          title: "GopherCon 2015: Robert Griesemer - The Evolution of Go",
+        },
+        {
+          s: "https://www.youtube.com/embed/sX8r6zATHGU",
+          title: "Steve Francia - Go: building on the shoulders of giants and stepping on a few toes",
+        },
+        {
+          s: "https://www.youtube.com/embed/rWJHbh6qO_Y",
+          title: "Brad Fitzpatrick Go 1.11 and beyond",
+        },
+        {
+          s: "https://www.youtube.com/embed/bmZNaUcwBt4",
+          title: "The Why of Go",
+        },
+        {
+          s: "https://www.youtube.com/embed/0Zbh_vmAKvk",
+          title: "GopherCon 2017: Russ Cox - The Future of Go",
+        },
+      ];
+      var v = videos[Math.floor(Math.random()*videos.length)];
+      $(".js-videoContainer iframe").attr("src", v.s).attr("title", v.title);
+    });
+
+})();
\ No newline at end of file
--- /dev/null
+++ b/domino/godoc/playground.js
@@ -1,0 +1,575 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+In the absence of any formal way to specify interfaces in JavaScript,
+here's a skeleton implementation of a playground transport.
+
+        function Transport() {
+                // Set up any transport state (eg, make a websocket connection).
+                return {
+                        Run: function(body, output, options) {
+                                // Compile and run the program 'body' with 'options'.
+				// Call the 'output' callback to display program output.
+                                return {
+                                        Kill: function() {
+                                                // Kill the running program.
+                                        }
+                                };
+                        }
+                };
+        }
+
+	// The output callback is called multiple times, and each time it is
+	// passed an object of this form.
+        var write = {
+                Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
+                Body: 'string'  // content of write or end status message
+        }
+
+	// The first call must be of Kind 'start' with no body.
+	// Subsequent calls may be of Kind 'stdout' or 'stderr'
+	// and must have a non-null Body string.
+	// The final call should be of Kind 'end' with an optional
+	// Body string, signifying a failure ("killed", for example).
+
+	// The output callback must be of this form.
+	// See PlaygroundOutput (below) for an implementation.
+        function outputCallback(write) {
+        }
+*/
+
+// HTTPTransport is the default transport.
+// enableVet enables running vet if a program was compiled and ran successfully.
+// If vet returned any errors, display them before the output of a program.
+function HTTPTransport(enableVet) {
+  'use strict';
+
+  function playback(output, data) {
+    // Backwards compatibility: default values do not affect the output.
+    var events = data.Events || [];
+    var errors = data.Errors || '';
+    var status = data.Status || 0;
+    var isTest = data.IsTest || false;
+    var testsFailed = data.TestsFailed || 0;
+
+    var timeout;
+    output({ Kind: 'start' });
+    function next() {
+      if (!events || events.length === 0) {
+        if (isTest) {
+          if (testsFailed > 0) {
+            output({
+              Kind: 'system',
+              Body:
+                '\n' +
+                testsFailed +
+                ' test' +
+                (testsFailed > 1 ? 's' : '') +
+                ' failed.',
+            });
+          } else {
+            output({ Kind: 'system', Body: '\nAll tests passed.' });
+          }
+        } else {
+          if (status > 0) {
+            output({ Kind: 'end', Body: 'status ' + status + '.' });
+          } else {
+            if (errors !== '') {
+              // errors are displayed only in the case of timeout.
+              output({ Kind: 'end', Body: errors + '.' });
+            } else {
+              output({ Kind: 'end' });
+            }
+          }
+        }
+        return;
+      }
+      var e = events.shift();
+      if (e.Delay === 0) {
+        output({ Kind: e.Kind, Body: e.Message });
+        next();
+        return;
+      }
+      timeout = setTimeout(function() {
+        output({ Kind: e.Kind, Body: e.Message });
+        next();
+      }, e.Delay / 1000000);
+    }
+    next();
+    return {
+      Stop: function() {
+        clearTimeout(timeout);
+      },
+    };
+  }
+
+  function error(output, msg) {
+    output({ Kind: 'start' });
+    output({ Kind: 'stderr', Body: msg });
+    output({ Kind: 'end' });
+  }
+
+  function buildFailed(output, msg) {
+    output({ Kind: 'start' });
+    output({ Kind: 'stderr', Body: msg });
+    output({ Kind: 'system', Body: '\nGo build failed.' });
+  }
+
+  var seq = 0;
+  return {
+    Run: function(body, output, options) {
+      seq++;
+      var cur = seq;
+      var playing;
+      $.ajax('/compile', {
+        type: 'POST',
+        data: { version: 2, body: body },
+        dataType: 'json',
+        success: function(data) {
+          if (seq != cur) return;
+          if (!data) return;
+          if (playing != null) playing.Stop();
+          if (data.Errors) {
+            if (data.Errors === 'process took too long') {
+              // Playback the output that was captured before the timeout.
+              playing = playback(output, data);
+            } else {
+              buildFailed(output, data.Errors);
+            }
+            return;
+          }
+
+          if (!enableVet) {
+            playing = playback(output, data);
+            return;
+          }
+
+          $.ajax('/vet', {
+            data: { body: body },
+            type: 'POST',
+            dataType: 'json',
+            success: function(dataVet) {
+              if (dataVet.Errors) {
+                if (!data.Events) {
+                  data.Events = [];
+                }
+                // inject errors from the vet as the first events in the output
+                data.Events.unshift({
+                  Message: 'Go vet exited.\n\n',
+                  Kind: 'system',
+                  Delay: 0,
+                });
+                data.Events.unshift({
+                  Message: dataVet.Errors,
+                  Kind: 'stderr',
+                  Delay: 0,
+                });
+              }
+              playing = playback(output, data);
+            },
+            error: function() {
+              playing = playback(output, data);
+            },
+          });
+        },
+        error: function() {
+          error(output, 'Error communicating with remote server.');
+        },
+      });
+      return {
+        Kill: function() {
+          if (playing != null) playing.Stop();
+          output({ Kind: 'end', Body: 'killed' });
+        },
+      };
+    },
+  };
+}
+
+function SocketTransport() {
+  'use strict';
+
+  var id = 0;
+  var outputs = {};
+  var started = {};
+  var websocket;
+  if (window.location.protocol == 'http:') {
+    websocket = new WebSocket('ws://' + window.location.host + '/socket');
+  } else if (window.location.protocol == 'https:') {
+    websocket = new WebSocket('wss://' + window.location.host + '/socket');
+  }
+
+  websocket.onclose = function() {
+    console.log('websocket connection closed');
+  };
+
+  websocket.onmessage = function(e) {
+    var m = JSON.parse(e.data);
+    var output = outputs[m.Id];
+    if (output === null) return;
+    if (!started[m.Id]) {
+      output({ Kind: 'start' });
+      started[m.Id] = true;
+    }
+    output({ Kind: m.Kind, Body: m.Body });
+  };
+
+  function send(m) {
+    websocket.send(JSON.stringify(m));
+  }
+
+  return {
+    Run: function(body, output, options) {
+      var thisID = id + '';
+      id++;
+      outputs[thisID] = output;
+      send({ Id: thisID, Kind: 'run', Body: body, Options: options });
+      return {
+        Kill: function() {
+          send({ Id: thisID, Kind: 'kill' });
+        },
+      };
+    },
+  };
+}
+
+function PlaygroundOutput(el) {
+  'use strict';
+
+  return function(write) {
+    if (write.Kind == 'start') {
+      el.innerHTML = '';
+      return;
+    }
+
+    var cl = 'system';
+    if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
+
+    var m = write.Body;
+    if (write.Kind == 'end') {
+      m = '\nProgram exited' + (m ? ': ' + m : '.');
+    }
+
+    if (m.indexOf('IMAGE:') === 0) {
+      // TODO(adg): buffer all writes before creating image
+      var url = 'data:image/png;base64,' + m.substr(6);
+      var img = document.createElement('img');
+      img.src = url;
+      el.appendChild(img);
+      return;
+    }
+
+    // ^L clears the screen.
+    var s = m.split('\x0c');
+    if (s.length > 1) {
+      el.innerHTML = '';
+      m = s.pop();
+    }
+
+    m = m.replace(/&/g, '&amp;');
+    m = m.replace(/</g, '&lt;');
+    m = m.replace(/>/g, '&gt;');
+
+    var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
+
+    var span = document.createElement('span');
+    span.className = cl;
+    span.innerHTML = m;
+    el.appendChild(span);
+
+    if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
+  };
+}
+
+(function() {
+  function lineHighlight(error) {
+    var regex = /prog.go:([0-9]+)/g;
+    var r = regex.exec(error);
+    while (r) {
+      $('.lines div')
+        .eq(r[1] - 1)
+        .addClass('lineerror');
+      r = regex.exec(error);
+    }
+  }
+  function highlightOutput(wrappedOutput) {
+    return function(write) {
+      if (write.Body) lineHighlight(write.Body);
+      wrappedOutput(write);
+    };
+  }
+  function lineClear() {
+    $('.lineerror').removeClass('lineerror');
+  }
+
+  // opts is an object with these keys
+  //  codeEl - code editor element
+  //  outputEl - program output element
+  //  runEl - run button element
+  //  fmtEl - fmt button element (optional)
+  //  fmtImportEl - fmt "imports" checkbox element (optional)
+  //  shareEl - share button element (optional)
+  //  shareURLEl - share URL text input element (optional)
+  //  shareRedirect - base URL to redirect to on share (optional)
+  //  toysEl - toys select element (optional)
+  //  enableHistory - enable using HTML5 history API (optional)
+  //  transport - playground transport to use (default is HTTPTransport)
+  //  enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
+  //  enableVet - enable running vet and displaying its errors
+  function playground(opts) {
+    var code = $(opts.codeEl);
+    var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
+    var running;
+
+    // autoindent helpers.
+    function insertTabs(n) {
+      // find the selection start and end
+      var start = code[0].selectionStart;
+      var end = code[0].selectionEnd;
+      // split the textarea content into two, and insert n tabs
+      var v = code[0].value;
+      var u = v.substr(0, start);
+      for (var i = 0; i < n; i++) {
+        u += '\t';
+      }
+      u += v.substr(end);
+      // set revised content
+      code[0].value = u;
+      // reset caret position after inserted tabs
+      code[0].selectionStart = start + n;
+      code[0].selectionEnd = start + n;
+    }
+    function autoindent(el) {
+      var curpos = el.selectionStart;
+      var tabs = 0;
+      while (curpos > 0) {
+        curpos--;
+        if (el.value[curpos] == '\t') {
+          tabs++;
+        } else if (tabs > 0 || el.value[curpos] == '\n') {
+          break;
+        }
+      }
+      setTimeout(function() {
+        insertTabs(tabs);
+      }, 1);
+    }
+
+    // NOTE(cbro): e is a jQuery event, not a DOM event.
+    function handleSaveShortcut(e) {
+      if (e.isDefaultPrevented()) return false;
+      if (!e.metaKey && !e.ctrlKey) return false;
+      if (e.key != 'S' && e.key != 's') return false;
+
+      e.preventDefault();
+
+      // Share and save
+      share(function(url) {
+        window.location.href = url + '.go?download=true';
+      });
+
+      return true;
+    }
+
+    function keyHandler(e) {
+      if (opts.enableShortcuts && handleSaveShortcut(e)) return;
+
+      if (e.keyCode == 9 && !e.ctrlKey) {
+        // tab (but not ctrl-tab)
+        insertTabs(1);
+        e.preventDefault();
+        return false;
+      }
+      if (e.keyCode == 13) {
+        // enter
+        if (e.shiftKey) {
+          // +shift
+          run();
+          e.preventDefault();
+          return false;
+        }
+        if (e.ctrlKey) {
+          // +control
+          fmt();
+          e.preventDefault();
+        } else {
+          autoindent(e.target);
+        }
+      }
+      return true;
+    }
+    code.unbind('keydown').bind('keydown', keyHandler);
+    var outdiv = $(opts.outputEl).empty();
+    var output = $('<pre/>').appendTo(outdiv);
+
+    function body() {
+      return $(opts.codeEl).val();
+    }
+    function setBody(text) {
+      $(opts.codeEl).val(text);
+    }
+    function origin(href) {
+      return ('' + href)
+        .split('/')
+        .slice(0, 3)
+        .join('/');
+    }
+
+    var pushedEmpty = window.location.pathname == '/';
+    function inputChanged() {
+      if (pushedEmpty) {
+        return;
+      }
+      pushedEmpty = true;
+      $(opts.shareURLEl).hide();
+      window.history.pushState(null, '', '/');
+    }
+    function popState(e) {
+      if (e === null) {
+        return;
+      }
+      if (e && e.state && e.state.code) {
+        setBody(e.state.code);
+      }
+    }
+    var rewriteHistory = false;
+    if (
+      window.history &&
+      window.history.pushState &&
+      window.addEventListener &&
+      opts.enableHistory
+    ) {
+      rewriteHistory = true;
+      code[0].addEventListener('input', inputChanged);
+      window.addEventListener('popstate', popState);
+    }
+
+    function setError(error) {
+      if (running) running.Kill();
+      lineClear();
+      lineHighlight(error);
+      output
+        .empty()
+        .addClass('error')
+        .text(error);
+    }
+    function loading() {
+      lineClear();
+      if (running) running.Kill();
+      output.removeClass('error').text('Waiting for remote server...');
+    }
+    function run() {
+      loading();
+      running = transport.Run(
+        body(),
+        highlightOutput(PlaygroundOutput(output[0]))
+      );
+    }
+
+    function fmt() {
+      loading();
+      var data = { body: body() };
+      if ($(opts.fmtImportEl).is(':checked')) {
+        data['imports'] = 'true';
+      }
+      $.ajax('/fmt', {
+        data: data,
+        type: 'POST',
+        dataType: 'json',
+        success: function(data) {
+          if (data.Error) {
+            setError(data.Error);
+          } else {
+            setBody(data.Body);
+            setError('');
+          }
+        },
+      });
+    }
+
+    var shareURL; // jQuery element to show the shared URL.
+    var sharing = false; // true if there is a pending request.
+    var shareCallbacks = [];
+    function share(opt_callback) {
+      if (opt_callback) shareCallbacks.push(opt_callback);
+
+      if (sharing) return;
+      sharing = true;
+
+      var sharingData = body();
+      $.ajax('/share', {
+        processData: false,
+        data: sharingData,
+        type: 'POST',
+        contentType: 'text/plain; charset=utf-8',
+        complete: function(xhr) {
+          sharing = false;
+          if (xhr.status != 200) {
+            alert('Server error; try again.');
+            return;
+          }
+          if (opts.shareRedirect) {
+            window.location = opts.shareRedirect + xhr.responseText;
+          }
+          var path = '/p/' + xhr.responseText;
+          var url = origin(window.location) + path;
+
+          for (var i = 0; i < shareCallbacks.length; i++) {
+            shareCallbacks[i](url);
+          }
+          shareCallbacks = [];
+
+          if (shareURL) {
+            shareURL
+              .show()
+              .val(url)
+              .focus()
+              .select();
+
+            if (rewriteHistory) {
+              var historyData = { code: sharingData };
+              window.history.pushState(historyData, '', path);
+              pushedEmpty = false;
+            }
+          }
+        },
+      });
+    }
+
+    $(opts.runEl).click(run);
+    $(opts.fmtEl).click(fmt);
+
+    if (
+      opts.shareEl !== null &&
+      (opts.shareURLEl !== null || opts.shareRedirect !== null)
+    ) {
+      if (opts.shareURLEl) {
+        shareURL = $(opts.shareURLEl).hide();
+      }
+      $(opts.shareEl).click(function() {
+        share();
+      });
+    }
+
+    if (opts.toysEl !== null) {
+      $(opts.toysEl).bind('change', function() {
+        var toy = $(this).val();
+        $.ajax('/doc/play/' + toy, {
+          processData: false,
+          type: 'GET',
+          complete: function(xhr) {
+            if (xhr.status != 200) {
+              alert('Server error; try again.');
+              return;
+            }
+            setBody(xhr.responseText);
+          },
+        });
+      });
+    }
+  }
+
+  window.playground = playground;
+})();
--- a/domino/playground.js
+++ /dev/null
@@ -1,575 +1,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/*
-In the absence of any formal way to specify interfaces in JavaScript,
-here's a skeleton implementation of a playground transport.
-
-        function Transport() {
-                // Set up any transport state (eg, make a websocket connection).
-                return {
-                        Run: function(body, output, options) {
-                                // Compile and run the program 'body' with 'options'.
-				// Call the 'output' callback to display program output.
-                                return {
-                                        Kill: function() {
-                                                // Kill the running program.
-                                        }
-                                };
-                        }
-                };
-        }
-
-	// The output callback is called multiple times, and each time it is
-	// passed an object of this form.
-        var write = {
-                Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
-                Body: 'string'  // content of write or end status message
-        }
-
-	// The first call must be of Kind 'start' with no body.
-	// Subsequent calls may be of Kind 'stdout' or 'stderr'
-	// and must have a non-null Body string.
-	// The final call should be of Kind 'end' with an optional
-	// Body string, signifying a failure ("killed", for example).
-
-	// The output callback must be of this form.
-	// See PlaygroundOutput (below) for an implementation.
-        function outputCallback(write) {
-        }
-*/
-
-// HTTPTransport is the default transport.
-// enableVet enables running vet if a program was compiled and ran successfully.
-// If vet returned any errors, display them before the output of a program.
-function HTTPTransport(enableVet) {
-  'use strict';
-
-  function playback(output, data) {
-    // Backwards compatibility: default values do not affect the output.
-    var events = data.Events || [];
-    var errors = data.Errors || '';
-    var status = data.Status || 0;
-    var isTest = data.IsTest || false;
-    var testsFailed = data.TestsFailed || 0;
-
-    var timeout;
-    output({ Kind: 'start' });
-    function next() {
-      if (!events || events.length === 0) {
-        if (isTest) {
-          if (testsFailed > 0) {
-            output({
-              Kind: 'system',
-              Body:
-                '\n' +
-                testsFailed +
-                ' test' +
-                (testsFailed > 1 ? 's' : '') +
-                ' failed.',
-            });
-          } else {
-            output({ Kind: 'system', Body: '\nAll tests passed.' });
-          }
-        } else {
-          if (status > 0) {
-            output({ Kind: 'end', Body: 'status ' + status + '.' });
-          } else {
-            if (errors !== '') {
-              // errors are displayed only in the case of timeout.
-              output({ Kind: 'end', Body: errors + '.' });
-            } else {
-              output({ Kind: 'end' });
-            }
-          }
-        }
-        return;
-      }
-      var e = events.shift();
-      if (e.Delay === 0) {
-        output({ Kind: e.Kind, Body: e.Message });
-        next();
-        return;
-      }
-      timeout = setTimeout(function() {
-        output({ Kind: e.Kind, Body: e.Message });
-        next();
-      }, e.Delay / 1000000);
-    }
-    next();
-    return {
-      Stop: function() {
-        clearTimeout(timeout);
-      },
-    };
-  }
-
-  function error(output, msg) {
-    output({ Kind: 'start' });
-    output({ Kind: 'stderr', Body: msg });
-    output({ Kind: 'end' });
-  }
-
-  function buildFailed(output, msg) {
-    output({ Kind: 'start' });
-    output({ Kind: 'stderr', Body: msg });
-    output({ Kind: 'system', Body: '\nGo build failed.' });
-  }
-
-  var seq = 0;
-  return {
-    Run: function(body, output, options) {
-      seq++;
-      var cur = seq;
-      var playing;
-      $.ajax('/compile', {
-        type: 'POST',
-        data: { version: 2, body: body },
-        dataType: 'json',
-        success: function(data) {
-          if (seq != cur) return;
-          if (!data) return;
-          if (playing != null) playing.Stop();
-          if (data.Errors) {
-            if (data.Errors === 'process took too long') {
-              // Playback the output that was captured before the timeout.
-              playing = playback(output, data);
-            } else {
-              buildFailed(output, data.Errors);
-            }
-            return;
-          }
-
-          if (!enableVet) {
-            playing = playback(output, data);
-            return;
-          }
-
-          $.ajax('/vet', {
-            data: { body: body },
-            type: 'POST',
-            dataType: 'json',
-            success: function(dataVet) {
-              if (dataVet.Errors) {
-                if (!data.Events) {
-                  data.Events = [];
-                }
-                // inject errors from the vet as the first events in the output
-                data.Events.unshift({
-                  Message: 'Go vet exited.\n\n',
-                  Kind: 'system',
-                  Delay: 0,
-                });
-                data.Events.unshift({
-                  Message: dataVet.Errors,
-                  Kind: 'stderr',
-                  Delay: 0,
-                });
-              }
-              playing = playback(output, data);
-            },
-            error: function() {
-              playing = playback(output, data);
-            },
-          });
-        },
-        error: function() {
-          error(output, 'Error communicating with remote server.');
-        },
-      });
-      return {
-        Kill: function() {
-          if (playing != null) playing.Stop();
-          output({ Kind: 'end', Body: 'killed' });
-        },
-      };
-    },
-  };
-}
-
-function SocketTransport() {
-  'use strict';
-
-  var id = 0;
-  var outputs = {};
-  var started = {};
-  var websocket;
-  if (window.location.protocol == 'http:') {
-    websocket = new WebSocket('ws://' + window.location.host + '/socket');
-  } else if (window.location.protocol == 'https:') {
-    websocket = new WebSocket('wss://' + window.location.host + '/socket');
-  }
-
-  websocket.onclose = function() {
-    console.log('websocket connection closed');
-  };
-
-  websocket.onmessage = function(e) {
-    var m = JSON.parse(e.data);
-    var output = outputs[m.Id];
-    if (output === null) return;
-    if (!started[m.Id]) {
-      output({ Kind: 'start' });
-      started[m.Id] = true;
-    }
-    output({ Kind: m.Kind, Body: m.Body });
-  };
-
-  function send(m) {
-    websocket.send(JSON.stringify(m));
-  }
-
-  return {
-    Run: function(body, output, options) {
-      var thisID = id + '';
-      id++;
-      outputs[thisID] = output;
-      send({ Id: thisID, Kind: 'run', Body: body, Options: options });
-      return {
-        Kill: function() {
-          send({ Id: thisID, Kind: 'kill' });
-        },
-      };
-    },
-  };
-}
-
-function PlaygroundOutput(el) {
-  'use strict';
-
-  return function(write) {
-    if (write.Kind == 'start') {
-      el.innerHTML = '';
-      return;
-    }
-
-    var cl = 'system';
-    if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
-
-    var m = write.Body;
-    if (write.Kind == 'end') {
-      m = '\nProgram exited' + (m ? ': ' + m : '.');
-    }
-
-    if (m.indexOf('IMAGE:') === 0) {
-      // TODO(adg): buffer all writes before creating image
-      var url = 'data:image/png;base64,' + m.substr(6);
-      var img = document.createElement('img');
-      img.src = url;
-      el.appendChild(img);
-      return;
-    }
-
-    // ^L clears the screen.
-    var s = m.split('\x0c');
-    if (s.length > 1) {
-      el.innerHTML = '';
-      m = s.pop();
-    }
-
-    m = m.replace(/&/g, '&amp;');
-    m = m.replace(/</g, '&lt;');
-    m = m.replace(/>/g, '&gt;');
-
-    var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
-
-    var span = document.createElement('span');
-    span.className = cl;
-    span.innerHTML = m;
-    el.appendChild(span);
-
-    if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
-  };
-}
-
-(function() {
-  function lineHighlight(error) {
-    var regex = /prog.go:([0-9]+)/g;
-    var r = regex.exec(error);
-    while (r) {
-      $('.lines div')
-        .eq(r[1] - 1)
-        .addClass('lineerror');
-      r = regex.exec(error);
-    }
-  }
-  function highlightOutput(wrappedOutput) {
-    return function(write) {
-      if (write.Body) lineHighlight(write.Body);
-      wrappedOutput(write);
-    };
-  }
-  function lineClear() {
-    $('.lineerror').removeClass('lineerror');
-  }
-
-  // opts is an object with these keys
-  //  codeEl - code editor element
-  //  outputEl - program output element
-  //  runEl - run button element
-  //  fmtEl - fmt button element (optional)
-  //  fmtImportEl - fmt "imports" checkbox element (optional)
-  //  shareEl - share button element (optional)
-  //  shareURLEl - share URL text input element (optional)
-  //  shareRedirect - base URL to redirect to on share (optional)
-  //  toysEl - toys select element (optional)
-  //  enableHistory - enable using HTML5 history API (optional)
-  //  transport - playground transport to use (default is HTTPTransport)
-  //  enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
-  //  enableVet - enable running vet and displaying its errors
-  function playground(opts) {
-    var code = $(opts.codeEl);
-    var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
-    var running;
-
-    // autoindent helpers.
-    function insertTabs(n) {
-      // find the selection start and end
-      var start = code[0].selectionStart;
-      var end = code[0].selectionEnd;
-      // split the textarea content into two, and insert n tabs
-      var v = code[0].value;
-      var u = v.substr(0, start);
-      for (var i = 0; i < n; i++) {
-        u += '\t';
-      }
-      u += v.substr(end);
-      // set revised content
-      code[0].value = u;
-      // reset caret position after inserted tabs
-      code[0].selectionStart = start + n;
-      code[0].selectionEnd = start + n;
-    }
-    function autoindent(el) {
-      var curpos = el.selectionStart;
-      var tabs = 0;
-      while (curpos > 0) {
-        curpos--;
-        if (el.value[curpos] == '\t') {
-          tabs++;
-        } else if (tabs > 0 || el.value[curpos] == '\n') {
-          break;
-        }
-      }
-      setTimeout(function() {
-        insertTabs(tabs);
-      }, 1);
-    }
-
-    // NOTE(cbro): e is a jQuery event, not a DOM event.
-    function handleSaveShortcut(e) {
-      if (e.isDefaultPrevented()) return false;
-      if (!e.metaKey && !e.ctrlKey) return false;
-      if (e.key != 'S' && e.key != 's') return false;
-
-      e.preventDefault();
-
-      // Share and save
-      share(function(url) {
-        window.location.href = url + '.go?download=true';
-      });
-
-      return true;
-    }
-
-    function keyHandler(e) {
-      if (opts.enableShortcuts && handleSaveShortcut(e)) return;
-
-      if (e.keyCode == 9 && !e.ctrlKey) {
-        // tab (but not ctrl-tab)
-        insertTabs(1);
-        e.preventDefault();
-        return false;
-      }
-      if (e.keyCode == 13) {
-        // enter
-        if (e.shiftKey) {
-          // +shift
-          run();
-          e.preventDefault();
-          return false;
-        }
-        if (e.ctrlKey) {
-          // +control
-          fmt();
-          e.preventDefault();
-        } else {
-          autoindent(e.target);
-        }
-      }
-      return true;
-    }
-    code.unbind('keydown').bind('keydown', keyHandler);
-    var outdiv = $(opts.outputEl).empty();
-    var output = $('<pre/>').appendTo(outdiv);
-
-    function body() {
-      return $(opts.codeEl).val();
-    }
-    function setBody(text) {
-      $(opts.codeEl).val(text);
-    }
-    function origin(href) {
-      return ('' + href)
-        .split('/')
-        .slice(0, 3)
-        .join('/');
-    }
-
-    var pushedEmpty = window.location.pathname == '/';
-    function inputChanged() {
-      if (pushedEmpty) {
-        return;
-      }
-      pushedEmpty = true;
-      $(opts.shareURLEl).hide();
-      window.history.pushState(null, '', '/');
-    }
-    function popState(e) {
-      if (e === null) {
-        return;
-      }
-      if (e && e.state && e.state.code) {
-        setBody(e.state.code);
-      }
-    }
-    var rewriteHistory = false;
-    if (
-      window.history &&
-      window.history.pushState &&
-      window.addEventListener &&
-      opts.enableHistory
-    ) {
-      rewriteHistory = true;
-      code[0].addEventListener('input', inputChanged);
-      window.addEventListener('popstate', popState);
-    }
-
-    function setError(error) {
-      if (running) running.Kill();
-      lineClear();
-      lineHighlight(error);
-      output
-        .empty()
-        .addClass('error')
-        .text(error);
-    }
-    function loading() {
-      lineClear();
-      if (running) running.Kill();
-      output.removeClass('error').text('Waiting for remote server...');
-    }
-    function run() {
-      loading();
-      running = transport.Run(
-        body(),
-        highlightOutput(PlaygroundOutput(output[0]))
-      );
-    }
-
-    function fmt() {
-      loading();
-      var data = { body: body() };
-      if ($(opts.fmtImportEl).is(':checked')) {
-        data['imports'] = 'true';
-      }
-      $.ajax('/fmt', {
-        data: data,
-        type: 'POST',
-        dataType: 'json',
-        success: function(data) {
-          if (data.Error) {
-            setError(data.Error);
-          } else {
-            setBody(data.Body);
-            setError('');
-          }
-        },
-      });
-    }
-
-    var shareURL; // jQuery element to show the shared URL.
-    var sharing = false; // true if there is a pending request.
-    var shareCallbacks = [];
-    function share(opt_callback) {
-      if (opt_callback) shareCallbacks.push(opt_callback);
-
-      if (sharing) return;
-      sharing = true;
-
-      var sharingData = body();
-      $.ajax('/share', {
-        processData: false,
-        data: sharingData,
-        type: 'POST',
-        contentType: 'text/plain; charset=utf-8',
-        complete: function(xhr) {
-          sharing = false;
-          if (xhr.status != 200) {
-            alert('Server error; try again.');
-            return;
-          }
-          if (opts.shareRedirect) {
-            window.location = opts.shareRedirect + xhr.responseText;
-          }
-          var path = '/p/' + xhr.responseText;
-          var url = origin(window.location) + path;
-
-          for (var i = 0; i < shareCallbacks.length; i++) {
-            shareCallbacks[i](url);
-          }
-          shareCallbacks = [];
-
-          if (shareURL) {
-            shareURL
-              .show()
-              .val(url)
-              .focus()
-              .select();
-
-            if (rewriteHistory) {
-              var historyData = { code: sharingData };
-              window.history.pushState(historyData, '', path);
-              pushedEmpty = false;
-            }
-          }
-        },
-      });
-    }
-
-    $(opts.runEl).click(run);
-    $(opts.fmtEl).click(fmt);
-
-    if (
-      opts.shareEl !== null &&
-      (opts.shareURLEl !== null || opts.shareRedirect !== null)
-    ) {
-      if (opts.shareURLEl) {
-        shareURL = $(opts.shareURLEl).hide();
-      }
-      $(opts.shareEl).click(function() {
-        share();
-      });
-    }
-
-    if (opts.toysEl !== null) {
-      $(opts.toysEl).bind('change', function() {
-        var toy = $(this).val();
-        $.ajax('/doc/play/' + toy, {
-          processData: false,
-          type: 'GET',
-          complete: function(xhr) {
-            if (xhr.status != 200) {
-              alert('Server error; try again.');
-              return;
-            }
-            setBody(xhr.responseText);
-          },
-        });
-      });
-    }
-  }
-
-  window.playground = playground;
-})();
--- a/nodes/nodes.go
+++ b/nodes/nodes.go
@@ -190,13 +190,13 @@
 		return ref, true
 	}
 
-	i := 0
+	i := 1
 	for _, c := range n.Parent.Children {
-		if c.Type() == html.ElementNode {
-			i++
-		}
 		if c == n {
 			break
+		}
+		if c.Type() == html.ElementNode {
+			i++
 		}
 	}
 	ref += fmt.Sprintf(":nth-child(%v)", i)