Permalink
| package main | |
| import ( | |
| "crypto/sha1" | |
| "fmt" | |
| "github.com/russross/blackfriday" | |
| "io/ioutil" | |
| "net/http" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "regexp" | |
| "strings" | |
| "text/template" | |
| ) | |
| var cacheDir = "/tmp/gobyexample-cache" | |
| var siteDir = "./public" | |
| var pygmentizeBin = "./vendor/pygments/pygmentize" | |
| func check(err error) { | |
| if err != nil { | |
| panic(err) | |
| } | |
| } | |
| func ensureDir(dir string) { | |
| err := os.MkdirAll(dir, 0755) | |
| check(err) | |
| } | |
| func copyFile(src, dst string) { | |
| dat, err := ioutil.ReadFile(src) | |
| check(err) | |
| err = ioutil.WriteFile(dst, dat, 0644) | |
| check(err) | |
| } | |
| func pipe(bin string, arg []string, src string) []byte { | |
| cmd := exec.Command(bin, arg...) | |
| in, err := cmd.StdinPipe() | |
| check(err) | |
| out, err := cmd.StdoutPipe() | |
| check(err) | |
| err = cmd.Start() | |
| check(err) | |
| _, err = in.Write([]byte(src)) | |
| check(err) | |
| err = in.Close() | |
| check(err) | |
| bytes, err := ioutil.ReadAll(out) | |
| check(err) | |
| err = cmd.Wait() | |
| check(err) | |
| return bytes | |
| } | |
| func sha1Sum(s string) string { | |
| h := sha1.New() | |
| h.Write([]byte(s)) | |
| b := h.Sum(nil) | |
| return fmt.Sprintf("%x", b) | |
| } | |
| func mustReadFile(path string) string { | |
| bytes, err := ioutil.ReadFile(path) | |
| check(err) | |
| return string(bytes) | |
| } | |
| func cachedPygmentize(lex string, src string) string { | |
| ensureDir(cacheDir) | |
| arg := []string{"-l", lex, "-f", "html"} | |
| cachePath := cacheDir + "/pygmentize-" + strings.Join(arg, "-") + "-" + sha1Sum(src) | |
| cacheBytes, cacheErr := ioutil.ReadFile(cachePath) | |
| if cacheErr == nil { | |
| return string(cacheBytes) | |
| } | |
| renderBytes := pipe(pygmentizeBin, arg, src) | |
| writeErr := ioutil.WriteFile(cachePath, renderBytes, 0600) | |
| check(writeErr) | |
| return string(renderBytes) | |
| } | |
| func markdown(src string) string { | |
| return string(blackfriday.MarkdownCommon([]byte(src))) | |
| } | |
| func readLines(path string) []string { | |
| src := mustReadFile(path) | |
| return strings.Split(src, "\n") | |
| } | |
| func mustGlob(glob string) []string { | |
| paths, err := filepath.Glob(glob) | |
| check(err) | |
| return paths | |
| } | |
| func whichLexer(path string) string { | |
| if strings.HasSuffix(path, ".go") { | |
| return "go" | |
| } else if strings.HasSuffix(path, ".sh") { | |
| return "console" | |
| } | |
| panic("No lexer for " + path) | |
| return "" | |
| } | |
| func debug(msg string) { | |
| if os.Getenv("DEBUG") == "1" { | |
| fmt.Fprintln(os.Stderr, msg) | |
| } | |
| } | |
| var docsPat = regexp.MustCompile("^\\s*(\\/\\/|#)\\s") | |
| var todoPat = regexp.MustCompile("\\/\\/ todo: ") | |
| var dashPat = regexp.MustCompile("\\-+") | |
| type Seg struct { | |
| Docs, DocsRendered string | |
| Code, CodeRendered string | |
| CodeEmpty, CodeLeading, CodeRun bool | |
| } | |
| type Example struct { | |
| Id, Name string | |
| GoCode, GoCodeHash, UrlHash string | |
| Segs [][]*Seg | |
| NextExample *Example | |
| } | |
| func parseHashFile(sourcePath string) (string, string) { | |
| lines := readLines(sourcePath) | |
| return lines[0], lines[1] | |
| } | |
| func resetUrlHashFile(codehash, code, sourcePath string) string { | |
| payload := strings.NewReader(code) | |
| resp, err := http.Post("https://play.golang.org/share", "text/plain", payload) | |
| if err != nil { | |
| panic(err) | |
| } | |
| defer resp.Body.Close() | |
| body, err := ioutil.ReadAll(resp.Body) | |
| urlkey := string(body) | |
| data := fmt.Sprintf("%s\n%s\n", codehash, urlkey) | |
| ioutil.WriteFile(sourcePath, []byte(data), 0644) | |
| return urlkey | |
| } | |
| func parseSegs(sourcePath string) ([]*Seg, string) { | |
| lines := readLines(sourcePath) | |
| filecontent := strings.Join(lines, "\n") | |
| segs := []*Seg{} | |
| lastSeen := "" | |
| for _, line := range lines { | |
| if line == "" { | |
| lastSeen = "" | |
| continue | |
| } | |
| if todoPat.MatchString(line) { | |
| continue | |
| } | |
| matchDocs := docsPat.MatchString(line) | |
| matchCode := !matchDocs | |
| newDocs := (lastSeen == "") || ((lastSeen != "docs") && (segs[len(segs)-1].Docs != "")) | |
| newCode := (lastSeen == "") || ((lastSeen != "code") && (segs[len(segs)-1].Code != "")) | |
| if newDocs || newCode { | |
| debug("NEWSEG") | |
| } | |
| if matchDocs { | |
| trimmed := docsPat.ReplaceAllString(line, "") | |
| if newDocs { | |
| newSeg := Seg{Docs: trimmed, Code: ""} | |
| segs = append(segs, &newSeg) | |
| } else { | |
| segs[len(segs)-1].Docs = segs[len(segs)-1].Docs + "\n" + trimmed | |
| } | |
| debug("DOCS: " + line) | |
| lastSeen = "docs" | |
| } else if matchCode { | |
| if newCode { | |
| newSeg := Seg{Docs: "", Code: line} | |
| segs = append(segs, &newSeg) | |
| } else { | |
| segs[len(segs)-1].Code = segs[len(segs)-1].Code + "\n" + line | |
| } | |
| debug("CODE: " + line) | |
| lastSeen = "code" | |
| } | |
| } | |
| for i, seg := range segs { | |
| seg.CodeEmpty = (seg.Code == "") | |
| seg.CodeLeading = (i < (len(segs) - 1)) | |
| seg.CodeRun = strings.Contains(seg.Code, "package main") | |
| } | |
| return segs, filecontent | |
| } | |
| func parseAndRenderSegs(sourcePath string) ([]*Seg, string) { | |
| segs, filecontent := parseSegs(sourcePath) | |
| lexer := whichLexer(sourcePath) | |
| for _, seg := range segs { | |
| if seg.Docs != "" { | |
| seg.DocsRendered = markdown(seg.Docs) | |
| } | |
| if seg.Code != "" { | |
| seg.CodeRendered = cachedPygmentize(lexer, seg.Code) | |
| } | |
| } | |
| // we are only interested in the 'go' code to pass to play.golang.org | |
| if lexer != "go" { | |
| filecontent = "" | |
| } | |
| return segs, filecontent | |
| } | |
| func parseExamples() []*Example { | |
| exampleNames := readLines("examples.txt") | |
| examples := make([]*Example, 0) | |
| for _, exampleName := range exampleNames { | |
| if (exampleName != "") && !strings.HasPrefix(exampleName, "#") { | |
| example := Example{Name: exampleName} | |
| exampleId := strings.ToLower(exampleName) | |
| exampleId = strings.Replace(exampleId, " ", "-", -1) | |
| exampleId = strings.Replace(exampleId, "/", "-", -1) | |
| exampleId = strings.Replace(exampleId, "'", "", -1) | |
| exampleId = dashPat.ReplaceAllString(exampleId, "-") | |
| example.Id = exampleId | |
| example.Segs = make([][]*Seg, 0) | |
| sourcePaths := mustGlob("examples/" + exampleId + "/*") | |
| for _, sourcePath := range sourcePaths { | |
| if strings.HasSuffix(sourcePath, ".hash") { | |
| example.GoCodeHash, example.UrlHash = parseHashFile(sourcePath) | |
| } else { | |
| sourceSegs, filecontents := parseAndRenderSegs(sourcePath) | |
| if filecontents != "" { | |
| example.GoCode = filecontents | |
| } | |
| example.Segs = append(example.Segs, sourceSegs) | |
| } | |
| } | |
| newCodeHash := sha1Sum(example.GoCode) | |
| if example.GoCodeHash != newCodeHash { | |
| example.UrlHash = resetUrlHashFile(newCodeHash, example.GoCode, "examples/"+example.Id+"/"+example.Id+".hash") | |
| } | |
| examples = append(examples, &example) | |
| } | |
| } | |
| for i, example := range examples { | |
| if i < (len(examples) - 1) { | |
| example.NextExample = examples[i+1] | |
| } | |
| } | |
| return examples | |
| } | |
| func renderIndex(examples []*Example) { | |
| indexTmpl := template.New("index") | |
| _, err := indexTmpl.Parse(mustReadFile("templates/index.tmpl")) | |
| check(err) | |
| indexF, err := os.Create(siteDir + "/index.html") | |
| check(err) | |
| indexTmpl.Execute(indexF, examples) | |
| } | |
| func renderExamples(examples []*Example) { | |
| exampleTmpl := template.New("example") | |
| _, err := exampleTmpl.Parse(mustReadFile("templates/example.tmpl")) | |
| check(err) | |
| for _, example := range examples { | |
| exampleF, err := os.Create(siteDir + "/" + example.Id) | |
| check(err) | |
| exampleTmpl.Execute(exampleF, example) | |
| } | |
| } | |
| func main() { | |
| copyFile("templates/site.css", siteDir+"/site.css") | |
| copyFile("templates/favicon.ico", siteDir+"/favicon.ico") | |
| copyFile("templates/404.html", siteDir+"/404.html") | |
| copyFile("templates/play.png", siteDir+"/play.png") | |
| examples := parseExamples() | |
| renderIndex(examples) | |
| renderExamples(examples) | |
| } |