This is a replacement for the UI of play.golang.org,
although it still uses play.golang.org as the backend
and probably always will, to keep the playground backend
deployment separate from the rest of the web site.

Change-Id: Ia39000e80368b98d9cc273d246f2c83670fbacc4
Reviewed-on: https://go-review.googlesource.com/c/website/+/364815
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
This commit is contained in:
Russ Cox 2021-11-09 12:50:51 -05:00
Родитель 36da86b3f1
Коммит 961523a997
14 изменённых файлов: 413 добавлений и 97 удалений

Просмотреть файл

@ -44,7 +44,6 @@ var csp = map[string][]string{
"connect-src": {
"'self'",
"https://golang.org",
"https://play.golang.org", // For running playground snippets on the blog.
"www.google-analytics.com",
"stats.g.doubleclick.net",
},

Просмотреть файл

@ -207,6 +207,8 @@ func NewHandler(contentDir, goroot string) http.Handler {
if err := blog.RegisterFeeds(godevMux, "", godevSite); err != nil {
log.Fatalf("blog: %v", err)
}
mux.Handle("go.dev/play", playHandler(godevSite))
mux.Handle("go.dev/play/p/", playHandler(godevSite))
mux.Handle("go.dev/", addCSP(godevMux))
mux.Handle("blog.golang.org/", redirectPrefix("https://go.dev/blog/"))
@ -215,6 +217,7 @@ func NewHandler(contentDir, goroot string) http.Handler {
if runningOnAppEngine {
appEngineSetup(site, chinaSite, mux)
}
proxy.RegisterHandlers(mux)
dl.RegisterHandlers(mux, site, "golang.org", datastoreClient, memcacheClient)
dl.RegisterHandlers(mux, chinaSite, "golang.google.cn", datastoreClient, memcacheClient)
@ -224,6 +227,16 @@ func NewHandler(contentDir, goroot string) http.Handler {
return h
}
func playHandler(godevSite *web.Site) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
godevSite.ServePage(w, r, web.Page{
"URL": r.URL.Path,
"layout": "play",
"title": "Go Playground",
})
})
}
// newSite creates a new site for a given content and goroot file system pair
// and registers it in mux to handle requests for host.
// If host is the empty string, the registrations are for the wildcard host.
@ -340,7 +353,6 @@ func appEngineSetup(site, chinaSite *web.Site, mux *http.ServeMux) {
memcacheClient = memcache.New(redisAddr)
short.RegisterHandlers(mux, "golang.org", datastoreClient, memcacheClient)
proxy.RegisterHandlers(mux, googleCN)
log.Println("AppEngine initialization complete")
}

4
cmd/golangorg/testdata/godev.txt поставляемый
Просмотреть файл

@ -26,3 +26,7 @@ redirect == /solutions/google/sitereliability
GET https://go.dev/solutions/americanexpress
body contains <div class="Article-date">19 December 2019</div>
GET https://go.dev/play
body contains The Go Playground
body contains About the Playground
body contains Hello, 世界

Просмотреть файл

@ -24,10 +24,12 @@ table {
-webkit-border-vertical-spacing: 0;
}
code,
pre {
pre,
.linedtextarea .lines div {
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
}
pre {
pre,
.linedtextarea .lines div {
font-size: 0.83em;
overflow-x: auto;
}
@ -344,7 +346,7 @@ a.Footer-link--primary {
}
@media only screen and (min-width: 57.7rem) {
.Header {
.Header, .PlayPage {
padding: 0 1.5rem;
}
.Header-menuItem {
@ -3802,15 +3804,32 @@ a.error {
border-top-right-radius: 0.3125rem;
overflow: hidden;
}
.PlayPage .Playground-inputContainer {
height: 20em;
}
.PlayPage .Playground-outputContainer {
height: 20em;
}
@media screen and (min-height: 60em) {
.PlayPage .Playground-inputContainer {
height: 40em;
}
}
.PlayAbout {
font-size: 83%;
}
.Playground-input {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0.625rem;
min-height: 11rem;
min-height: 12rem;
resize: none;
}
.Playground-inputContainer #wrap {
height: 100%;
}
.Playground-outputContainer {
border-bottom-right-radius: 0.3125rem;
border-bottom-left-radius: 0.3125rem;
@ -3825,10 +3844,20 @@ a.error {
border-radius: 0;
}
.Playground-inputContainer,
.Playground-input,
.Playground-input {
background: #ffffdd;
}
.Playground-runButton {
background-color: #ffffdd !important;
}
.Playground-outputContainer,
.Playground-output {
background: #f7f9fa;
}
.Playground-inputContainer,
.Playground-input,
.Playground-outputContainer,
.Playground-output {
color: #202224;
}
.Playground-inputContainer,
@ -3855,7 +3884,7 @@ a.error {
font-family: inherit;
font-size: 16px;
/* Prevents automatic zoom on mobile devices */
height: 2.375rem;
height: 1.75rem;
margin: 0 0 0.5rem 0;
width: 100%;
}
@ -3877,6 +3906,45 @@ a.error {
width: auto;
}
}
h1.Playground-title {
font-size: 1.5rem;
line-height: 1.75rem;
display: inline;
margin: 0;
}
h2.Playground-about {
font-size: 1rem;
line-height: 2rem;
vertical-align: bottom;
height: 1.75rem;
display: inline;
margin: 0;
margin-left: 1rem;
}
.PlayPage .Playground-controls {
margin-top: 1rem;
margin-bottom: 1rem;
}
.linedtextarea .lines {
float: left;
overflow: hidden;
text-align: right;
}
.linedtextarea .lines div {
padding-right: 5px;
color: lightgray;
}
.linedtextarea .lineerror {
color: black !important;
background: #fdd;
}
.linedtextarea textarea {
height: 100%;
width: 100%;
float: right;
padding: 0;
font-size: 0.83rem;
}
.Button,
.Button:link,
.Button:visited {
@ -3890,7 +3958,7 @@ a.error {
cursor: pointer;
display: inline-flex;
font: bold 0.875rem Roboto, sans-serif;
height: 2.375rem;
height: 1.75rem;
padding: 0 0.625rem;
justify-content: center;
min-width: 4.063rem;

Просмотреть файл

@ -174,7 +174,7 @@ func main() {
<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>
<button class="Button Button--primary js-playgroundRunEl Playground-runButton" title="Run this code [shift-enter]">Run</button>
<div class="Playground-secondaryButtons">
{{- if $canShare}}
<button class="Button js-playgroundShareEl" title="Share this code">Share</button>

Просмотреть файл

@ -158,7 +158,7 @@
runEl: $('.run', el),
fmtEl: $('.fmt', el),
shareEl: $('.share', el),
shareRedirect: '//play.golang.org/p/',
shareRedirect: '//go.dev/play/p/',
});
// Make the code textarea resize to fit content.

Просмотреть файл

@ -9,8 +9,9 @@ window.addEventListener('DOMContentLoaded', () => {
"codeEl": ".js-playgroundCodeEl",
"outputEl": ".js-playgroundOutputEl",
"runEl": ".js-playgroundRunEl",
"fmtEl": ".js-playgroundFmtEl",
"shareEl": ".js-playgroundShareEl",
"shareRedirect": "//play.golang.org/p/",
"shareRedirect": "/play/p/",
"toysEl": ".js-playgroundToysEl"
});

57
go.dev/_content/js/jquery-linedtextarea.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,57 @@
/**
* Adapted from jQuery Lined Textarea Plugin
* http://alan.blog-city.com/jquerylinedtextarea.htm
*
* Released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*/
(function($) {
$.fn.linedtextarea = function() {
/*
* Helper function to make sure the line numbers are always kept up to
* the current system
*/
var fillOutLines = function(linesDiv, h, lineNo) {
while (linesDiv.height() < h) {
linesDiv.append("<div>" + lineNo + "</div>");
lineNo++;
}
return lineNo;
};
return this.each(function() {
var lineNo = 1;
var textarea = $(this);
/* Wrap the text area in the elements we need */
textarea.wrap("<div class='linedtextarea' style='height:100%; overflow:hidden'></div>");
textarea.width("97%");
textarea.parent().prepend("<div class='lines' style='width:3%'></div>");
var linesDiv = textarea.parent().find(".lines");
var scroll = function(tn) {
console.log('scroll');
var domTextArea = $(this)[0];
var scrollTop = domTextArea.scrollTop;
var clientHeight = domTextArea.clientHeight;
linesDiv.css({
'margin-top' : (-scrollTop) + "px"
});
lineNo = fillOutLines(linesDiv, scrollTop + clientHeight,
lineNo);
};
/* React to the scroll event */
textarea.scroll(scroll);
$(window).resize(function() { textarea.scroll(); });
/* We call scroll once to add the line numbers */
textarea.scroll();
/* React to textarea resize via css resize attribute. */
var observer = new ResizeObserver(function(mutations) {
textarea.scroll();
});
observer.observe(textarea[0], {attributes: true});
});
};
})(jQuery);

Просмотреть файл

@ -123,9 +123,9 @@ function HTTPTransport(enableVet) {
seq++;
var cur = seq;
var playing;
$.ajax('https://play.golang.org/compile', {
$.ajax('/_/compile', {
type: 'POST',
data: { version: 2, body: body },
data: { version: 2, body: body, withVet: enableVet },
dataType: 'json',
success: function(data) {
if (seq != cur) return;
@ -140,39 +140,7 @@ function HTTPTransport(enableVet) {
}
return;
}
if (!enableVet) {
playing = playback(output, data);
return;
}
$.ajax('https://play.golang.org/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);
},
});
playing = playback(output, data);
},
error: function() {
error(output, 'Error communicating with remote server.');
@ -417,14 +385,16 @@ function PlaygroundOutput(el) {
.join('/');
}
var pushedEmpty = window.location.pathname == '/';
var pushedPlay = window.location.pathname == '/play';
function inputChanged() {
if (pushedEmpty) {
if (pushedPlay) {
return;
}
pushedEmpty = true;
pushedPlay = true;
$(opts.shareURLEl).hide();
window.history.pushState(null, '', '/');
var path = window.location.pathname;
var i = path.indexOf('/play');
window.history.pushState(null, '', path.substr(0, i+5));
}
function popState(e) {
if (e === null) {
@ -460,7 +430,7 @@ function PlaygroundOutput(el) {
if (running) running.Kill();
output.removeClass('error').text('Waiting for remote server...');
}
function run() {
function runOnly() {
loading();
running = transport.Run(
body(),
@ -468,13 +438,11 @@ function PlaygroundOutput(el) {
);
}
function fmt() {
function fmtAnd(run) {
loading();
var data = { body: body() };
if ($(opts.fmtImportEl).is(':checked')) {
data['imports'] = 'true';
}
$.ajax('https://play.golang.org/fmt', {
data['imports'] = 'true';
$.ajax('/_/fmt', {
data: data,
type: 'POST',
dataType: 'json',
@ -485,10 +453,33 @@ function PlaygroundOutput(el) {
setBody(data.Body);
setError('');
}
run();
},
});
}
function loadShare(id) {
$.ajax('/_/share?id='+id, {
processData: false,
type: 'GET',
complete: function(xhr) {
if(xhr.status != 200) {
setBody('Cannot load shared snippet; try again.');
return;
}
setBody(xhr.responseText);
},
})
}
function fmt() {
fmtAnd(function(){});
}
function run() {
fmtAnd(runOnly);
}
var shareURL; // jQuery element to show the shared URL.
var sharing = false; // true if there is a pending request.
var shareCallbacks = [];
@ -499,7 +490,7 @@ function PlaygroundOutput(el) {
sharing = true;
var sharingData = body();
$.ajax('https://play.golang.org/share', {
$.ajax('/_/share', {
processData: false,
data: sharingData,
type: 'POST',
@ -513,7 +504,7 @@ function PlaygroundOutput(el) {
if (opts.shareRedirect) {
window.location = opts.shareRedirect + xhr.responseText;
}
var path = '/p/' + xhr.responseText;
var path = '/play/p/' + xhr.responseText;
var url = origin(window.location) + path;
for (var i = 0; i < shareCallbacks.length; i++) {
@ -553,8 +544,24 @@ function PlaygroundOutput(el) {
});
}
var path = window.location.pathname;
var toyDisable = false;
if (path.startsWith('/go.dev/')) {
path = path.slice(7);
}
if (path.startsWith('/play/p/')) {
var id = path.slice(8);
id = id.replace(/\.go$/, "");
loadShare(id);
toyDisable = true;
}
if (opts.toysEl !== null) {
$(opts.toysEl).bind('change', function() {
if (toyDisable) {
toyDisable = false;
return;
}
var toy = $(this).val();
$.ajax('/doc/play/' + toy, {
processData: false,

Просмотреть файл

@ -0,0 +1,31 @@
// Copyright 2021 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.
// for /play; play.js is for embedded play widgets
window.addEventListener('DOMContentLoaded', () => {
// Set up playground if enabled.
if (window.playground) {
window.playground({
"codeEl": ".js-playgroundCodeEl",
"outputEl": ".js-playgroundOutputEl",
"runEl": ".js-playgroundRunEl",
"fmtEl": ".js-playgroundFmtEl",
"shareEl": ".js-playgroundShareEl",
"shareRedirect": "/play/p/",
"toysEl": ".js-playgroundToysEl",
'enableHistory': true,
'enableShortcuts': true,
'enableVet': true
});
// The pre matched below is added by the code above. Style it appropriately.
document.querySelector(".js-playgroundOutputEl pre").classList.add("Playground-output");
$('.js-playgroundToysEl').val("hello.go").trigger("change")
$('#code').linedtextarea();
$('#code').attr('wrap', 'off');
$('#code').resize(function() { $('#code').linedtextarea(); });
}
});

Просмотреть файл

@ -12,4 +12,4 @@
content: The Playground allows anyone with a web browser to write Go code
that we immediately compile, link, and run on our servers.
cta: Go to playground
url: https://play.golang.org
url: /play

Просмотреть файл

@ -8,7 +8,7 @@ main:
- name: Packages
url: https://pkg.go.dev
- name: Play
url: https://play.golang.org/
url: /play
- name: Blog
url: /blog/
@ -25,7 +25,7 @@ footer:
url: /learn/
children:
- name: Playground
url: https://play.golang.org
url: /play
- name: Tour
url: https://tour.golang.org
- name: Stack Overflow

120
go.dev/_content/play.tmpl Normal file
Просмотреть файл

@ -0,0 +1,120 @@
{{define "layout"}}
{{$canShare := not googleCN}}
<div class="PlayPage">
<div class="Playground-controls">
<h1 class="Playground-title">The Go Playground</h1>
<div class="Playground-buttons">
<button id="run" class="Button Button--primary js-playgroundRunEl Playground-runButton" title="Run this code [shift-enter]">Run</button>
<div class="Playground-secondaryButtons">
<button id="fmt" class="Button js-playgroundFmtEl" title="Format this code">Format</button>
{{- if $canShare}}
<button id="share" class="Button js-playgroundShareEl" title="Share this code">Share</button>
{{- end}}
</div>
<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>
</div>
<div class="Playground-inputContainer">
<div id="wrap">
<textarea id="code" name="code" class="Playground-input js-playgroundCodeEl" autocorrect="off" autocomplete="off" autocapitalize="off" spellcheck="false" aria-label="Try Go">{{if strings.Contains .URL "/play/p/"}}Loading share...
{{else}}// You can edit this code!
// Click here and start typing.
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
{{end}} </textarea>
</div>
</div>
<div class="Playground-outputContainer js-playgroundOutputEl">
<pre class="Playground-output"><noscript>Hello, 世界</noscript></pre>
</div>
<div class="PlayAbout">
<p><b>About the Playground</b></p>
<p>
The Go Playground is a web service that runs on
<a href="https://go.dev/">go.dev</a>'s servers.
The service receives a Go program, <a href="/cmd/vet/">vets</a>, compiles, links, and
runs the program inside a sandbox, then returns the output.
</p>
<p>
If the program contains <a href="/pkg/testing">tests or examples</a>
and no main function, the service runs the tests.
Benchmarks will likely not be supported since the program runs in a sandboxed
environment with limited resources.
</p>
<p>
There are limitations to the programs that can be run in the playground:
</p>
<ul>
<li>
The playground can use most of the standard library, with some exceptions.
The only communication a playground program has to the outside world
is by writing to standard output and standard error.
</li>
<li>
In the playground the time begins at 2009-11-10 23:00:00 UTC
(determining the significance of this date is an exercise for the reader).
This makes it easier to cache programs by giving them deterministic output.
</li>
<li>
There are also limits on execution time and on CPU and memory usage.
</li>
</ul>
<p>
The article "<a href="/playground" target="_blank" rel="noopener">Inside
the Go Playground</a>" describes how the playground is implemented.
The source code is available at <a href="https://go.googlesource.com/playground" target="_blank" rel="noopener">https://go.googlesource.com/playground</a>.
</p>
<p>
The playground uses the <a href="/play/p/Ztyu2FJaajl">latest stable release of Go</a>.
</p>
<p>
The playground service is used by more than just the official Go project
(<a href="https://gobyexample.com/">Go by Example</a> is one other instance)
and we are happy for you to use it on your own site.
All we ask is that you
<a href="mailto:golang-dev@googlegroups.com">contact us first (note this is a public mailing list)</a>,
that you use a unique user agent in your requests (so we can identify you),
and that your service is of benefit to the Go community.
</p>
<p>
Any requests for content removal should be directed to
<a href="mailto:security@golang.org">security@golang.org</a>.
Please include the URL and the reason for the request.
</p>
</div>
</div>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-11222381-7"></script>
<script src="/js/jquery-linedtextarea.js" defer></script>
<script src="/js/playsite.js" defer></script>
{{end}}

Просмотреть файл

@ -15,6 +15,8 @@ import (
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"time"
)
@ -38,13 +40,17 @@ type Event struct {
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
// RegisterHandlers registers handlers
// for golang.org/compile and golang.org/share on mux.
// If disallow is non-nil, then the share handler disallows requests
// for which disallowShare returns true.
func RegisterHandlers(mux *http.ServeMux, disallowShare func(*http.Request) bool) {
mux.HandleFunc("golang.org/compile", compile)
mux.HandleFunc("golang.org/share", share(disallowShare))
// RegisterHandlers registers handlers for the playground endpoints,
// proxying to the actual play.golang.org, so that we avoid cross-site requests
// in the browser.
func RegisterHandlers(mux *http.ServeMux) {
for _, host := range []string{"golang.org", "go.dev/_", "golang.google.cn"} {
mux.HandleFunc(host+"/compile", compile)
if host != "golang.google.cn" {
mux.HandleFunc(host+"/share", share)
}
mux.HandleFunc(host+"/fmt", fmtHandler)
}
}
func compile(w http.ResponseWriter, r *http.Request) {
@ -87,7 +93,7 @@ func compile(w http.ResponseWriter, r *http.Request) {
w.Write(b)
}
// makePlaygroundRequest sends the given Request to the playground compile
// makeCompileRequest sends the given Request to the playground compile
// endpoint and stores the response in the given Response.
func makeCompileRequest(ctx context.Context, req *Request, res *Response) error {
reqJ, err := json.Marshal(req)
@ -124,33 +130,44 @@ func flatten(seq []Event) string {
return buf.String()
}
func share(disallow func(*http.Request) bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if disallow != nil && disallow(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
var validID = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
// HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
// TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
req = req.WithContext(r.Context())
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ERROR share error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
copyHeader := func(k string) {
if v := resp.Header.Get(k); v != "" {
w.Header().Set(k, v)
}
}
copyHeader("Content-Type")
copyHeader("Content-Length")
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
func share(w http.ResponseWriter, r *http.Request) {
if id := r.FormValue("id"); r.Method == "GET" && validID.MatchString(id) {
simpleProxy(w, r, playgroundURL+"/p/"+id+".go")
return
}
simpleProxy(w, r, playgroundURL+"/share")
}
func fmtHandler(w http.ResponseWriter, r *http.Request) {
simpleProxy(w, r, playgroundURL+"/fmt")
}
func simpleProxy(w http.ResponseWriter, r *http.Request, url string) {
if r.Method == "GET" {
r.Body = nil
} else if len(r.Form) > 0 {
r.Body = io.NopCloser(strings.NewReader(r.Form.Encode()))
}
req, _ := http.NewRequest(r.Method, url, r.Body)
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
req = req.WithContext(r.Context())
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ERROR share error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
copyHeader := func(k string) {
if v := resp.Header.Get(k); v != "" {
w.Header().Set(k, v)
}
}
copyHeader("Content-Type")
copyHeader("Content-Length")
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}