Commit 3fd5e0be authored by Andrew Gerrand's avatar Andrew Gerrand

godoc: make examples editable and runnable in playground

R=dsymonds
CC=golang-dev
https://golang.org/cl/6523045
parent 65dbe678
...@@ -10,16 +10,11 @@ ...@@ -10,16 +10,11 @@
// shareEl - share button element (optional) // shareEl - share button element (optional)
// shareURLEl - share URL text input element (optional) // shareURLEl - share URL text input element (optional)
// shareRedirect - base URL to redirect to on share (optional) // shareRedirect - base URL to redirect to on share (optional)
// preCompile - callback to mutate request data before compiling (optional) // enableHistory - enable using HTML5 history API (optional)
// postCompile - callback to read response data after compiling (optional)
// simple - use plain textarea instead of CodeMirror. (optional)
// toysEl - select element with a list of toys. (optional)
function playground(opts) { function playground(opts) {
var simple = opts['simple'];
var code = $(opts['codeEl']); var code = $(opts['codeEl']);
var editor;
// autoindent helpers for simple mode. // autoindent helpers.
function insertTabs(n) { function insertTabs(n) {
// find the selection start and end // find the selection start and end
var start = code[0].selectionStart; var start = code[0].selectionStart;
...@@ -49,12 +44,12 @@ function playground(opts) { ...@@ -49,12 +44,12 @@ function playground(opts) {
} }
} }
setTimeout(function() { setTimeout(function() {
insertTabs(tabs, 1); insertTabs(tabs);
}, 1); }, 1);
} }
function keyHandler(e) { function keyHandler(e) {
if (simple && e.keyCode == 9) { // tab if (e.keyCode == 9) { // tab
insertTabs(1); insertTabs(1);
e.preventDefault(); e.preventDefault();
return false; return false;
...@@ -64,58 +59,19 @@ function playground(opts) { ...@@ -64,58 +59,19 @@ function playground(opts) {
run(); run();
e.preventDefault(); e.preventDefault();
return false; return false;
} else if (simple) { } else {
autoindent(e.target); autoindent(e.target);
} }
} }
return true; return true;
} }
if (simple) {
code.unbind('keydown').bind('keydown', keyHandler); code.unbind('keydown').bind('keydown', keyHandler);
} else {
editor = CodeMirror.fromTextArea(
code[0],
{
lineNumbers: true,
indentUnit: 8,
indentWithTabs: true,
onKeyEvent: function(editor, e) { keyHandler(e); }
}
);
}
var output = $(opts['outputEl']); var output = $(opts['outputEl']);
function clearErrors() {
if (!editor) {
return;
}
var lines = editor.lineCount();
for (var i = 0; i < lines; i++) {
editor.setLineClass(i, null);
}
}
function highlightErrors(text) {
if (!editor) {
return;
}
var errorRe = /[a-z]+\.go:([0-9]+):/g;
var result;
while ((result = errorRe.exec(text)) != null) {
var line = result[1]*1-1;
editor.setLineClass(line, "errLine")
}
}
function body() { function body() {
if (editor) {
return editor.getValue();
}
return $(opts['codeEl']).val(); return $(opts['codeEl']).val();
} }
function setBody(text) { function setBody(text) {
if (editor) {
editor.setValue(text);
return;
}
$(opts['codeEl']).val(text); $(opts['codeEl']).val(text);
} }
function origin(href) { function origin(href) {
...@@ -128,22 +84,56 @@ function playground(opts) { ...@@ -128,22 +84,56 @@ function playground(opts) {
} }
function setOutput(text, error) { function setOutput(text, error) {
output.empty(); output.empty();
$(".lineerror").removeClass("lineerror");
if (error) { if (error) {
output.addClass("error"); output.addClass("error");
var regex = /prog.go:([0-9]+)/g;
var r;
while (r = regex.exec(text)) {
$(".lines div").eq(r[1]-1).addClass("lineerror");
}
} }
$("<pre/>").text(text).appendTo(output); $("<pre/>").text(text).appendTo(output);
} }
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)
}
var seq = 0; var seq = 0;
function run() { function run() {
clearErrors();
loading(); loading();
seq++; seq++;
var cur = seq; var cur = seq;
var data = {"body": body()}; var data = {"body": body()};
if (opts['preCompile']) {
opts['preCompile'](data);
}
$.ajax("/compile", { $.ajax("/compile", {
data: data, data: data,
type: "POST", type: "POST",
...@@ -152,15 +142,11 @@ function playground(opts) { ...@@ -152,15 +142,11 @@ function playground(opts) {
if (seq != cur) { if (seq != cur) {
return; return;
} }
if (opts['postCompile']) {
opts['postCompile'](data);
}
if (!data) { if (!data) {
return; return;
} }
if (data.compile_errors != "") { if (data.compile_errors != "") {
setOutput(data.compile_errors, true); setOutput(data.compile_errors, true);
highlightErrors(data.compile_errors);
return; return;
} }
var out = ""+data.output; var out = ""+data.output;
...@@ -174,12 +160,10 @@ function playground(opts) { ...@@ -174,12 +160,10 @@ function playground(opts) {
} }
setOutput(out, false); setOutput(out, false);
}, },
error: function(xhr) { error: function() {
var text = "Error communicating with remote server."; output.addClass("error").text(
if (xhr.status == 501) { "Error communicating with remote server."
text = xhr.responseText; );
}
output.addClass("error").text(text);
} }
}); });
} }
...@@ -194,7 +178,6 @@ function playground(opts) { ...@@ -194,7 +178,6 @@ function playground(opts) {
success: function(data) { success: function(data) {
if (data.Error) { if (data.Error) {
setOutput(data.Error, true); setOutput(data.Error, true);
highlightErrors(data.Error);
return; return;
} }
setBody(data.Body); setBody(data.Body);
...@@ -203,23 +186,6 @@ function playground(opts) { ...@@ -203,23 +186,6 @@ function playground(opts) {
}); });
}); });
$(opts['toysEl']).bind('change', function() {
var toy = $(this).val();
loading();
$.ajax("/doc/play/"+toy, {
processData: false,
type: "GET",
complete: function(xhr) {
if (xhr.status != 200) {
setOutput("Server error; try again.", true);
return;
}
setBody(xhr.responseText);
setOutput("", false);
}
});
});
if (opts['shareEl'] != null && (opts['shareURLEl'] != null || opts['shareRedirect'] != null)) { if (opts['shareEl'] != null && (opts['shareURLEl'] != null || opts['shareRedirect'] != null)) {
var shareURL; var shareURL;
if (opts['shareURLEl']) { if (opts['shareURLEl']) {
...@@ -229,16 +195,13 @@ function playground(opts) { ...@@ -229,16 +195,13 @@ function playground(opts) {
$(opts['shareEl']).click(function() { $(opts['shareEl']).click(function() {
if (sharing) return; if (sharing) return;
sharing = true; sharing = true;
var sharingData = body();
$.ajax("/share", { $.ajax("/share", {
processData: false, processData: false,
data: body(), data: sharingData,
type: "POST", type: "POST",
complete: function(xhr) { complete: function(xhr) {
sharing = false; sharing = false;
if (xhr.status == 501) {
alert(xhr.responseText);
return;
}
if (xhr.status != 200) { if (xhr.status != 200) {
alert("Server error; try again."); alert("Server error; try again.");
return; return;
...@@ -247,13 +210,20 @@ function playground(opts) { ...@@ -247,13 +210,20 @@ function playground(opts) {
window.location = opts['shareRedirect'] + xhr.responseText; window.location = opts['shareRedirect'] + xhr.responseText;
} }
if (shareURL) { if (shareURL) {
var url = origin(window.location) + "/p/" + xhr.responseText; var path = "/p/" + xhr.responseText
var url = origin(window.location) + path;
shareURL.show().val(url).focus().select(); shareURL.show().val(url).focus().select();
if (rewriteHistory) {
var historyData = {
"code": sharingData,
};
window.history.pushState(historyData, "", path);
pushedEmpty = false;
}
} }
} }
}); });
}); });
} }
return editor;
} }
...@@ -161,6 +161,7 @@ div#footer { ...@@ -161,6 +161,7 @@ div#footer {
div#menu > a, div#menu > a,
div#menu > input, div#menu > input,
div#learn .buttons a, div#learn .buttons a,
div.play .buttons a,
div#blog .read a { div#blog .read a {
padding: 10px; padding: 10px;
...@@ -181,6 +182,7 @@ div#menu > a { ...@@ -181,6 +182,7 @@ div#menu > a {
} }
a#start, a#start,
div#learn .buttons a, div#learn .buttons a,
div.play .buttons a,
div#blog .read a { div#blog .read a {
color: #222; color: #222;
border: 1px solid #375EAB; border: 1px solid #375EAB;
...@@ -391,3 +393,79 @@ img.gopher { ...@@ -391,3 +393,79 @@ img.gopher {
margin-bottom: -120px; margin-bottom: -120px;
} }
h2 { clear: right; } h2 { clear: right; }
div.play {
padding: 0 20px 40px 20px;
}
div.play pre,
div.play textarea,
div.play .lines {
padding: 0;
margin: 0;
font-family: Menlo, monospace;
font-size: 14px;
}
div.play .input {
padding: 10px;
margin-top: 10px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
overflow: hidden;
}
div.play .input textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
resize: none;
overflow: hidden;
}
div.play .output {
border-top: none !important;
padding: 10px;
max-height: 200px;
overflow: auto;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
-moz-border-radius-bottomright: 5px;
-moz-border-radius-bottomleft: 5px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
div.play .output pre {
padding: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
div.play .input,
div.play .input textarea,
div.play .output,
div.play .output pre {
background: #FFFFD8;
}
div.play .input,
div.play .output {
border: 1px solid #375EAB;
}
div.play .buttons {
float: right;
padding: 20px 0 10px 0;
text-align: right;
}
div.play .buttons a {
height: 16px;
margin-left: 5px;
padding: 10px;
cursor: pointer;
}
...@@ -5,11 +5,24 @@ ...@@ -5,11 +5,24 @@
<div class="expanded"> <div class="expanded">
<p class="exampleHeading toggleButton"><span class="text">Example{{example_suffix .Name}}</span></p> <p class="exampleHeading toggleButton"><span class="text">Example{{example_suffix .Name}}</span></p>
{{with .Doc}}<p>{{html .}}</p>{{end}} {{with .Doc}}<p>{{html .}}</p>{{end}}
{{$output := .Output}}
{{with .Play}}
<div class="play">
<div class="input"><textarea class="code">{{.}}</textarea></div>
<div class="output"><pre>{{html $output}}</pre></div>
<div class="buttons">
<a class="run" title="Run this code [shift-enter]">Run</a>
<a class="fmt" title="Format this code">Format</a>
<a class="share" title="Share this code">Share</a>
</div>
</div>
{{else}}
<p>Code:</p> <p>Code:</p>
<pre class="code">{{.Code}}</pre> <pre class="code">{{.Code}}</pre>
{{with .Output}} {{with .Output}}
<p>Output:</p> <p>Output:</p>
<pre class="output">{{html .}}</pre> <pre class="output">{{html .}}</pre>
{{end}} {{end}}
{{end}}
</div> </div>
</div> </div>
...@@ -222,3 +222,45 @@ ...@@ -222,3 +222,45 @@
<p>Need more packages? Take a look at the <a href="http://godashboard.appspot.com/">Go Project Dashboard</a>.</p> <p>Need more packages? Take a look at the <a href="http://godashboard.appspot.com/">Go Project Dashboard</a>.</p>
{{end}} {{end}}
{{end}} {{end}}
{{if $.Examples}}
<script type="text/javascript" src="/doc/play/playground.js"></script>
<script>
$(document).ready(function() {
'use strict';
// Set up playground when each element is toggled.
$('div.play').each(function (i, el) {
var built = false;
$(el).closest('.toggle').click(function() {
// Only set up playground once.
if (built) {
return;
}
built = true;
// Set up playground.
var code = $('.code', el);
playground({
'codeEl': code,
'outputEl': $('.output', el),
'runEl': $('.run', el),
'fmtEl': $('.fmt', el),
'shareEl': $('.share', el),
'shareRedirect': 'http://play.golang.org/p/'
});
// Make the code textarea resize to fit content.
var resize = function() {
code.height(0);
var h = code[0].scrollHeight;
code.height(h+20); // minimize bouncing
code.closest('.input').height(h);
};
code.on('keydown', resize);
code.on('keyup', resize);
code.keyup(); // resize now.
});
});
});
</script>
{{end}}
...@@ -62,6 +62,7 @@ var ( ...@@ -62,6 +62,7 @@ var (
tabwidth = flag.Int("tabwidth", 4, "tab width") tabwidth = flag.Int("tabwidth", 4, "tab width")
showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
templateDir = flag.String("templates", "", "directory containing alternate template files") templateDir = flag.String("templates", "", "directory containing alternate template files")
showPlayground = flag.Bool("play", false, "enable playground in web interface")
// search index // search index
indexEnabled = flag.Bool("index", false, "enable search index") indexEnabled = flag.Bool("index", false, "enable search index")
...@@ -320,8 +321,8 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File ...@@ -320,8 +321,8 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
for _, eg := range examples { for _, eg := range examples {
name := eg.Name name := eg.Name
// strip lowercase braz in Foo_braz or Foo_Bar_braz from name // Strip lowercase braz in Foo_braz or Foo_Bar_braz from name
// while keeping uppercase Braz in Foo_Braz // while keeping uppercase Braz in Foo_Braz.
if i := strings.LastIndex(name, "_"); i != -1 { if i := strings.LastIndex(name, "_"); i != -1 {
if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { if i < len(name)-1 && !startsWithUppercase(name[i+1:]) {
name = name[:i] name = name[:i]
...@@ -336,9 +337,11 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File ...@@ -336,9 +337,11 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
code := node_htmlFunc(cnode, fset) code := node_htmlFunc(cnode, fset)
out := eg.Output out := eg.Output
wholeFile := true
// additional formatting if this is a function body // Additional formatting if this is a function body.
if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
wholeFile = false
// remove surrounding braces // remove surrounding braces
code = code[1 : n-1] code = code[1 : n-1]
// unindent // unindent
...@@ -347,14 +350,29 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File ...@@ -347,14 +350,29 @@ func example_htmlFunc(funcName string, examples []*doc.Example, fset *token.File
if loc := exampleOutputRx.FindStringIndex(code); loc != nil { if loc := exampleOutputRx.FindStringIndex(code); loc != nil {
code = strings.TrimSpace(code[:loc[0]]) code = strings.TrimSpace(code[:loc[0]])
} }
}
// Write out the playground code in standard Go style
// (use tabs, no comment highlight, etc).
play := ""
if eg.Play != nil && *showPlayground {
var buf bytes.Buffer
err := (&printer.Config{Mode: printer.TabIndent, Tabwidth: 8}).Fprint(&buf, fset, eg.Play)
if err != nil {
log.Print(err)
} else { } else {
// drop output, as the output comment will appear in the code play = buf.String()
}
}
// Drop output, as the output comment will appear in the code.
if wholeFile && play == "" {
out = "" out = ""
} }
err := exampleHTML.Execute(&buf, struct { err := exampleHTML.Execute(&buf, struct {
Name, Doc, Code, Output string Name, Doc, Code, Play, Output string
}{eg.Name, eg.Doc, code, out}) }{eg.Name, eg.Doc, code, play, out})
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
......
...@@ -283,9 +283,13 @@ func main() { ...@@ -283,9 +283,13 @@ func main() {
registerPublicHandlers(http.DefaultServeMux) registerPublicHandlers(http.DefaultServeMux)
// Playground handlers are not available in local godoc. playHandler := disabledHandler
http.HandleFunc("/compile", disabledHandler) if *showPlayground {
http.HandleFunc("/share", disabledHandler) playHandler = bounceToPlayground
}
http.HandleFunc("/compile", playHandler)
http.HandleFunc("/share", playHandler)
http.HandleFunc("/fmt", playHandler)
// Initialize default directory tree with corresponding timestamp. // Initialize default directory tree with corresponding timestamp.
// (Do it in a goroutine so that launch is quick.) // (Do it in a goroutine so that launch is quick.)
...@@ -466,6 +470,22 @@ type httpWriter struct { ...@@ -466,6 +470,22 @@ type httpWriter struct {
func (w *httpWriter) Header() http.Header { return w.h } func (w *httpWriter) Header() http.Header { return w.h }
func (w *httpWriter) WriteHeader(code int) { w.code = code } func (w *httpWriter) WriteHeader(code int) { w.code = code }
// bounceToPlayground forwards the request to play.golang.org.
// TODO(adg): implement this stuff locally.
func bounceToPlayground(w http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
req.URL.Scheme = "http"
req.URL.Host = "play.golang.org"
resp, err := http.Post(req.URL.String(), req.Header.Get("Content-type"), req.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
resp.Body.Close()
}
// disabledHandler serves a 501 "Not Implemented" response. // disabledHandler serves a 501 "Not Implemented" response.
func disabledHandler(w http.ResponseWriter, r *http.Request) { func disabledHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment