2011/02/24

Recent entries from same category

  1. net/url に JoinPath が入った。
  2. Go の struct は小さくできる(fieldalignment のススメ)
  3. 「エキスパートたちのGo言語」を読んだ。
  4. Go の http パッケージに MaxBytesHandler が入った。
  5. text/template と html/template に continue/break が入った。

新しい言語を始めて慣れてくると、必ず作りたくなるのがblosxomですね。(ですよね?)
て事でGoでblosxomライクなのを作った。cgiも出来なくないけど、GoはWebに強かったりもするので、web.goを使ってサーバとして書いた。
名前は「blogo」(ブロゴー!えっ)。blosxomライクにテキストファイルを読み込んで、タイトルと本文をこしらえます。
今回はHTMLのテンプレートエンジンとしてmustache(マツタケじゃないよ)を選びました。
結構ハマってしまったけど、一通り動いた。設定ファイル(JSON)でフォルダ位置やタイトル、サブタイトル、リンク一覧なんかを変更出来ます。見た目はこんな感じ。
blogo
短いのでソース全部のっける。
package main

import "bytes"
import "html"
import "io"
import "io/ioutil"
import "mustache"
import "os"
import pathutil "path"
import "regexp"
import "strings"
import "time"
import "web"
import "json"

type Tag struct {
    Name string
}

type Entry struct {
    Id       string
    Filename string
    Title    string
    Body     string
    Created  *time.Time
    Category string
    Author   string
    Tags     []Tag
}

func toTextChild(w io.Writer, n *html.Node) os.Error {
    switch n.Type {
    case html.ErrorNode:
        return os.NewError("unexpected ErrorNode")
    case html.DocumentNode:
        return os.NewError("unexpected DocumentNode")
    case html.ElementNode:
    case html.TextNode:
        w.Write([]byte(n.Data))
    case html.CommentNode:
        return os.NewError("COMMENT")
    default:
        return os.NewError("unknown node type")
    }
    for _, c := range n.Child {
        if err := toTextChild(w, c); err != nil {
            return err
        }
    }
    return nil
}

func toText(n *html.Node) (stringos.Error) {
    if n == nil || len(n.Child) == 0 {
        return ""nil
    }
    b := bytes.NewBuffer(nil)
    for _, child := range n.Child {
        if err := toTextChild(b, child); err != nil {
            return "", err
        }
    }
    return b.String(), nil
}

func GetEntry(filename string) (entry *Entry, err os.Error) {
    fi, err := os.Stat(filename)
    if err != nil {
        return nil, err
    }
    f, err := os.Open(filename, os.O_RDONLY, 0)
    if err != nil {
        return nil, err
    }
    b, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, err
    }

    in_body := false
    re, err := regexp.Compile("^meta-([a-zA-Z]+):[:space:]*(.*)$")
    if err != nil {
        return nil, err
    }
    for n, line := range strings.Split(string(b), "\n", -1) {
        line = strings.TrimSpace(line)
        if n == 0 {
            entry = new(Entry)
            entry.Title = line
            entry.Filename = pathutil.Clean(filename)
            entry.Tags = []Tag{}
            entry.Created = time.SecondsToUTC(fi.Ctime_ns / 1e9)
            continue
        }
        if n > 0 && len(line) == 0 {
            in_body = true
            continue
        }
        if in_body == false && re.MatchString(line) {
            submatch := re.FindStringSubmatch(line)
            if submatch[1] == "tags" {
                tags := strings.Split(submatch[2], ",", -1)
                entry.Tags = make([]Tag, len(tags))
                for i, t := range tags {
                    entry.Tags[i].Name = strings.TrimSpace(t)
                }
            }
            if submatch[1] == "author" {
                entry.Author = submatch[2]
            }
        } else {
            entry.Body += strings.Trim(line, "\r") + "\n"
        }
    }
    if entry == nil {
        err = os.NewError("invalid entry file")
    }
    return
}

type Entries []*Entry

func (p *Entries) VisitDir(path string, f *os.FileInfo) bool { return true }
func (p *Entries) VisitFile(path string, f *os.FileInfo) {
    if strings.ToLower(pathutil.Ext(path)) != ".txt" {
        return
    }
    if entry, err := GetEntry(path); err == nil {
        *p = append(*p, entry)
    }
}

func GetEntries(path string, useSummary bool) (entries *Entries, err os.Error) {
    entries = new(Entries)
    e := make(chan os.Error)
    pathutil.Walk(path, entries, e)
    for _, entry := range *entries {
        if useSummary {
            doc, err := html.Parse(strings.NewReader(entry.Body))
            if err == nil {
                if text, err := toText(doc); err == nil {
                    if len(text) > 500 {
                        text = text[0:500] + "..."
                    }
                    entry.Body = text
                }
            }
        }
        entry.Id = entry.Filename[len(path):len(entry.Filename)-3] + "html"
    }
    if len(*entries) == 0 {
        entries = nil
    }
    return
}

type Config map[string]interface{}

func (c *Config) Set(key string, val interface{}) {
    (*c)[key] = val
}

func (c *Config) Get(key stringstring {
    val, ok := (*c)[key].(string)
    if !ok {
        return ""
    }
    return val
}

func LoadConfig() (config *Config) {
    root, _ := pathutil.Split(pathutil.Clean(os.Args[0]))
    b, err := ioutil.ReadFile(pathutil.Join(root, "config.json"))
    if err != nil {
        println(err.String())
        return &Config{}
    }
    err = json.Unmarshal(b, &config)
    return
}

func main() {
    config := LoadConfig()
    web.Get("/(.*)"func(ctx *web.Context, path string) {
        config = LoadConfig()
        datadir := config.Get("datadir")
        if path == "" || path[len(path)-1] == '/' {
            dir := pathutil.Join(datadir, path)
            stat, err := os.Stat(dir)
            if err != nil || !stat.IsDirectory() {
                ctx.NotFound("File Not Found")
                return
            }
            var useSummary = false
            if config.Get("useSummary") != "" {
                useSummary = true
            }
            entries, err := GetEntries(dir, useSummary)
            if err == nil {
                ctx.WriteString(mustache.RenderFile("entries.mustache",
                    map[string]interface{}{
                        "config":  config,
                        "entries": entries}))
                return
            }
        } else if len(path) > 5 && path[len(path)-5:] == ".html" {
            file := pathutil.Join(datadir, path[:len(path)-5] + ".txt")
            _, err := os.Stat(file)
            if err != nil {
                ctx.NotFound("File Not Found")
                return
            }
            entry, err := GetEntry(file)
            if err == nil {
                ctx.WriteString(mustache.RenderFile("entry.mustache",
                    map[string]interface{}{
                        "config": config,
                        "entry":  entry}))
                return
            }
        }
        ctx.Abort(500"Server Error")
    })
    web.Config.RecoverPanic = false
    web.Config.StaticDir = config.Get("staticdir")
    web.Run(config.Get("host"))
}
それと、今回初めてCSS FrameworkであるBlueprintを使った。まぁ確かに慣れてると使いやすいのかもしれないけど、CSSそんなに知らない人が使い始められる物じゃないなと思った。Goのコード書くよりこのレイアウト作る方に時間がかかったという...
mattn/blogo - GitHub

blogo : blosxom like blog engine for golang

http://github.com/mattn/blogo
Posted at by | Edit