Fork me on GitHub

2011/02/24


このエントリーをはてなブックマークに追加
新しい言語を始めて慣れてくると、必ず作りたくなるのが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 02:39 in ソフトウェア::lang::go
Tagged as: blosxom, golang
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip

2009/10/07


このエントリーをはてなブックマークに追加
blosxomにはnotfoundプラグインがあるので入れた。デフォルトのテンプレートのままだと見栄えが宜しくないので、"page.notfound"というファイルを作って各テンプレートファイルを引っ付けた物をベースに文言などを書いた。
見た目はこんな感じ。
Posted at 00:43 in その他
Tagged as: 404, blosxom, html
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip

2009/09/30


このエントリーをはてなブックマークに追加
ASINを指定すると、Amazonのアフィをヨロシク表示してくれるblosxomのプラグインでawsxomというのがあるのですが、以前それをItemSearchにも対応させ、以降何回か使ってました。ただ最近は記事を書く際に文章で頭が一杯になってしまい、毎回アフィを貼るのを忘れてしまうという難病にかかってしまったせいでawsxomをamazonの仕様変更に追従させるのを忘れてました。
で、案の定先ほどの記事をポストした際にASIN書いたら見事に記事が壊れて泣くハメに...

ええいと重い腰を上げて修正してみました。
修正方法はhail2uさんが書いた物をベースに修正しました。
あまりawxsomの原形を留めていないので修正後のファイルで...
#!/usr/bin/perl
# ---------------------------------------------------------------------
# awsxom: AWSからデータを取得して書影その他を作成(ECS v4対応版)
# Author: Fukazawa Tsuyoshi <tsuyoshi.fukazawa@gmail.com>
# Version: 2006-11-24
# http://fukaz55.main.jp/
# Modified: Yasuhiro Matsumoto <mattn.jp@gmail.com>
# ---------------------------------------------------------------------
package awsxom;

use strict;
use LWP::UserAgent;
use CGI qw/:standard/;
use FileHandle;
use URI::Escape;
use Digest::SHA::PurePerl qw(hmac_sha256_base64);

# --- Plug-in package variables --------
my $asoid = "XXXXXX-22";            # AmazonアソシエイトID
my $devkey = "XXXXXXXXXXXXXXXXXXXX";        # デベロッパートークン
my $secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";    # シークレットキー
my $cachedir = "$blosxom::plugin_state_dir/aws_cache";  # XMLのキャッシュ用ディレクトリ
my $EXPIRE = 24 * 7;                # データを再読込する間隔(単位:時間)
my $default_template = "awsxom";        # デフォルトのテンプレートファイル名

my $VERSION = '1.4';
my $ua_name = "awsxom $VERSION";
my $endpoint = "ecs.amazonaws.jp";
my $unsafe   = "^A-Za-z0-9\-_.~";
my $debug_mode = 0;

# ---------------------------------------------------------------------
sub start {
    # キャッシュ用ディレクトリの作成
    if (!-e $cachedir) {
        my $mkdir_r = mkdir($cachedir, 0755);
        warn $mkdir_r
        ? "blosxom : aws plugin > \$cachedir, $cachedir, created.\n"
        : "blosxom : aws plugin > mkdir missed:$!";
        $mkdir_r or return 0;
    }

    1;
}

# ---------------------------------------------------------------------
sub story {
    my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
    $$body_ref = conv($$body_ref);
    1;
}

# ---------------------------------------------------------------------
sub foot {
    my($pkg, $currentdir, $foot_ref) = @_;
    $$foot_ref = conv($$foot_ref);
    1;
}

# ---------------------------------------------------------------------
sub conv {
    $_ = shift;

    # ASIN/ISBNが書かれていたら置き換える
    # テンプレート指定版
    s/(?:ASIN|ISBN):([A-Z0-9]{10}):(.*?):/to_html_asin($1,$2)/ge;

    # テンプレート無指定版
    s/(?:ASIN|ISBN):([A-Z0-9]{10})/to_html_asin($1,$default_template)/ge;

    # テンプレート無指定版
    s/(?:AWSWORD):([a-zA-Z0-9_]*?):/to_html_word($1,$default_template)/ge;

    return $_;
}

# ---------------------------------------------------------------------
# ASINからAmazonのアフィリエイト用HTMLを作成
sub to_html_asin {
    my ($asin, $template) = @_;    # ASINとテンプレ名称
    my $cache = "$cachedir/$asin.xml";
    my $outfile = "$cachedir/$asin.html";

    my $q = CGI->new;
    $q->param("AWSAccessKeyId", $devkey);
    $q->param("AssociateTag", $asoid);
    $q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3],  $_[2], $_[1], $_[0] ) }->(gmtime(time))));
    $q->param("Service", "AWSECommerceService");
    $q->param("Operation", "ItemLookup");
    $q->param("ItemId", $asin);
    $q->param("ResponseGroup", "Medium,Offers");
    $q->param("Version", "2009-01-06");
    my @p = $q->param();
    foreach (@p) {
        $_ = escape($_) . "=" . escape($q->param($_));
    }
    my $qs = join("&", sort(@p));
    my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
    my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);

    # 取り込み直す必要はあるか?
    if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
    # AWSから情報を取得してキャッシュファイルに保存
        # UserAgent初期化
        my $ua = new LWP::UserAgent;
        $ua->agent($ua_name);
        $ua->timeout(60);
        my $rtn = $ua->mirror($url, $cache);
    }

    # キャッシュからXMLを読み込んで解析
    my $content = getFile($cache);
    my %detail = parseXML($content, $asin);

    # テンプレートを展開。エラーの場合はエラー文字列を返す
    my $form;
    if (!defined($detail{"ErrorMsg"})) {
        #$form = &$blosxom::template($blosxom::path, $template, 'html');
        my $fh = new FileHandle;
        if ($fh->open("< $blosxom::datadir/$template.html")) {
            $form = join '', <$fh>;
            $form =~ s/\$(\w+)/$detail{$1}/ge;
            $fh->close();
        }
    }
    else {
        $form = "<p>" . $detail{"ErrorMsg"} . "</p>";
    }

    return $form;
}

# ---------------------------------------------------------------------
# ASINからAmazonのアフィリエイト用HTMLを作成
sub to_html_word {
    my ($word, $template) = @_;    # ASINとテンプレ名称
    my $cache = "$cachedir/$word.xml";
    my $outfile = "$cachedir/$word.html";

    my $q = CGI->new;
    $q->param("AWSAccessKeyId", $devkey);
    $q->param("AssociateTag", $asoid);
    $q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3],  $_[2], $_[1], $_[0] ) }->(gmtime(time))));
    $q->param("Service", "AWSECommerceService");
    $q->param("Operation", "ItemSearch");
    $q->param("Keywords", $word);
    $q->param("SearchIndex", "Books");
    $q->param("ResponseGroup", "Medium,Offers");
    $q->param("Version", "2009-01-06");
    my @p = $q->param();
    foreach (@p) {
        $_ = escape($_) . "=" . escape($q->param($_));
    }
    my $qs = join("&", sort(@p));
    my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
    my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);

    # 取り込み直す必要はあるか?
    if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
    # AWSから情報を取得してキャッシュファイルに保存
        # UserAgent初期化
        my $ua = new LWP::UserAgent;
        $ua->agent($ua_name);
        $ua->timeout(60);
        my $rtn = $ua->mirror($url, $cache);
    }

    # キャッシュからXMLを読み込んで解析
    my $content = getFile($cache);
    $content =~ s!.*?(<Item>.*?</Item>).*!$1!is;
    my $asin = "";
    $asin = $1 if ($content =~ /<ASIN>([^<]*)<\/ASIN>/);
    return "" if !$asin;
    my %detail = parseXML($content, $asin);

    # テンプレートを展開。エラーの場合はエラー文字列を返す
    my $form;
    if (!defined($detail{"ErrorMsg"})) {
        #$form = &$blosxom::template($blosxom::path, $template, 'html');
        my $fh = new FileHandle;
        if ($fh->open("< $blosxom::datadir/$template.html")) {
            $form = join '', <$fh>;
            $form =~ s/\$(\w+)/$detail{$1}/ge;
            $fh->close();
        }
    }
    else {
        $form = "<p>" . $detail{"ErrorMsg"} . "</p>";
    }

    return $form;
}

# ---------------------------------------------------------------------
# ファイルを読み込む
sub getFile {
    my $cache = shift;
    my $fh = new FileHandle;

    $fh->open($cache);
    my @data = <$fh>;
    $fh->close();
    my $content = join('', @data);
    return undef if (!$content);

    return $content;
}

# ---------------------------------------------------------------------
sub parseXML {
    my ($buf, $asin) = @_;
    my %detail;

    # Amazonへのリンク
    $detail{"Link"} = "http://www.amazon.co.jp/exec/obidos/ASIN/$asin/ref=nosim/$asoid";

    # 個々の要素の抽出
    $detail{"Asin"} = $1 if ($buf =~ /<ASIN>([^<]*)<\/ASIN>/);
    $detail{"ProductName"} = $1 if ($buf =~ /<Title>([^<]*)<\/Title>/);
    $detail{"Catalog"} = $1 if ($buf =~ /<Binding>([^<]*)<\/Binding>/);
    $detail{"ReleaseDate"} = $1 if ($buf =~ /<PublicationDate>([^<]*)<\/PublicationDate>/);
    $detail{"ReleaseDate"} = $1 if ($buf =~ /<ReleaseDate>([^<]*)<\/ReleaseDate>/);
    $detail{"Manufacturer"} = $1 if ($buf =~ /<Manufacturer>([^<]*)<\/Manufacturer>/);
    $detail{"ImageUrlSmall"} = $1 if ($buf =~ /<SmallImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"ImageUrlMedium"} = $1 if ($buf =~ /<MediumImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"ImageUrlLarge"} = $1 if ($buf =~ /<LargeImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"Availability"} = $1 if ($buf =~ /<Availability>([^<]*)<\/Availability>/);
    $detail{"ListPrice"} = $1 if ($buf =~ /<LowestNewPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"OurPrice"} = $1 if ($buf =~ /<ListPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"UsedPrice"} = $1 if ($buf =~ /<LowestUsedPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"Author"} = $1 if ($buf =~ /<Author>([^<]*)<\/Author>/);
    # エラー?
    if ($buf =~ /<Errors>.*?<Message>([^<]*)<\/Message>/) {
        $detail{"ErrorMsg"} = $1;
    }

    return %detail;
}

# ---------------------------------------------------------------------
# デバッグ用
sub print_debug {
    return if (!$debug_mode);

    my $fd = new FileHandle;
    $fd->open("/path/to/log/output/directory/logfile.log", "a");
    print $fd "$_[0]";
    $fd->close();
}

sub escape {
  my $s = shift;

  $s =~ s/([^\0-\x7F])/do {
    my $o = ord($1);
    sprintf("%c%c", 0xc0 | ($o >> 6), 0x80 | ($o & 0x3f));
  }/ge;

  return uri_escape($s, $unsafe);
}

1;
ちなみに、オリジナルに追加した機能は「AWSWORD:perl」と書くとItemSearch結果の1番目を表示する...というモノグサ機能です。

Amazon Hacks 世界最大のショッピングサイト完全活用テクニック100選 Amazon Hacks 世界最大のショッピングサイト完全活用テクニック100選
ポール・ボシュ
オライリー・ジャパン / (2004-04-24)
 
発送可能時間:


モダンPerl入門 (CodeZine BOOKS) モダンPerl入門 (CodeZine BOOKS)
牧 大輔
翔泳社 / ¥ 2,940 (2009-02-10)
 
発送可能時間:在庫あり。

Posted at 00:50 in ソフトウェア::lang::perl
Tagged as: blosxom, perl
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip

2009/08/21


このエントリーをはてなブックマークに追加
テストも兼ねて...

追記
これだけだとなんなので...。
RSSもしくはAtomにrel="hub" href="http://pubsubhubbub.appspot.com"のlink要素を追加し、このpluginのhub_urlにRSSもしくはAtomのURLを指定すれば動きます。
Posted at 02:37 in ソフトウェア::lang::perl
Tagged as: blosxom, perl
Bookmarks: add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip