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 ></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, '&');
+ m = m.replace(/</g, '<');
+ m = m.replace(/>/g, '>');
+
+ 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, '&');
- m = m.replace(/</g, '<');
- m = m.replace(/>/g, '>');
-
- 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)--
⑨