func (r *Repo) AddOrdering(path string, content []byte) { ordering := &Ordering{} err := goyaml.Unmarshal(content, ordering) if err != nil { log.Error("couldn't decode yaml data from %s: %s", path, err) return } if ordering.ID == 0 { log.Error("unable to decode ID from %s", path) return } r.Orderings[path] = ordering r.info = nil }
func (ctx *Context) IsAuthorised(repo *Repo) bool { auth := ctx.GetCookie("auth") if auth == "0" { return false } else if auth == "1" { return true } user := ctx.GetCookie("user") if user == "" { ctx.SetCookie("auth", "0") return false } resp, err := httpClient.Get("https://api.github.com/repos/" + repo.Path + "/collaborators/" + user) if err != nil { log.Error("couldn't do authorisation check for %q: %s", user, err) return false } defer resp.Body.Close() if resp.StatusCode != 204 { ctx.SetCookie("auth", "0") return false } ctx.SetCookie("auth", "1") return true }
func (s *LiveServer) HandleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err, closed := websocket.Upgrade(w, r, s.websocketOrigin, "") if err != nil { log.StandardError(err) if !closed { serve400(w, r) } return } logRequest(HTTP_WEBSOCKET, http.StatusOK, r) conn.SetReadMaxSize(1 << 20) reads := make(chan []byte, 1) quit := make(chan bool, 1) writes := make(chan []byte, 1) tick := time.NewTicker(pingInterval) go readWebSocket(conn, reads, quit) defer func() { tick.Stop() conn.Close() }() for { select { case <-tick.C: err = conn.WriteControl(websocket.OpPing, pingData, time.Now().Add(pingInterval)) if err != nil { log.Error("websocket: failed on ping: %s", err) return } case read := <-reads: writes <- read case write := <-writes: w, err := conn.NextWriter(websocket.OpText) if err != nil { log.Error("websocket: failed on NextWriter: %s", err) return } n, err := w.Write(write) w.Close() if n != len(write) || err != nil { log.Error("websocket: failed on write: %s", err) return } case <-quit: return } } }
func (r *Repo) Load(callGithub githubCallFunc) error { log.Info("loading repo: %s", r.Path) url := "https://github.com/" + r.Path + "/tarball/master" resp, err := httpClient.Get(url) if err != nil { log.StandardError(err) return err } defer resp.Body.Close() zf, err := gzip.NewReader(resp.Body) if err != nil { log.Error("couldn't find a valid repo tarball at %s -- %s", url, err) return err } tr := tar.NewReader(zf) r.Avatars = map[string]string{} r.Orderings = map[string]*Ordering{} r.Planfiles = map[string]*Planfile{} for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { log.Error("reading tarball: %s", err) return err } filename, ext := rsplit(hdr.Name, ".") _, filename = rsplit(filename, "/") if ext == "md" || ext == "order" { log.Info("parsing: %s", filename) data, err := ioutil.ReadAll(tr) if err != nil { log.Error("reading tarball file %q: %s", hdr.Name, err) continue } if ext == "md" { r.AddPlanfile(filename+".md", data, callGithub) } else { r.AddOrdering(filename+".order", data) } } } log.Info("successfully loaded repo: %s", r.Path) return nil }
func (ctx *Context) Error(s string, err error) { log.Error("%s: %s", s, err) if err == nil { fmt.Fprintf(ctx, "ERROR: %s", s) } else { fmt.Fprintf(ctx, "ERROR: %s: %s", s, err) } }
func (db *DB) Run(host string, port int, limit int64, master MasterController) error { if host == "" { host = runtime.GetIP() } db.id = getNodeID(host, port) db.limit = limit << 10 db.limitNotify = (db.limit * 2) / 3 addr, listener := runtime.GetAddrListener(host, port) defer listener.Close() go master.Run(db.id) log.Info("MatchDB running on %s", addr) delay := 1 * time.Millisecond maxDelay := 1 * time.Second for { conn, err := listener.Accept() if err != nil { if ne, ok := err.(net.Error); ok && ne.Temporary() { if delay == 0 { delay = 5 * time.Millisecond } else { delay *= 2 } if delay > maxDelay { delay = maxDelay } log.Error("Accept error: %v; retrying in %v", err, delay) time.Sleep(delay) continue } log.Error("Accept error: %v", err) return err } delay = 0 go db.Handle(conn) } }
func (r *Repo) AddPlanfile(path string, content []byte, callGithub githubCallFunc) { planfile, users, ok := ParsePlanfile(path, content) if !ok { return } for _, username := range users { if _, ok := r.Avatars[username]; !ok { user := &User{} err := callGithub("/users/"+username, user) if err == nil { r.Avatars[username] = user.AvatarURL } else { log.Error("couldn't load github user info for %q: %s", username, err) r.Avatars[username] = "https://assets.github.com/images/gravatars/gravatar-140.png" } } } r.Planfiles[path] = planfile r.info = nil if !planfile.Summary && planfile.ID > r.lastID { r.lastID = planfile.ID } }
func parseFile(path string, force bool) { dir, filename := filepath.Split(path) if !strings.HasSuffix(filename, ".go") { runtime.Error("%s does not look like a go file", filename) } log.Info("Parsing %s", path) fset := token.NewFileSet() pkg, err := parser.ParseFile(fset, path, nil, 0) if err != nil { runtime.StandardError(err) } newpath := filepath.Join(dir, fmt.Sprintf("%s_marshal.go", filename[:len(filename)-3])) exists, err := fsutil.FileExists(newpath) if exists && !force { runtime.Error("%s already exists! please specify --force to overwrite", newpath) } prev := "" models := []*model{} ast.Inspect(pkg, func(n ast.Node) bool { if s, ok := n.(*ast.StructType); ok { fields := []fieldInfo{} for _, field := range s.Fields.List { if field.Names == nil { continue } name := field.Names[0].Name dbName := "" kind := "" if field.Tag != nil { tag := field.Tag.Value[1 : len(field.Tag.Value)-1] if tag == "-" { continue } dbName = tag } if dbName == "" { dbName = name rune, _ := utf8.DecodeRuneInString(name) if !unicode.IsUpper(rune) { continue } } switch expr := field.Type.(type) { case *ast.Ident: switch expr.Name { case "bool", "string", "int", "int64", "uint", "uint64": kind = expr.Name } case *ast.ArrayType: if expr.Len == nil { // slice type switch iexpr := expr.Elt.(type) { case *ast.Ident: switch iexpr.Name { case "byte", "bool", "string", "int", "int64", "uint", "uint64": kind = "[]" + iexpr.Name } case *ast.ArrayType: if iexpr.Len == nil { if iiexpr, ok := iexpr.Elt.(*ast.Ident); ok { if iiexpr.Name == "byte" { kind = "[][]byte" } } } } } case *ast.SelectorExpr: if lexpr, ok := expr.X.(*ast.Ident); ok { if lexpr.Name == "time" && expr.Sel.Name == "Time" { kind = "time" } } } if kind == "" { log.Error("unsupported: %v field (%s.%s)", field.Type, prev, name) continue } fields = append(fields, fieldInfo{ dbName: dbName, kind: kind, name: name, }) } model := &model{ fields: fields, name: prev, } models = append(models, model) } switch x := n.(type) { case *ast.Ident: prev = x.Name } return true }) buf := &bytes.Buffer{} buf.Write(header) buf.Write([]byte(pkg.Name.Name)) buf.Write([]byte("\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"strconv\"\n\t\"time\"\n\t\"unicode/utf8\"\n)\n\n")) for _, model := range models { ref := strings.ToLower(string(model.name[0])) fmt.Fprintf(buf, "func (%s *%s) Encode(buf *bytes.Buffer) {\n", ref, model.name) last := len(model.fields) - 1 close := `{"` written := false for idx, field := range model.fields { dbKind, ok := kindMap[field.kind] if !ok { log.Error("unsupported kind: %s", field.kind) continue } prefix := `"` suffix := `"` if len(dbKind) == 2 { prefix = "[" suffix = "]" } open := fmt.Sprintf(`%s%s":{"%s":%s`, close, field.dbName, dbKind, prefix) comma := "," if idx == last { comma = "" } fmt.Fprintf(buf, "\tbuf.WriteString(`%s`)\n", open) close = fmt.Sprintf(`%s}%s"`, suffix, comma) written = true selector := fmt.Sprintf("%s.%s", ref, field.name) if len(dbKind) == 2 { fmt.Fprintf(buf, "\tfor idx, elem := range %s {\n", selector) fmt.Fprint(buf, "\t\tbuf.WriteByte('\"')\n") write(buf, "\t\t", field.kind[2:], "elem") fmt.Fprintf(buf, "\t\tif idx == len(%s)-1 {\n", selector) fmt.Fprint(buf, "\t\t\tbuf.WriteByte('\"')\n") fmt.Fprint(buf, "\t\t} else {\n") fmt.Fprint(buf, "\t\t\tbuf.WriteString(`\",`)\n") fmt.Fprint(buf, "\t\t}\n") fmt.Fprint(buf, "\t}\n") } else { write(buf, "\t", field.kind, selector) } } if written { fmt.Fprintf(buf, "\tbuf.WriteString(`%s}`)\n", close[:len(close)-1]) } fmt.Fprintf(buf, "}\n\n") fmt.Fprintf(buf, "func (%s *%s) Decode(data map[string]map[string]interface{}) {\n", ref, model.name) close = "" for _, field := range model.fields { dbKind, ok := kindMap[field.kind] if !ok { continue } selector := fmt.Sprintf("%s.%s", ref, field.name) if len(dbKind) == 2 { fmt.Fprintf(buf, "%s\tif vals, ok := data[\"%s\"][\"%s\"].([]interface{}); ok {\n", close, field.dbName, dbKind) fmt.Fprint(buf, "\t\tfor _, sval := range vals {\n") fmt.Fprint(buf, "\t\t\tval := sval.(string)\n") readMulti(buf, "\t\t\t", field.kind, selector) fmt.Fprint(buf, "\t\t}\n") } else { fmt.Fprintf(buf, "%s\tif val, ok := data[\"%s\"][\"%s\"].(string); ok {\n", close, field.dbName, dbKind) read(buf, "\t\t", field.kind, selector) } close = "\t}\n" } fmt.Fprintf(buf, "%s}\n\n", close) } buf.Write(jsonSupport) log.Info("Writing %s", newpath) newfile, err := os.Create(newpath) if err != nil { runtime.StandardError(err) } newfile.Write(buf.Bytes()) newfile.Close() }
func Error(format string, v ...interface{}) { log.Error(format, v...) Exit(1) }
func main() { opts := optparse.Parser("Usage: html2domly [options]", "v") outputFile := opts.String([]string{"-o", "--output"}, "../coffee/templates.coffee", "coffeescript file to compile to", "PATH") templatesSrcDir := opts.String([]string{"-i", "--input"}, "../etc/domly", "template source directory", "PATH") printJSON := opts.Bool([]string{"--print"}, false, "Print the JSON nicely to the output logger") os.Args[0] = "html2domly" opts.Parse(os.Args) log.AddConsoleLogger() var ( data []byte err error prettyStr string basename string pretty bytes.Buffer ) dir, err := os.Open(*templatesSrcDir) if err != nil { runtime.StandardError(err) } defer dir.Close() out, err := os.Create(*outputFile) if err != nil { runtime.StandardError(err) } defer out.Close() names, err := dir.Readdirnames(0) if err != nil { runtime.StandardError(err) } out.Write([]byte("define 'templates', (exports, root) ->\n")) for _, name := range names { if strings.HasSuffix(name, ".html") { basename = strings.TrimSuffix(name, ".html") } else { log.Error("file %v does not end in .html", name) continue } templatePath := filepath.Join(*templatesSrcDir, name) data, err = ui.ParseTemplate(templatePath) if err != nil { log.StandardError(err) } else { err := json.Indent(&pretty, data, ">", " ") if err != nil { log.StandardError(err) log.Info("%v", data) } else if *printJSON { prettyStr = pretty.String() pretty.Reset() log.Info("%v", prettyStr) } out.Write([]byte(fmt.Sprintf(" exports['%s'] = `%s`\n", basename, data))) log.Info("compiled '%s'", name) } } out.Write([]byte(" return")) outPath, _ := filepath.Abs(*outputFile) log.Info("compiled domly written to: %v", outPath) log.Wait() }
func main() { // Define the options for the command line and config file options parser. opts := optparse.New( "Usage: planfile <config.yaml> [options]\n", "planfile 0.0.1") cookieKeyFile := opts.StringConfig("cookie-key-file", "cookie.key", "the file containing the key to sign cookie values [cookie.key]") gaHost := opts.StringConfig("ga-host", "", "the google analytics hostname to use") gaID := opts.StringConfig("ga-id", "", "the google analytics id to use") httpAddr := opts.StringConfig("http-addr", ":8888", "the address to bind the http server [:8888]") oauthID := opts.Required().StringConfig("oauth-id", "", "the oauth client id for github") oauthSecret := opts.Required().StringConfig("oauth-secret", "", "the oauth client secret for github") redirectURL := opts.StringConfig("redirect-url", "/.oauth", "the redirect url for handling oauth [/.oauth]") repository := opts.Required().StringConfig("repository", "", "the username/repository on github") secureMode := opts.BoolConfig("secure-mode", "enable hsts and secure cookies [false]") title := opts.StringConfig("title", "Planfile", "the title for the web app [Planfile]") refreshKey := opts.StringConfig("refresh-key", "", "key for anonymously calling refresh at /.refresh?key=<refresh-key>") refreshOpt := opts.IntConfig("refresh-interval", 1, "the number of through-the-web edits before a full refresh [1]") debug, instanceDirectory, _, logPath, _ = runtime.DefaultOpts("planfile", opts, os.Args, true) service := &oauth.OAuthService{ ClientID: *oauthID, ClientSecret: *oauthSecret, Scope: "public_repo", AuthURL: "https://github.com/login/oauth/authorize", TokenURL: "https://github.com/login/oauth/access_token", RedirectURL: *redirectURL, AcceptHeader: "application/json", } assets := map[string]string{} json.Unmarshal(readFile("assets.json"), &assets) setupPygments() mutex := sync.RWMutex{} repo := &Repo{Path: *repository} err := repo.Load(callGithubAnon) if err != nil { runtime.Exit(1) } repo.Title = *title repo.Updated = time.Now().UTC() repoJSON, err := json.Marshal(repo) if err != nil { runtime.StandardError(err) } refreshCount := 0 refreshInterval := *refreshOpt refreshKeySet := *refreshKey != "" refreshKeyBytes := []byte(*refreshKey) secret := readFile(*cookieKeyFile) newContext := func(w http.ResponseWriter, r *http.Request) *Context { return &Context{ r: r, w: w, secret: secret, secure: *secureMode, } } register := func(path string, handler func(*Context), usegzip ...bool) { gzippable := len(usegzip) > 0 && usegzip[0] http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { log.Info("serving %s", r.URL) w.Header().Set("Content-Type", "text/html; charset=utf-8") if gzippable && httputil.Parse(r, "Accept-Encoding").Accepts("gzip") { buf := &bytes.Buffer{} enc := gzip.NewWriter(buf) handler(newContext(GzipWriter{enc, w}, r)) enc.Close() w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) buf.WriteTo(w) } else { handler(newContext(w, r)) } }) } anon := []byte(", null, null, '', false") authFalse := []byte("', false") authTrue := []byte("', true") header := []byte(`<!doctype html> <meta charset=utf-8> <title>` + html.EscapeString(*title) + `</title> <link href="//fonts.googleapis.com/css?family=Abel|Coustard:400" rel=stylesheet> <link href=/.static/` + assets["planfile.css"] + ` rel=stylesheet> <body><script>DATA = ['` + *gaHost + `', '` + *gaID + `', `) footer := []byte(`];</script> <script src=/.static/` + assets["planfile.js"] + `></script> <noscript>Sorry, your browser needs <a href=http://enable-javascript.com>JavaScript enabled</a>.</noscript> `) register("/", func(ctx *Context) { mutex.RLock() defer mutex.RUnlock() ctx.Write(header) ctx.Write(repoJSON) avatar := ctx.GetCookie("avatar") user := ctx.GetCookie("user") if avatar != "" && user != "" { ctx.Write([]byte(", '" + user + "', '" + avatar + "', '" + ctx.GetCookie("xsrf"))) if ctx.IsAuthorised(repo) { ctx.Write(authTrue) } else { ctx.Write(authFalse) } } else { ctx.Write(anon) } ctx.Write(footer) }, true) register("/.api", func(ctx *Context) { mutex.RLock() defer mutex.RUnlock() if cb := ctx.FormValue("callback"); cb != "" { ctx.Write([]byte(cb)) ctx.Write([]byte{'('}) ctx.Write(repoJSON) ctx.Write([]byte{')', ';'}) } else { ctx.Write(repoJSON) } }, true) register("/.login", func(ctx *Context) { b := make([]byte, 20) if n, err := rand.Read(b); err != nil || n != 20 { ctx.Error("Couldn't access cryptographic device", err) return } s := hex.EncodeToString(b) ctx.SetCookie("xsrf", s) ctx.Redirect(service.AuthCodeURL(s)) }) register("/.logout", func(ctx *Context) { ctx.ExpireCookie("auth") ctx.ExpireCookie("avatar") ctx.ExpireCookie("token") ctx.ExpireCookie("user") ctx.ExpireCookie("xsrf") ctx.Redirect("/") }) notAuthorised := []byte("ERROR: Not Authorised!") savedHeader := []byte(`<!doctype html> <meta charset=utf-8> <title>` + html.EscapeString(*title) + `</title> <body><script>SAVED="`) savedFooter := []byte(`"</script><script src=/.static/` + assets["planfile.js"] + `></script>`) exportRepo := func(ctx *Context) bool { repo.Updated = time.Now().UTC() repoJSON, err = json.Marshal(repo) if err != nil { ctx.Error("Couldn't encode repo data during refresh", err) return false } return true } refresh := func(ctx *Context) { err := repo.Load(ctx.CreateCallGithub()) if err != nil { log.Error("couldn't rebuild planfile info: %s", err) ctx.Write([]byte("ERROR: " + err.Error())) return } exportRepo(ctx) } saveItem := func(ctx *Context, update bool) { mutex.Lock() defer mutex.Unlock() if !ctx.IsAuthorised(repo) { ctx.Write(notAuthorised) return } if !isEqual([]byte(ctx.FormValue("xsrf")), []byte(ctx.GetCookie("xsrf"))) { ctx.Write(notAuthorised) return } callGithub := ctx.CreateCallGithub() err := repo.UpdateInfo(callGithub) if err != nil { ctx.Error("Couldn't update repo info", err) return } var id, path, message string if update { id = ctx.FormValue("id") path = ctx.FormValue("path") } else { baseID := ctx.FormValue("id") id = baseID count := 0 for repo.Exists(id + ".md") { count += 1 id = fmt.Sprintf("%s%d", baseID, count) } path = id + ".md" } content := strings.Replace(ctx.FormValue("content"), "\r\n", "\n", -1) tags := ctx.FormValue("tags") title := ctx.FormValue("title") redir := "/" if ctx.FormValue("summary") == "yes" { if id != "/" { content = fmt.Sprintf(`--- title: %s --- %s`, title, content) if strings.HasPrefix(id, "summary.") { redir = "/" + id[8:] } else { // Shouldn't ever happen. But just in case... redir = "/" + id } } } else { redir = "/.item." + id content = fmt.Sprintf(`--- id: %s tags: %s title: %s --- %s`, id, tags, title, content) } if title == "" { title = id } if update { message = "update: " + title + "." } else { message = "add: " + title + "." } log.Info("SAVE PATH: %q for %q", path, title) err = repo.Modify(ctx, path, content, message) if err != nil { if update { ctx.Error("<a href='/.refresh'>Try refreshing.</a> Couldn't update item", err) } else { ctx.Error("<a href='/.refresh'>Try refreshing.</a> Couldn't save new item", err) } return } refreshCount++ if refreshCount%refreshInterval == 0 { refresh(ctx) } else { repo.AddPlanfile(path, []byte(content), callGithub) if !exportRepo(ctx) { return } } ctx.Write(savedHeader) ctx.Write([]byte(html.EscapeString(redir))) ctx.Write(savedFooter) } register("/.modify", func(ctx *Context) { saveItem(ctx, true) }) register("/.new", func(ctx *Context) { saveItem(ctx, false) }) register("/.oauth", func(ctx *Context) { s := ctx.FormValue("state") if s == "" { ctx.Redirect("/.login") return } if !isEqual([]byte(s), []byte(ctx.GetCookie("xsrf"))) { ctx.ExpireCookie("xsrf") ctx.Redirect("/.login") return } t := &oauth.Transport{OAuthService: service} tok, err := t.ExchangeAuthorizationCode(ctx.FormValue("code")) if err != nil { ctx.Error("Auth Exchange Error", err) return } jtok, err := json.Marshal(tok) if err != nil { ctx.Error("Couldn't encode token", err) return } ctx.SetCookie("token", hex.EncodeToString(jtok)) ctx.token = tok user := &User{} err = ctx.Call("/user", user, nil, false) if err != nil { ctx.Error("Couldn't load user info", err) return } ctx.SetCookie("avatar", user.AvatarURL) ctx.SetCookie("user", user.Login) ctx.Redirect("/") }) register("/.preview", func(ctx *Context) { rendered, err := renderMarkdown([]byte(ctx.FormValue("content"))) if err != nil { ctx.Error("Couldn't render Markdown", err) return } ctx.Write(rendered) }, true) register("/.refresh", func(ctx *Context) { if !ctx.IsAuthorised(repo) { if !(refreshKeySet && isEqual(refreshKeyBytes, []byte(ctx.FormValue("key")))) { ctx.Write(notAuthorised) return } } mutex.Lock() defer mutex.Unlock() refresh(ctx) ctx.Redirect("/") }) mimetypes := map[string]string{ "css": "text/css", "gif": "image/gif", "ico": "image/x-icon", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript", "png": "image/png", "swf": "application/x-shockwave-flash", "txt": "text/plain", } registerStatic := func(filepath, urlpath string) { _, ext := rsplit(filepath, ".") ctype, ok := mimetypes[ext] if !ok { ctype = "application/octet-stream" } if debug { register(urlpath, func(ctx *Context) { ctx.SetHeader("Content-Type", ctype) ctx.Write(readFile(filepath)) }, strings.HasPrefix(ctype, "text/")) } else { content := readFile(filepath) register(urlpath, func(ctx *Context) { ctx.SetHeader("Cache-Control", "public, max-age=86400") ctx.SetHeader("Content-Type", ctype) ctx.Write(content) }, strings.HasPrefix(ctype, "text/")) } } for _, path := range assets { registerStatic(filepath.Join(instanceDirectory, "static", path), "/.static/"+path) } wwwPath := filepath.Join(instanceDirectory, "www") if files, err := ioutil.ReadDir(wwwPath); err == nil { for _, file := range files { if !file.IsDir() { registerStatic(filepath.Join(wwwPath, file.Name()), "/"+file.Name()) } } } log.Info("Listening on %s", *httpAddr) server := &http.Server{ Addr: *httpAddr, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } err = server.ListenAndServe() if err != nil { runtime.Error("couldn't bind to tcp socket: %s", err) } }
func ParsePlanfile(path string, content []byte) (p *Planfile, users []string, ok bool) { var ( metadata []byte seenID []byte ) if len(content) >= 4 && bytes.HasPrefix(content, tripleDash) { s := bytes.SplitN(content[4:], tripleDash, 2) if len(s) == 2 { metadata = s[0] content = bytes.TrimSpace(s[1]) } } p = &Planfile{ Content: string(content), Tags: []string{}, } if len(metadata) > 0 { for _, line := range bytes.Split(metadata, []byte{'\n'}) { kv := bytes.SplitN(line, []byte{':'}, 2) if len(kv) != 2 { continue } v := bytes.TrimSpace(kv[1]) if len(v) == 0 { continue } switch string(bytes.TrimSpace(kv[0])) { case "id": n, err := strconv.ParseUint(string(v), 10, 64) if err == nil { p.ID = n } else { seenID = v } case "tags": tags := []string{} for _, f := range bytes.Split(v, []byte{' '}) { for _, tag := range bytes.Split(f, []byte{','}) { if len(tag) >= 2 { tags = append(tags, string(tag)) } } } for _, tag := range tags { if tag[0] == '@' || tag[0] == '+' { users = append(users, strings.ToLower(tag[1:])) } else if tagUpper := strings.ToUpper(tag); tagUpper == tag { p.Status = tag } else { tag = strings.ToLower(tag) } if !contains(p.Tags, tag) { p.Tags = append(p.Tags, tag) } } case "title": p.Title = string(v) } } sort.StringSlice(p.Tags).Sort() } rendered, err := renderMarkdown(content) if err != nil { log.Error("couldn't render %s: %s", path, err) return } if strings.HasPrefix(path, "summary.") { split := strings.Split(path, ".") p.Handle = strings.Join(split[1:len(split)-1], ".") p.Summary = true } else if strings.ToLower(path) == "readme.md" { p.Handle = "/" p.Summary = true } else if p.Status == "" { p.Status = "TODO" p.Tags = append(p.Tags, "TODO") } if p.ID > 0 || p.Summary { ok = true } else { log.Error("invalid id for %s: %s", path, seenID) return } p.Rendered = string(rendered) return }