[x/blog] go.blog: use godoc design, use blog package from go.tools
Also delete go.blog/pkg/blog and unused static files. R=golang-dev, dsymonds https://golang.org/cl/13754043 X-Blog-Commit: a7251a3023bb3d78672ef1620be8de4151b3b41b
This commit is contained in:
Родитель
4d4e41fa2d
Коммит
ae8dc04163
|
@ -7,15 +7,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.blog/pkg/blog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.tools/godoc/blog"
|
||||
"code.google.com/p/go.tools/godoc/static"
|
||||
|
||||
_ "code.google.com/p/go.tools/godoc/playground"
|
||||
)
|
||||
|
||||
const hostname = "blog.golang.org" // default hostname for blog server
|
||||
|
||||
var config = &blog.Config{
|
||||
var config = blog.Config{
|
||||
Hostname: hostname,
|
||||
BaseURL: "http://" + hostname,
|
||||
GodocURL: "http://golang.org",
|
||||
HomeArticles: 5, // articles to display on the home page
|
||||
FeedArticles: 10, // articles to include in Atom and JSON feeds
|
||||
PlayEnabled: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Redirect "/blog/" to "/", because the menu bar link is to "/blog/"
|
||||
// but we're serving from the root.
|
||||
redirect := func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
http.HandleFunc("/blog", redirect)
|
||||
http.HandleFunc("/blog/", redirect)
|
||||
|
||||
http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/", http.HandlerFunc(staticHandler)))
|
||||
}
|
||||
|
||||
func staticHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Path
|
||||
b, ok := static.Files[name]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, name, time.Time{}, strings.NewReader(b))
|
||||
}
|
||||
|
|
|
@ -1,420 +0,0 @@
|
|||
// 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 blog implements a web server for articles written in present format.
|
||||
package blog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.blog/pkg/atom"
|
||||
"code.google.com/p/go.talks/pkg/present"
|
||||
|
||||
_ "code.google.com/p/go.tools/godoc/playground"
|
||||
)
|
||||
|
||||
var validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`)
|
||||
|
||||
// Config specifies Server configuration values.
|
||||
type Config struct {
|
||||
ContentPath string // Relative or absolute location of article files and related content.
|
||||
TemplatePath string // Relative or absolute location of template files.
|
||||
|
||||
BaseURL string // Absolute base URL (for permalinks; no trailing slash).
|
||||
BasePath string // Base URL path relative to server root (no trailing slash).
|
||||
HomeArticles int // Articles to display on the home page.
|
||||
|
||||
Hostname string // Server host name, used for rendering ATOM feeds.
|
||||
FeedArticles int // Articles to include in Atom and JSON feeds.
|
||||
}
|
||||
|
||||
// Doc represents an article adorned with presentation data.
|
||||
type Doc struct {
|
||||
*present.Doc
|
||||
Permalink string // Canonical URL for this document.
|
||||
Path string // Path relative to server root (including base).
|
||||
HTML template.HTML // rendered article
|
||||
|
||||
Related []*Doc
|
||||
Newer, Older *Doc
|
||||
}
|
||||
|
||||
// Server implements an http.Handler that serves blog articles.
|
||||
type Server struct {
|
||||
cfg *Config
|
||||
docs []*Doc
|
||||
tags []string
|
||||
docPaths map[string]*Doc // key is path without BasePath.
|
||||
docTags map[string][]*Doc
|
||||
template struct {
|
||||
home, index, article, doc *template.Template
|
||||
}
|
||||
atomFeed []byte // pre-rendered Atom feed
|
||||
jsonFeed []byte // pre-rendered JSON feed
|
||||
content http.Handler
|
||||
}
|
||||
|
||||
// NewServer constructs a new Server using the specified config.
|
||||
func NewServer(cfg *Config) (*Server, error) {
|
||||
present.PlayEnabled = true
|
||||
|
||||
root := filepath.Join(cfg.TemplatePath, "root.tmpl")
|
||||
parse := func(name string) (*template.Template, error) {
|
||||
t := template.New("").Funcs(funcMap)
|
||||
return t.ParseFiles(root, filepath.Join(cfg.TemplatePath, name))
|
||||
}
|
||||
|
||||
s := &Server{cfg: cfg}
|
||||
|
||||
// Parse templates.
|
||||
var err error
|
||||
s.template.home, err = parse("home.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.index, err = parse("index.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.article, err = parse("article.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := present.Template().Funcs(funcMap)
|
||||
s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load content.
|
||||
err = s.loadDocs(filepath.Clean(cfg.ContentPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderAtomFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderJSONFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set up content file server.
|
||||
s.content = http.FileServer(http.Dir(cfg.ContentPath))
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"sectioned": sectioned,
|
||||
"authors": authors,
|
||||
}
|
||||
|
||||
// sectioned returns true if the provided Doc contains more than one section.
|
||||
// This is used to control whether to display the table of contents and headings.
|
||||
func sectioned(d *present.Doc) bool {
|
||||
return len(d.Sections) > 1
|
||||
}
|
||||
|
||||
// authors returns a comma-separated list of author names.
|
||||
func authors(authors []present.Author) string {
|
||||
var b bytes.Buffer
|
||||
last := len(authors) - 1
|
||||
for i, a := range authors {
|
||||
if i > 0 {
|
||||
if i == last {
|
||||
b.WriteString(" and ")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
b.WriteString(authorName(a))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// authorName returns the first line of the Author text: the author's name.
|
||||
func authorName(a present.Author) string {
|
||||
el := a.TextElem()
|
||||
if len(el) == 0 {
|
||||
return ""
|
||||
}
|
||||
text, ok := el[0].(present.Text)
|
||||
if !ok || len(text.Lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
return text.Lines[0]
|
||||
}
|
||||
|
||||
// loadDocs reads all content from the provided file system root, renders all
|
||||
// the articles it finds, adds them to the Server's docs field, computes the
|
||||
// denormalized docPaths, docTags, and tags fields, and populates the various
|
||||
// helper fields (Next, Previous, Related) for each Doc.
|
||||
func (s *Server) loadDocs(root string) error {
|
||||
// Read content into docs field.
|
||||
const ext = ".article"
|
||||
fn := func(p string, info os.FileInfo, err error) error {
|
||||
if filepath.Ext(p) != ext {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := present.Parse(f, p, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html := new(bytes.Buffer)
|
||||
err = d.Render(html, s.template.doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p = p[len(root) : len(p)-len(ext)] // trim root and extension
|
||||
s.docs = append(s.docs, &Doc{
|
||||
Doc: d,
|
||||
Path: s.cfg.BasePath + p,
|
||||
Permalink: s.cfg.BaseURL + p,
|
||||
HTML: template.HTML(html.String()),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
err := filepath.Walk(root, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Sort(docsByTime(s.docs))
|
||||
|
||||
// Pull out doc paths and tags and put in reverse-associating maps.
|
||||
s.docPaths = make(map[string]*Doc)
|
||||
s.docTags = make(map[string][]*Doc)
|
||||
for _, d := range s.docs {
|
||||
s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d
|
||||
for _, t := range d.Tags {
|
||||
s.docTags[t] = append(s.docTags[t], d)
|
||||
}
|
||||
}
|
||||
|
||||
// Pull out unique sorted list of tags.
|
||||
for t := range s.docTags {
|
||||
s.tags = append(s.tags, t)
|
||||
}
|
||||
sort.Strings(s.tags)
|
||||
|
||||
// Set up presentation-related fields, Newer, Older, and Related.
|
||||
for _, doc := range s.docs {
|
||||
// Newer, Older: docs adjacent to doc
|
||||
for i := range s.docs {
|
||||
if s.docs[i] != doc {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
doc.Newer = s.docs[i-1]
|
||||
}
|
||||
if i+1 < len(s.docs) {
|
||||
doc.Older = s.docs[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Related: all docs that share tags with doc.
|
||||
related := make(map[*Doc]bool)
|
||||
for _, t := range doc.Tags {
|
||||
for _, d := range s.docTags[t] {
|
||||
if d != doc {
|
||||
related[d] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for d := range related {
|
||||
doc.Related = append(doc.Related, d)
|
||||
}
|
||||
sort.Sort(docsByTime(doc.Related))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderAtomFeed generates an XML Atom feed and stores it in the Server's
|
||||
// atomFeed field.
|
||||
func (s *Server) renderAtomFeed() error {
|
||||
var updated time.Time
|
||||
if len(s.docs) > 1 {
|
||||
updated = s.docs[0].Time
|
||||
}
|
||||
feed := atom.Feed{
|
||||
Title: "The Go Programming Language Blog",
|
||||
ID: "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname,
|
||||
Updated: atom.Time(updated),
|
||||
Link: []atom.Link{{
|
||||
Rel: "self",
|
||||
Href: s.cfg.BaseURL + "/feed.atom",
|
||||
}},
|
||||
}
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
e := &atom.Entry{
|
||||
Title: doc.Title,
|
||||
ID: feed.ID + doc.Path,
|
||||
Link: []atom.Link{{
|
||||
Rel: "alternate",
|
||||
Href: doc.Permalink,
|
||||
}},
|
||||
Published: atom.Time(doc.Time),
|
||||
Updated: atom.Time(doc.Time),
|
||||
Summary: &atom.Text{
|
||||
Type: "html",
|
||||
Body: summary(doc),
|
||||
},
|
||||
Content: &atom.Text{
|
||||
Type: "html",
|
||||
Body: string(doc.HTML),
|
||||
},
|
||||
Author: &atom.Person{
|
||||
Name: authors(doc.Authors),
|
||||
},
|
||||
}
|
||||
feed.Entry = append(feed.Entry, e)
|
||||
}
|
||||
data, err := xml.Marshal(&feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.atomFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonItem struct {
|
||||
Title string
|
||||
Link string
|
||||
Time time.Time
|
||||
Summary string
|
||||
Content string
|
||||
Author string
|
||||
}
|
||||
|
||||
// renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
|
||||
// field.
|
||||
func (s *Server) renderJSONFeed() error {
|
||||
var feed []jsonItem
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
item := jsonItem{
|
||||
Title: doc.Title,
|
||||
Link: doc.Permalink,
|
||||
Time: doc.Time,
|
||||
Summary: summary(doc),
|
||||
Content: string(doc.HTML),
|
||||
Author: authors(doc.Authors),
|
||||
}
|
||||
feed = append(feed, item)
|
||||
}
|
||||
data, err := json.Marshal(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.jsonFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// summary returns the first paragraph of text from the provided Doc.
|
||||
func summary(d *Doc) string {
|
||||
if len(d.Sections) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, elem := range d.Sections[0].Elem {
|
||||
text, ok := elem.(present.Text)
|
||||
if !ok || text.Pre {
|
||||
// skip everything but non-text elements
|
||||
continue
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, s := range text.Lines {
|
||||
buf.WriteString(string(present.Style(s)))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rootData encapsulates data destined for the root template.
|
||||
type rootData struct {
|
||||
Doc *Doc
|
||||
BasePath string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// ServeHTTP serves the front, index, and article pages
|
||||
// as well as the ATOM and JSON feeds.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
d = rootData{BasePath: s.cfg.BasePath}
|
||||
t *template.Template
|
||||
)
|
||||
switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p {
|
||||
case "/":
|
||||
d.Data = s.docs
|
||||
if len(s.docs) > s.cfg.HomeArticles {
|
||||
d.Data = s.docs[:s.cfg.HomeArticles]
|
||||
}
|
||||
t = s.template.home
|
||||
case "/index":
|
||||
d.Data = s.docs
|
||||
t = s.template.index
|
||||
case "/feed.atom", "/feeds/posts/default":
|
||||
w.Header().Set("Content-type", "application/atom+xml; charset=utf-8")
|
||||
w.Write(s.atomFeed)
|
||||
return
|
||||
case "/.json":
|
||||
if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) {
|
||||
w.Header().Set("Content-type", "application/javascript; charset=utf-8")
|
||||
fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||
w.Write(s.jsonFeed)
|
||||
return
|
||||
default:
|
||||
doc, ok := s.docPaths[p]
|
||||
if !ok {
|
||||
// Not a doc; try to just serve static content.
|
||||
s.content.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
d.Doc = doc
|
||||
t = s.template.article
|
||||
}
|
||||
err := t.ExecuteTemplate(w, "root", d)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// docsByTime implements sort.Interface, sorting Docs by their Time field.
|
||||
type docsByTime []*Doc
|
||||
|
||||
func (s docsByTime) Len() int { return len(s) }
|
||||
func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Двоичные данные
blog/static/logo.png
Двоичные данные
blog/static/logo.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 3.3 KiB |
|
@ -1,2 +0,0 @@
|
|||
These files are copies of those in code.google.com/p/go.talks/present/js.
|
||||
Please keep them in sync.
|
|
@ -1,99 +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.
|
||||
|
||||
function initPlayground(transport) {
|
||||
"use strict";
|
||||
|
||||
function text(node) {
|
||||
var s = "";
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var n = node.childNodes[i];
|
||||
if (n.nodeType === 1 && n.tagName === "SPAN" && n.className != "number") {
|
||||
var innerText = n.innerText === undefined ? "textContent" : "innerText";
|
||||
s += n[innerText] + "\n";
|
||||
continue;
|
||||
}
|
||||
if (n.nodeType === 1 && n.tagName !== "BUTTON") {
|
||||
s += text(n);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function init(code) {
|
||||
var output = document.createElement('div');
|
||||
var outpre = document.createElement('pre');
|
||||
var running;
|
||||
|
||||
if ($ && $(output).resizable) {
|
||||
$(output).resizable({
|
||||
handles: "n,w,nw",
|
||||
minHeight: 27,
|
||||
minWidth: 135,
|
||||
maxHeight: 608,
|
||||
maxWidth: 990
|
||||
});
|
||||
}
|
||||
|
||||
function onKill() {
|
||||
if (running) running.Kill();
|
||||
}
|
||||
|
||||
function onRun(e) {
|
||||
onKill();
|
||||
output.style.display = "block";
|
||||
outpre.innerHTML = "";
|
||||
run1.style.display = "none";
|
||||
var options = {Race: e.shiftKey};
|
||||
running = transport.Run(text(code), PlaygroundOutput(outpre), options);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
onKill();
|
||||
output.style.display = "none";
|
||||
run1.style.display = "inline-block";
|
||||
}
|
||||
|
||||
var run1 = document.createElement('button');
|
||||
run1.innerHTML = 'Run';
|
||||
run1.className = 'run';
|
||||
run1.addEventListener("click", onRun, false);
|
||||
var run2 = document.createElement('button');
|
||||
run2.className = 'run';
|
||||
run2.innerHTML = 'Run';
|
||||
run2.addEventListener("click", onRun, false);
|
||||
var kill = document.createElement('button');
|
||||
kill.className = 'kill';
|
||||
kill.innerHTML = 'Kill';
|
||||
kill.addEventListener("click", onKill, false);
|
||||
var close = document.createElement('button');
|
||||
close.className = 'close';
|
||||
close.innerHTML = 'Close';
|
||||
close.addEventListener("click", onClose, false);
|
||||
|
||||
var button = document.createElement('div');
|
||||
button.classList.add('buttons');
|
||||
button.appendChild(run1);
|
||||
// Hack to simulate insertAfter
|
||||
code.parentNode.insertBefore(button, code.nextSibling);
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.classList.add('buttons');
|
||||
buttons.appendChild(run2);
|
||||
buttons.appendChild(kill);
|
||||
buttons.appendChild(close);
|
||||
|
||||
output.classList.add('output');
|
||||
output.appendChild(buttons);
|
||||
output.appendChild(outpre);
|
||||
output.style.display = "none";
|
||||
code.parentNode.insertBefore(output, button.nextSibling);
|
||||
}
|
||||
|
||||
var play = document.querySelectorAll('div.playground');
|
||||
for (var i = 0; i < play.length; i++) {
|
||||
init(play[i]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,425 +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.
|
||||
|
||||
/*
|
||||
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 connnection).
|
||||
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) {
|
||||
// Append writes to
|
||||
}
|
||||
*/
|
||||
|
||||
function HTTPTransport() {
|
||||
'use strict';
|
||||
|
||||
// TODO(adg): support stderr
|
||||
|
||||
function playback(output, events) {
|
||||
var timeout;
|
||||
output({Kind: 'start'});
|
||||
function next() {
|
||||
if (events.length === 0) {
|
||||
output({Kind: 'end'});
|
||||
return;
|
||||
}
|
||||
var e = events.shift();
|
||||
if (e.Delay === 0) {
|
||||
output({Kind: 'stdout', Body: e.Message});
|
||||
next();
|
||||
return;
|
||||
}
|
||||
timeout = setTimeout(function() {
|
||||
output({Kind: 'stdout', 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'});
|
||||
}
|
||||
|
||||
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) {
|
||||
error(output, data.Errors);
|
||||
return;
|
||||
}
|
||||
playing = playback(output, data.Events);
|
||||
},
|
||||
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 = new WebSocket('ws://' + 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)
|
||||
// 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)
|
||||
function playground(opts) {
|
||||
var code = $(opts.codeEl);
|
||||
var transport = opts['transport'] || new HTTPTransport();
|
||||
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);
|
||||
}
|
||||
|
||||
function keyHandler(e) {
|
||||
if (e.keyCode == 9) { // tab
|
||||
insertTabs(1);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (e.keyCode == 13) { // enter
|
||||
if (e.shiftKey) { // +shift
|
||||
run();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} 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();
|
||||
$.ajax("/fmt", {
|
||||
data: {"body": body()},
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
if (data.Error) {
|
||||
setError(data.Error);
|
||||
} else {
|
||||
setBody(data.Body);
|
||||
setError("");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(opts.runEl).click(run);
|
||||
$(opts.fmtEl).click(fmt);
|
||||
|
||||
if (opts.shareEl !== null && (opts.shareURLEl !== null || opts.shareRedirect !== null)) {
|
||||
var shareURL;
|
||||
if (opts.shareURLEl) {
|
||||
shareURL = $(opts.shareURLEl).hide();
|
||||
}
|
||||
var sharing = false;
|
||||
$(opts.shareEl).click(function() {
|
||||
if (sharing) return;
|
||||
sharing = true;
|
||||
var sharingData = body();
|
||||
$.ajax("/share", {
|
||||
processData: false,
|
||||
data: sharingData,
|
||||
type: "POST",
|
||||
complete: function(xhr) {
|
||||
sharing = false;
|
||||
if (xhr.status != 200) {
|
||||
alert("Server error; try again.");
|
||||
return;
|
||||
}
|
||||
if (opts.shareRedirect) {
|
||||
window.location = opts.shareRedirect + xhr.responseText;
|
||||
}
|
||||
if (shareURL) {
|
||||
var path = "/p/" + xhr.responseText;
|
||||
var url = origin(window.location) + path;
|
||||
shareURL.show().val(url).focus().select();
|
||||
|
||||
if (rewriteHistory) {
|
||||
var historyData = {"code": sharingData};
|
||||
window.history.pushState(historyData, "", path);
|
||||
pushedEmpty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
|
@ -1,167 +0,0 @@
|
|||
/* Global */
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-weight: normal;
|
||||
}
|
||||
p, li {
|
||||
font-size: 10pt;
|
||||
}
|
||||
p {
|
||||
line-height: 1.6em;
|
||||
}
|
||||
li, pre {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
pre, code {
|
||||
font-size: 10pt;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
a {
|
||||
color: #5588aa;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
#container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
width: 800px;
|
||||
}
|
||||
#content {
|
||||
width: 550px;
|
||||
}
|
||||
#sidebar {
|
||||
width: 220px;
|
||||
float: right;
|
||||
}
|
||||
#footer {
|
||||
clear: both;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Heading */
|
||||
#heading {
|
||||
margin: 30px 0;
|
||||
font-size: 150%;
|
||||
font-weight: normal;
|
||||
}
|
||||
#heading a {
|
||||
color: black;
|
||||
}
|
||||
#heading img {
|
||||
margin-left: -30px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar h1 {
|
||||
font-size: 100%;
|
||||
}
|
||||
#sidebar li {
|
||||
list-style-type: none;
|
||||
}
|
||||
#sidebar ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
#content h1.title {
|
||||
font-size: 130%;
|
||||
}
|
||||
#content h1.title a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
#content h1.title a:hover {
|
||||
color: #5588aa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
#content h1, #content h2, #content h3, #content h4, #content h4 {
|
||||
font-size: 100%;
|
||||
}
|
||||
#content .date {
|
||||
text-transform: uppercase;
|
||||
color: #999;
|
||||
letter-spacing: .2em;
|
||||
font-size: 8pt;
|
||||
}
|
||||
#content .author {
|
||||
font-style: italic;
|
||||
}
|
||||
#content .article {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
#content img {
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
/* Code and playground */
|
||||
.code pre, .playground pre, .output pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.playground .number {
|
||||
color: #999;
|
||||
}
|
||||
.code, .playground, .output {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.code, .playground {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
.output {
|
||||
background: #202020;
|
||||
}
|
||||
.output .stdout, .output pre {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.output .stderr, .output .error {
|
||||
color: rgb(244, 74, 63);
|
||||
}
|
||||
.output .system, .output .exit {
|
||||
color: rgb(255, 209, 77)
|
||||
}
|
||||
.buttons {
|
||||
position: relative;
|
||||
float: right;
|
||||
top: -50px;
|
||||
right: 10px;
|
||||
}
|
||||
.output .buttons {
|
||||
top: -60px;
|
||||
right: 0;
|
||||
height: 0;
|
||||
}
|
||||
.buttons .kill {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Index */
|
||||
.tags {
|
||||
color: #ccc;
|
||||
font-size: smaller;
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
{{end}}
|
||||
|
||||
{{define "section"}}
|
||||
<h{{len .Number}} id="TOC_{{.FormattedNumber}}">{{.Title}}</h{{len .Number}}>
|
||||
<h4 id="TOC_{{.FormattedNumber}}">{{.Title}}</h4>
|
||||
{{range .Elem}}{{elem $.Template .}}{{end}}
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -6,32 +6,78 @@
|
|||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>{{template "title" .}}</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
||||
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
|
||||
<link rel="alternate" type="application/atom+xml" title="blog.golang.org - Atom Feed" href="http://blog.golang.org/feed.atom" />
|
||||
<script type="text/javascript">window.initFuncs = [];</script>
|
||||
<style>
|
||||
#sidebar {
|
||||
float: right;
|
||||
padding-left: 20px;
|
||||
width: 250px;
|
||||
background: white;
|
||||
}
|
||||
#sidebar p, #sidebar ul {
|
||||
margin: 20px 5px;
|
||||
}
|
||||
#sidebar ul {
|
||||
padding: 0;
|
||||
}
|
||||
#sidebar li {
|
||||
list-style-type: none;
|
||||
}
|
||||
#content .author {
|
||||
font-style: italic;
|
||||
}
|
||||
#content .article {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
#content .date {
|
||||
color: #999;
|
||||
}
|
||||
#content .tags {
|
||||
color: #999;
|
||||
font-size: smaller;
|
||||
}
|
||||
#content .iframe, #content .image {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="container">
|
||||
<div id="topbar"><div class="container">
|
||||
|
||||
<div id="heading">
|
||||
<a href="http://golang.org/"><img src="/static/logo.png"></a>
|
||||
<a href="{{.BasePath}}/">The Go Programming Language Blog</a>
|
||||
</div><!-- #heading -->
|
||||
<form method="GET" action="{{.GodocURL}}/search">
|
||||
<div id="menu">
|
||||
<a href="{{.GodocURL}}/doc/">Documents</a>
|
||||
<a href="{{.GodocURL}}/pkg/">Packages</a>
|
||||
<a href="{{.GodocURL}}/project/">The Project</a>
|
||||
<a href="{{.GodocURL}}/help/">Help</a>
|
||||
<a href="{{.GodocURL}}/blog/">Blog</a>
|
||||
<input type="text" id="search" name="q" class="inactive" value="Search" placeholder="Search">
|
||||
</div>
|
||||
<div id="heading"><a href="{{.GodocURL}}/">The Go Programming Language</a></div>
|
||||
</form>
|
||||
|
||||
</div></div>
|
||||
|
||||
<div id="page">
|
||||
<div class="container">
|
||||
|
||||
<div id="sidebar">
|
||||
{{with .Doc}}
|
||||
{{with .Newer}}
|
||||
<h1>Next article</h1>
|
||||
<h4>Next article</h4>
|
||||
<p><a href="{{.Path}}">{{.Title}}</a></p>
|
||||
{{end}}
|
||||
|
||||
{{with .Older}}
|
||||
<h1>Previous article</h1>
|
||||
<h4>Previous article</h4>
|
||||
<p><a href="{{.Path}}">{{.Title}}</a></p>
|
||||
{{end}}
|
||||
|
||||
{{with .Related}}
|
||||
<h1>Related articles</h1>
|
||||
<h4>Related articles</h4>
|
||||
<ul>
|
||||
{{range .}}
|
||||
<li><a href="{{.Path}}">{{.Title}}</a></li>
|
||||
|
@ -40,7 +86,7 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<h1>Links</h1>
|
||||
<h4>Links</h4>
|
||||
<ul>
|
||||
<li><a href='http://golang.org/'>golang.org</a></li>
|
||||
<li><a href='http://golang.org/doc/install.html'>Install Go</a></li>
|
||||
|
@ -52,7 +98,7 @@
|
|||
<li><a href='http://twitter.com/go_nuts'>Go on Twitter</a></li>
|
||||
</ul>
|
||||
|
||||
<h1>Keep up with Google Developers</h1>
|
||||
<h4>Keep up with Google Developers</h4>
|
||||
<ul>
|
||||
<li><a href='https://plus.google.com/+GoogleDevelopers/posts'>Google Developers +Page</a></li>
|
||||
<li><a href='https://developers.google.com/live/'>Google Developers Live</a></li>
|
||||
|
@ -60,7 +106,7 @@
|
|||
<li><a href='http://googledevelopers.blogspot.com/'>Google Developers Blog</a></li>
|
||||
</ul>
|
||||
|
||||
<h1>Blog Archive</h1>
|
||||
<h4>Blog Archive</h4>
|
||||
<p><a href="{{.BasePath}}/index">Article index</a></p>
|
||||
</div><!-- #sidebar -->
|
||||
|
||||
|
@ -80,12 +126,14 @@
|
|||
</p>
|
||||
</div><!-- #footer -->
|
||||
|
||||
</div><!-- #container -->
|
||||
</div><!-- .container -->
|
||||
</div><!-- #page -->
|
||||
|
||||
</body>
|
||||
<script src="/static/jquery.js"></script>
|
||||
<script src="/static/play/playground.js"></script>
|
||||
<script src="/static/play/play.js"></script>
|
||||
<script src="/lib/godoc/jquery.js"></script>
|
||||
<script src="/lib/godoc/playground.js"></script>
|
||||
<script src="/lib/godoc/play.js"></script>
|
||||
<script src="/lib/godoc/godocs.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
// Insert line numbers for all playground elements.
|
||||
|
@ -120,8 +168,8 @@ $(function() {
|
|||
|
||||
{{define "doc"}}
|
||||
<div class="article">
|
||||
<p class="date">{{.Time.Format "2 January 2006"}}</p>
|
||||
<h1 class="title"><a href="{{.Path}}">{{.Title}}</a></h1>
|
||||
<p class="date">{{.Time.Format "2 January 2006"}}</p>
|
||||
{{.HTML}}
|
||||
{{with .Authors}}
|
||||
<p class="author">By {{authors .}}</p>
|
||||
|
|
Загрузка…
Ссылка в новой задаче