diff --git a/TRANSLATE b/TRANSLATE new file mode 100644 index 0000000..d0b5586 --- /dev/null +++ b/TRANSLATE @@ -0,0 +1,35 @@ +Translating the Tour + +A Tour of Go is a Go program that runs as a stand-alone web server or +an App Engine app. The version available at tour.golang.org is run from +App Engine. There are several localized versions of the tour, such as +this Chinese translation, also running on App Engine: + + http://go-tour-zh.appspot.com + +The Tour contains a slide named "Go local", which lists several of +these translations. If you are a native speaker of a language not on +that list and have some experience with Go, please consider providing +a translation of the Tour in your own language. + +To translate the tour: + +1. Translate the contents of tour.article +2. Provide localized verison fo the UI strings in js/lang.js +3. Sign up to App Engine and create an app named go-tour-LL, + where LL is the two-letter country code that applies best + to your chosen language. (This shouldn't cost you anything; + the Tour App usually runs inside App Engine's free quota.) +4. Deploy your version of the Tour to that app. +5. Announce to golang-nuts@googlegroups.com + +The Tour content changes occasionally, and you should keep your +translation up to date. To follow the development of the tour, +subscribe to the go-tour-commits mailing list: + + https://groups.google.com/group/go-tour-commits + +All new commits to the go-tour repository are mailed there. + +Finally, if you have any questions about the Tour or Go, +please mail golang-nuts@googlegroups.com. diff --git a/appengine/app.yaml b/app.yaml similarity index 72% rename from appengine/app.yaml rename to app.yaml index 1311ef0..b06acf9 100644 --- a/appengine/app.yaml +++ b/app.yaml @@ -4,9 +4,6 @@ runtime: go api_version: go1 handlers: -- url: / - static_files: static/index.html - upload: static/index.html - url: /favicon.ico static_files: static/favicon.ico upload: static/favicon.ico @@ -14,5 +11,7 @@ handlers: static_dir: static - url: /talks static_dir: talks -- url: /(compile|fmt) +- url: /(|compile|fmt|script\.js) script: _go_app + +nobuild_files: (solutions|prog)/.* diff --git a/appengine/README b/appengine/README deleted file mode 100644 index d3616a0..0000000 --- a/appengine/README +++ /dev/null @@ -1,16 +0,0 @@ -This is the App Engine version of the Go Playground. - -To deploy: (instructions relative to the appengine directory) - -1. Make a copy of the static directory. - - cp -r ../static ../talks . - -2. Edit static/mode.js to set the tourMode variable to "appengine". - -3. Edit app.yaml to set the application name to something you have access to. - -4. Use appcfg.py to deploy it. - - /path/to/sdk/appcfg.py update . - diff --git a/appengine/goplay/compile.go b/appengine/goplay/compile.go deleted file mode 100644 index be1e3e7..0000000 --- a/appengine/goplay/compile.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2011 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. - -package goplay - -import ( - "fmt" - "io" - "net/http" - - "appengine" - "appengine/urlfetch" -) - -const runUrl = "http://golang.org/compile?output=json" - -func init() { - http.HandleFunc("/compile", compile) -} - -func compile(w http.ResponseWriter, r *http.Request) { - if err := passThru(w, r); err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintln(w, "Compile server error.") - } -} - -func passThru(w io.Writer, req *http.Request) error { - c := appengine.NewContext(req) - client := urlfetch.Client(c) - defer req.Body.Close() - r, err := client.Post(runUrl, req.Header.Get("Content-type"), req.Body) - if err != nil { - c.Errorf("making POST request:", err) - return err - } - defer r.Body.Close() - if _, err := io.Copy(w, r.Body); err != nil { - c.Errorf("copying response Body:", err) - return err - } - return nil -} diff --git a/appengine/goplay/fmt.go b/appengine/goplay/fmt.go deleted file mode 100644 index e49bc72..0000000 --- a/appengine/goplay/fmt.go +++ /dev/null @@ -1,50 +0,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. - -package goplay - -import ( - "bytes" - "encoding/json" - "go/ast" - "go/parser" - "go/printer" - "go/token" - "net/http" -) - -func init() { - http.HandleFunc("/fmt", fmtHandler) -} - -type fmtResponse struct { - Body string - Error string -} - -func fmtHandler(w http.ResponseWriter, r *http.Request) { - resp := new(fmtResponse) - body, err := gofmt(r.FormValue("body")) - if err != nil { - resp.Error = err.Error() - } else { - resp.Body = body - } - json.NewEncoder(w).Encode(resp) -} - -func gofmt(body string) (string, error) { - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "prog.go", body, parser.ParseComments) - if err != nil { - return "", err - } - ast.SortImports(fset, f) - var buf bytes.Buffer - err = printer.Fprint(&buf, fset, f) - if err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/gotour/appengine.go b/gotour/appengine.go new file mode 100644 index 0000000..a410989 --- /dev/null +++ b/gotour/appengine.go @@ -0,0 +1,103 @@ +// Copyright 2011 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. + +// +build appengine + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + + "appengine" + "appengine/urlfetch" +) + +const runUrl = "http://golang.org/compile" + +func init() { + http.HandleFunc("/", rootHandler) + http.HandleFunc("/compile", compileHandler) + err := serveScripts("js", "playground.js") + if err != nil { + panic(err) + } + if err := initTour("."); err != nil { + panic(err) + } +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + if err := renderTour(w); err != nil { + c.Criticalf("template render: %v", err) + } +} + +func compileHandler(w http.ResponseWriter, r *http.Request) { + if err := passThru(w, r); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "Compile server error.") + } +} + +func passThru(w io.Writer, req *http.Request) error { + c := appengine.NewContext(req) + client := urlfetch.Client(c) + defer req.Body.Close() + r, err := client.Post(runUrl, req.Header.Get("Content-type"), req.Body) + if err != nil { + c.Errorf("making POST request:", err) + return err + } + defer r.Body.Close() + if _, err := io.Copy(w, r.Body); err != nil { + c.Errorf("copying response Body:", err) + return err + } + return nil +} + +// prepContent returns a Reader that produces the content from the given +// Reader, but strips the prefix "#appengine: " from each line. It also drops +// any non-blank like that follows a series of 1 or more lines with the prefix. +func prepContent(in io.Reader) io.Reader { + var prefix = []byte("#appengine: ") + out, w := io.Pipe() + go func() { + r := bufio.NewReader(in) + drop := false + for { + b, err := r.ReadBytes('\n') + if err != nil && err != io.EOF { + w.CloseWithError(err) + return + } + if bytes.HasPrefix(b, prefix) { + b = b[len(prefix):] + drop = true + } else if drop { + if len(b) > 1 { + b = nil + } + drop = false + } + if len(b) > 0 { + w.Write(b) + } + if err == io.EOF { + w.Close() + return + } + } + }() + return out +} + +// socketAddr returns the WebSocket handler address. +// The App Engine version does not provide a WebSocket handler. +func socketAddr() string { return "" } diff --git a/gotour/goplay.go b/gotour/goplay.go deleted file mode 100644 index 8814def..0000000 --- a/gotour/goplay.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2010 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. - -package main - -import ( - "encoding/json" - "log" - "net/http" -) - -func init() { - http.HandleFunc("/compile", Compile) -} - -type Response struct { - Output string `json:"output"` - Errors string `json:"compile_errors"` -} - -func Compile(w http.ResponseWriter, req *http.Request) { - resp := new(Response) - out, err := compile(req) - if err != nil { - if len(out) > 0 { - resp.Errors = string(out) + "\n" + err.Error() - } else { - resp.Errors = err.Error() - } - } else { - resp.Output = string(out) - } - if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Println(err) - } -} diff --git a/gotour/local.go b/gotour/local.go index f42469c..d9a8be6 100644 --- a/gotour/local.go +++ b/gotour/local.go @@ -2,33 +2,37 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build !appengine + package main import ( - "bytes" "flag" "fmt" "go/build" - "io/ioutil" + "io" "log" "net" "net/http" "os" "os/exec" "path/filepath" - "regexp" "runtime" - "strconv" - "sync" + "strings" "time" + "code.google.com/p/go.talks/pkg/socket" + // Imports so that go build/install automatically installs them. _ "bitbucket.org/mikespook/go-tour-zh/pic" _ "bitbucket.org/mikespook/go-tour-zh/tree" _ "bitbucket.org/mikespook/go-tour-zh/wc" ) -const basePkg = "bitbucket.org/mikespook/go-tour-zh/" +const ( + basePkg = "bitbucket.org/mikespook/go-tour-zh/" + socketPath = "/socket" +) var ( httpListen = flag.String("http", "127.0.0.1:3999", "host:port to listen on") @@ -39,8 +43,34 @@ var ( var ( // a source of numbers, for naming temporary files uniq = make(chan int) + + // GOPATH containing the tour packages + gopath = os.Getenv("GOPATH") + + httpAddr string ) +func isRoot(path string) bool { + _, err := os.Stat(filepath.Join(path, "tour.article")) + return err == nil +} + +func findRoot() (string, error) { + ctx := build.Default + p, err := ctx.Import(basePkg, "", build.FindOnly) + if err == nil && isRoot(p.Dir) { + return p.Dir, nil + } + tourRoot := filepath.Join(runtime.GOROOT(), "misc", "tour") + ctx.GOPATH = tourRoot + p, err = ctx.Import(basePkg, "", build.FindOnly) + if err == nil && isRoot(tourRoot) { + gopath = tourRoot + return tourRoot, nil + } + return "", fmt.Errorf("could not find go-tour content; check $GOROOT and $GOPATH") +} + func main() { flag.Parse() @@ -52,23 +82,11 @@ func main() { }() // find and serve the go tour files - p, err := build.Default.Import(basePkg, "", build.FindOnly) + root, err := findRoot() if err != nil { log.Fatalf("Couldn't find tour files: %v", err) } - root := p.Dir log.Println("Serving content from", root) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/favicon.ico" || r.URL.Path == "/" { - fn := filepath.Join(root, "static", r.URL.Path[1:]) - http.ServeFile(w, r, fn) - return - } - http.Error(w, "not found", 404) - }) - http.Handle("/static/", http.FileServer(http.Dir(root))) - http.Handle("/talks/", http.FileServer(http.Dir(root))) - http.HandleFunc("/kill", kill) host, port, err := net.SplitHostPort(*httpListen) if err != nil { @@ -80,8 +98,34 @@ func main() { if host != "127.0.0.1" && host != "localhost" { log.Print(localhostWarning) } + httpAddr = host + ":" + port + + if err := initTour(root); err != nil { + log.Fatal(err) + } + + fs := http.FileServer(http.Dir(root)) + http.Handle("/favicon.ico", fs) + http.Handle("/static/", fs) + http.Handle("/talks/", fs) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + if err := renderTour(w); err != nil { + log.Println(err) + } + return + } + http.Error(w, "not found", 404) + }) + + http.Handle(socketPath, socket.Handler) + + err = serveScripts(filepath.Join(root, "js"), "socket.js") + if err != nil { + log.Fatal(err) + } - httpAddr := host + ":" + port go func() { url := "http://" + httpAddr if waitServer(url) && *openBrowser && startBrowser(url) { @@ -105,106 +149,26 @@ If you don't understand this message, hit Control-C to terminate this process. WARNING! WARNING! WARNING! ` -var running struct { - sync.Mutex - cmd *exec.Cmd +type response struct { + Output string `json:"output"` + Errors string `json:"compile_errors"` } -func stopRun() { - running.Lock() - if running.cmd != nil { - running.cmd.Process.Kill() - running.cmd = nil +// environ returns an execution environment containing only GO* variables +// and replacing GOPATH with the value of the global var gopath. +func environ() (env []string) { + for _, v := range os.Environ() { + if !strings.HasPrefix(v, "GO") { + continue } - running.Unlock() -} - -func kill(w http.ResponseWriter, r *http.Request) { - stopRun() + if strings.HasPrefix(v, "GOPATH=") { + v = "GOPATH=" + gopath } -var ( - commentRe = regexp.MustCompile(`(?m)^#.*\n`) - tmpdir string -) - -func init() { - // find real temporary directory (for rewriting filename in output) - var err error - tmpdir, err = filepath.EvalSymlinks(os.TempDir()) - if err != nil { - log.Fatal(err) - } + env = append(env, v) } -func compile(req *http.Request) (out []byte, err error) { - stopRun() - - // x is the base name for .go, .6, executable files - x := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq)) - src := x + ".go" - bin := x - if runtime.GOOS == "windows" { - bin += ".exe" - } - - // rewrite filename in error output - defer func() { - if err != nil { - // drop messages from the go tool like '# _/compile0' - out = commentRe.ReplaceAll(out, nil) - } - out = bytes.Replace(out, []byte(src+":"), []byte("main.go:"), -1) - }() - - // write body to x.go - body := []byte(req.FormValue("body")) - defer os.Remove(src) - if err = ioutil.WriteFile(src, body, 0666); err != nil { return - } - - // build x.go, creating x - dir, file := filepath.Split(src) - out, err = run(dir, "go", "build", "-o", bin, file) - defer os.Remove(bin) - if err != nil { - return - } - - // run x - return run("", bin) -} - -// run executes the specified command and returns its output and an error. -func run(dir string, args ...string) ([]byte, error) { - var buf bytes.Buffer - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = dir - cmd.Stdout = &buf - cmd.Stderr = cmd.Stdout - - // Start command and leave in 'running'. - running.Lock() - if running.cmd != nil { - defer running.Unlock() - return nil, fmt.Errorf("already running %s", running.cmd.Path) - } - if err := cmd.Start(); err != nil { - running.Unlock() - return nil, err - } - running.cmd = cmd - running.Unlock() - - // Wait for the command. Clean up, - err := cmd.Wait() - running.Lock() - if running.cmd == cmd { - running.cmd = nil - } - running.Unlock() - return buf.Bytes(), err } // waitServer waits some time for the http Server to start @@ -239,3 +203,9 @@ func startBrowser(url string) bool { cmd := exec.Command(args[0], append(args[1:], url)...) return cmd.Start() == nil } + +// prepContent for the local tour simply returns the content as-is. +func prepContent(r io.Reader) io.Reader { return r } + +// socketAddr returns the WebSocket handler address. +func socketAddr() string { return "ws://" + httpAddr + socketPath } diff --git a/gotour/tour.go b/gotour/tour.go new file mode 100644 index 0000000..180c193 --- /dev/null +++ b/gotour/tour.go @@ -0,0 +1,109 @@ +// Copyright 2013 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. + +package main + +import ( + "bytes" + "fmt" + "html/template" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "code.google.com/p/go.talks/pkg/present" +) + +func init() { + present.PlayEnabled = true +} + +var tourContent []byte + +// initTour loads tour.article and the relevant HTML templates from the given +// tour root, and renders the template to the tourContent global variable. +func initTour(root string) error { + // Open and parse source file. + source := filepath.Join(root, "tour.article") + f, err := os.Open(source) + if err != nil { + return err + } + defer f.Close() + doc, err := present.Parse(prepContent(f), source, 0) + if err != nil { + return err + } + + // Set up templates. + action := filepath.Join(root, "template", "action.tmpl") + tour := filepath.Join(root, "template", "tour.tmpl") + t := present.Template().Funcs(template.FuncMap{"nocode": nocode, "socketAddr": socketAddr}) + _, err = t.ParseFiles(action, tour) + if err != nil { + return err + } + + // Render. + buf := new(bytes.Buffer) + if err := doc.Render(buf, t); err != nil { + return err + } + tourContent = buf.Bytes() + return nil +} + +// renderTour writes the tour content to the provided Writer. +func renderTour(w io.Writer) error { + if tourContent == nil { + panic("renderTour called before successful initTour") + } + _, err := w.Write(tourContent) + return err +} + +// nocode returns true if the provided Section contains +// no Code elements with Play enabled. +func nocode(s present.Section) bool { + for _, e := range s.Elem { + if c, ok := e.(present.Code); ok && c.Play { + return false + } + } + return true +} + +var commonScripts = []string{ + "jquery.js", + "codemirror/lib/codemirror.js", + "codemirror/lib/go.js", + "lang.js", +} + +// serveScripts registers an HTTP handler at /script.js that serves a +// concatenated set of all the scripts specified by path relative to root. +func serveScripts(root string, path ...string) error { + modTime := time.Now() + var buf bytes.Buffer + scripts := append(commonScripts, path...) + scripts = append(scripts, "tour.js") + for _, p := range scripts { + fn := filepath.Join(root, p) + b, err := ioutil.ReadFile(fn) + if err != nil { + return err + } + fmt.Fprintf(&buf, "\n\n// **** %s ****\n\n", filepath.Base(fn)) + buf.Write(b) + } + b := buf.Bytes() + http.HandleFunc("/script.js", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-type", "application/javascript") + http.ServeContent(w, r, "", modTime, bytes.NewReader(b)) + }) + return nil +} diff --git a/static/codemirror/lib/codemirror.js b/js/codemirror/lib/codemirror.js similarity index 100% rename from static/codemirror/lib/codemirror.js rename to js/codemirror/lib/codemirror.js diff --git a/static/codemirror/lib/go.js b/js/codemirror/lib/go.js similarity index 100% rename from static/codemirror/lib/go.js rename to js/codemirror/lib/go.js diff --git a/static/jquery.js b/js/jquery.js similarity index 100% rename from static/jquery.js rename to js/jquery.js diff --git a/js/lang.js b/js/lang.js new file mode 100644 index 0000000..67a4067 --- /dev/null +++ b/js/lang.js @@ -0,0 +1,16 @@ +// Localized user interface. +var tr = { + "off": "关闭", + "on": "开启", + "syntax": "语法高亮", + "lineno": "行号", + "reset": "重置", + "format": "格式化代码", + "kill": "杀死进程", + "run": "运行", + "toc": "目录", + "prev": "向前", + "next": "向后", + "waiting": "等待远端服务器...", + "errcomm": "与远端服务器通讯异常。", +} diff --git a/js/playground.js b/js/playground.js new file mode 100644 index 0000000..a3d9539 --- /dev/null +++ b/js/playground.js @@ -0,0 +1,122 @@ +// 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. + +// HACK: This is file is identical to go.talks/present's playground.js except +// the lines containing "HACK". Also, the window.playground function is removed. + +(function() { + + function lineHighlight(error) { + // HACK: hook back into tour.js. + if (window.highlightErrors) window.highlightErrors(error); // HACK + } + + function connectPlayground() { + var playbackTimeout; + + function playback(pre, events) { + if (!pre.data("cleared")) pre.empty().data("cleared", true); // HACK + function show(msg) { + // ^L clears the screen. + var msgs = msg.split("\x0c"); + if (msgs.length == 1) { + pre.text(pre.text() + msg); + return; + } + pre.text(msgs.pop()); + } + function next() { + if (events.length === 0) { + var exit = $(''); + exit.text("\nProgram exited."); + exit.appendTo(pre); + return; + } + var e = events.shift(); + if (e.Delay === 0) { + show(e.Message); + next(); + } else { + playbackTimeout = setTimeout(function() { + show(e.Message); + next(); + }, e.Delay / 1000000); + } + } + next(); + } + + function stopPlayback() { + clearTimeout(playbackTimeout); + } + + function setOutput(output, events, error) { + stopPlayback(); + output.empty(); + + // Display errors. + if (error) { + lineHighlight(error); + output.addClass("error").text(error); + return; + } + + // Display image output. + if (events.length > 0 && events[0].Message.indexOf("IMAGE:") === 0) { + var out = ""; + for (var i = 0; i < events.length; i++) { + out += events[i].Message; + } + var url = "data:image/png;base64," + out.substr(6); + $("").attr("src", url).appendTo(output); + return; + } + + // Play back events. + if (events !== null) { + playback(output, events); + } + } + + var seq = 0; + function runFunc(body, output) { + output = $(output); + seq++; + var cur = seq; + var data = { + "version": 2, + "body": body + }; + $.ajax("/compile", { + data: data, + type: "POST", + dataType: "json", + success: function(data) { + if (seq != cur) { + return; + } + if (!data) { + return; + } + if (data.Errors) { + setOutput(output, null, data.Errors); + return; + } + setOutput(output, data.Events, false); + }, + error: function() { + output.addClass("error").text( + "Error communicating with remote server." + ); + } + }); + return stopPlayback; + } + + return runFunc; + } + + window.connectPlayground = connectPlayground; + +})(); diff --git a/js/socket.js b/js/socket.js new file mode 100644 index 0000000..24b209c --- /dev/null +++ b/js/socket.js @@ -0,0 +1,74 @@ +// 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. + +// HACK: This is file is identical to go.talks/present's socket.js except the +// lines containing "HACK". Please keep this file in sync. + +(function() { + "use strict"; + + var websocket, outputs = {}; + + function onClose() { + window.alert('websocket connection closed'); + } + + function sendMessage(m) { + websocket.send(JSON.stringify(m)); + } + + function onMessage(e) { + var m = JSON.parse(e.data); + var o = outputs[m.Id]; + if (o === null) { + return; + } + if (!$(o).data("cleared")) $(o).empty().data("cleared", true); // HACK + if (m.Kind === "stdout" || m.Kind === "stderr") { + showMessage(o, m.Body, m.Kind); + } + if (m.Kind === "end") { + var s = "Program exited"; + if (m.Body !== "") { + s += ": " + m.Body; + } else { + s += "."; + } + s += "\n"; + showMessage(o, s, "system"); + } + } + + function showMessage(o, m, className) { + // HACK: hook back into tour.js. + if (className == "stderr" && window.highlightErrors) { // HACK + window.highlightErrors(m); // HACK + } // HACK + var span = document.createElement("span"); + var needScroll = (o.scrollTop + o.offsetHeight) == o.scrollHeight; + m = m.replace(/&/g, "&"); + m = m.replace(/').insertBefore('#slides').hide(); $tocdiv.append($('
'+L('waiting')+'
');
+}
function run() {
- seq++;
- var cur = seq;
- $output.html('{{range .Lines}}{{.}}{{end}}+ {{else}} +
+ {{range $i, $l := .Lines}}{{if $i}}{{template "newline"}} + {{end}}{{style $l}}{{end}} +
+ {{end}} +{{end}} + +{{define "code"}} +