1.Amberの簡単な紹介
AmberはJade様式でタグを書かずにテンプレートを記述できます。同じような形式で、Goから直接データを渡せるものとして yosssi/aceもありますが、私はマクロ的なものが欲しかったのでamberを使ってみました。
例:amberファイル
mixin tableLine2($u1, $u2, $u3, $u4, $str2, $str3)
tr
td[rowspan="2"] #{$u1}
td #{$u2}
td #{$u3}
td[rowspan="2"] #{$u4}
tr
td #{$str2}
td #{$str3}
//--------------------------------------------------
doctype 5
html
head
meta[charset="utf-8"][content="text/css"]
link[rel="stylesheet"][href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css"]
//link[rel="stylesheet"][href="amber/0.6.0/pure-min.css"]
style[type="text/css"]
table { width: 300px; text-align: center; }
th,td { border: solid 2px #000000; }
div.wrap { padding: 50px; }
body
div.wrap
table
tbody
+tableLine2("a1", "a2", "a3", "a4", "b1", "b2")
上記では Pure( http://purecss.io/ )をリンクしてますが、意味ありません。メモ代わりにローカルファイルへのリンク方法を書いただけです。
Amberでは、amber形式のファイルをコンパイルした出力を標準ライブラリの template.Execute()に渡します。主な特徴は、
- テンプレート内変数、Goデータの展開(バインディング)
- 継承、mixin、import
- if/else 、イテレーション、簡単な式(+,-,*,%,/,==,>,<,>=,<=,&&,||,!)
- ディレクトリー内amberファイルの一括テンプレート化
- 標準ライブラリにあるようなパイプ機能なし
他にもあるみたいですが、よくわからないので省略しました。詳細は https://github.com/eknkc/amber をご覧ください。継承(extends)については、Jadeについて書かれた(例題の構造がちょっと違ってますが) https://gist.github.com/japboy/5402844 がわかりやすいです。あと、既存のhtmlをこれ流の形式にするには、htmlを jade形式に変換するサービスを利用すると少し楽できます。
2.メモ
1. インデントは全て空白か、全てタブかのどちらかに揃えておくのが吉です。たとえ<script>や<style>ブロック内の記述であっても、ブロックの終了を見るためと思いますが、インデント量をちゃんとしておかないとエラーになります。
2. <script>や<style>ブロック内では、javascriptやcss定義を直接記述できます。従ってその中では import や mixinなどは使えません。
3. mixinの2重定義は、後で定義したものが有効のようです。
4. mixin の記述は定義時もコール時も空白に要注意
mixin macro($p1) // "mixin"と"macro"の間は1文字の空白
mixin macro($p1, $p2, $p3) // パラメータ記述時、および呼び出し時の
+macro(pr1) // 空白パターンにも注意
+macro(pr1, pr2, pr3)
5. mixin のパラメータとして id や class を渡すときは、
mixin divx($id, $class, $text)
div[id=$id][class=$class] #{$text}
とする。
mixin divx($id, $class, $text)
✕ div#$id.$class #{$text}
とか
mixin divx($id, $class, $text)
✕ div##{$id}.#{$class} #{$text}
とはできないみたいです。
6. importは GOPATH、GOROOT等は見てないです。
7. importは、ネストの2段目以降はインデントを解釈してないようで、mixinの定義ファイルみたいなのならいいですが、直接展開するためのタグ群のネストは難ありでしょう。
8. import/extendsするファイル名に拡張子がないと ".amber"が付加されます。
9. import/extendsは、参照の循環?なので無限ループするかもです。
10. "tag text" の形は、tagによっては textが無視されることがあります。その場合は、
tag
| text
とします。
11. イテレーションのブロックでは、スコープがその要素に限定されるようです。たとえば、
type Hobby struct {
Code int
Name string
}
type Person struct {
FirstName string
Hobbies []Hobby
}
・・・
data := Person{ FirstName:"Goちゃん", Hobbies: []Hobby{
{ Code:10, Name:"プログラミング" },
{ Code:11, Name:"歩くこと" },
} }
・・・
tmpl.Execute( w,data )
・・・
amberファイル
div
each $i,$hobby in $.Hobbies
p #{$.FirstName} : #{$hobby.Name}
これは FirstNameが見えないためエラーとなります。問題のデータを繰り返しデータの中に入れておくか、あるいは次のように、事前に変数に入れれば参照できます。
div
$saved = $.FirstName
each $i,$hobby in $.Hobbies
p #{$saved} : #{$hobby.Name}
上記では $hoby.Name
としていますが、$.Name
でも同じです。出力は次のようになります。
<div>
<p> Goちゃん : プログラミング</p>
<p> Goちゃん : 歩くこと</p>
</div>
3.おまけ (ambファイル)
この手のものは見た目こそが重要と私は思いますが、amber形式は少し見づらい気がしたので、amberファイルならぬ、ambファイルというのを書いて、そこから amberファイルを生成することにしました。いろいろな制限や欠陥もあり全くもって個人的用途なのですが、それでも同じような事をしたい方の参考になればと思います。
1. タグ記法追加(一行以内のこと)
(a) input#id:: type="radio",, value="option1",, checked
---> input#id[type="radio"][value="option1"][checked]
(b) a.menu-link:: href="http://localhost:8010/", テキスト
---> a.menu-link[href="http://localhost:8010/"] テキスト
(c) mixin divx($id, $class, $text)
div.cls-x:: id=$id,, class=$class, #{$text}
---> mixin divx($id, $class, $text)
div.cls-x[id=$id][class=$class] #{$text}
★上記は次の変換に従います(ブランクに注意)
":: " ---> "["
",, " ---> "]["
", " ---> "]" または "["
必要なら最後に "]" を付加
★上記(b),(c)で、 最後の "," のあとにブランク2つ あることに注意
2. 行コメント記法追加
行頭の "///" は行の完全削除
行途中の "//-" は行末まで削除
3. 定数回繰り返し記法追加
each$$ 10 ・・・"記述群"中の文字列 $$ を 0~9に置き換えて計10回反復
記述群 "記述群"中での 空行・import・extends・include等不可。
インデントのタブ、ブランク 混在不可。255行以下。
each$$ 3,8 ・・・$$=3,4,5,6,7 計5回反復。 ","の前後にブランクを入れ
記述群 てはならない。
例:
ambファイル(ソース)
mixin month_select($id, $selected)
div
select.mon-slct:: name="month",, id=$id+"-chk"
each$$ 1,7
if $selected == $$
option:: value=$id + "-$$",, selected, $$
else
option:: value=$id + "-$$", $$
+month_select("month", "2")
変換後
<div>
<select class="mon-slct" id="month-chk" name="month">
<option value="month-1">1</option>
<option selected value="month-2">2</option>
<option value="month-3">3</option>
<option value="month-4">4</option>
<option value="month-5">5</option>
<option value="month-6">6</option>
</select>
</div>
4. 使用例
「6. ソースリスト」の後方もご参照ください。
func renderTempl( w http.ResponseWriter, ambfile string, data interface{} ) {
amberfile,_ := MakeAmberFile(ambfile, "") // ambファイル --->amberファイル生成
cmpl := amber.New()
cmpl.ParseFile(amberfile)
tpl,_ := cmpl.Compile()
tpl.Execute(w, data)
}
5. 注意
- UTF-8のみ可。パスに日本語やブランクは不可
- インデントは全タブor全ブランク
- 記述するファイルの拡張子は".amb"、amberファイルの拡張子は".amber"であること
- ambファイル、.amberファイルの行末のブランクは勝手に削除されることあり
- import/extendsでは、ファイル名の拡張子を除いて指定のこと
- import/extendsでは、そのambファイルがあればamberファイルを作成するが、
ambファイルがない場合、amberファイルの中は見に行かないので、その系列のamberファイル生成は打ち切られる。ちゃんとやりたいならmakeなんかで処理したほうがよい。 - パスの処理はいい加減
6. ソースリスト
標準記法から少し外れてます。
//package main
import (
"log"
"fmt"
"bufio"
"io"
"os"
"path/filepath"
ss "strings"
sv "strconv"
"github.com/eknkc/amber"
_ "time"
// "net/http"
)
//------------------------------------------------------------------
// 指定されたファイルの ambファイルから amberファイルを作成。
//・対応する ambファイルがなければ エラーを返す。
//・import/extends文が見つかった場合、そのambファイルについてもamberファイルを
// 作成するが、ambファイルがない場合、amberファイルの中は見に行かない
// (従ってその系列のamberファイルへの変換は打ち切られる)
//---------------------------------------------------------------------
func MakeAmberFile( filename,dir string ) (string,error) {
var fin,fout *os.File
var err error
if len(filename) == 0 { return "",fmt.Errorf( "you need file name" ) }
if dir == "." || dir == "" {
dir,err = filepath.Abs( filename )
if err != nil { return "",err }
dir = filepath.Dir( dir )
}
body := getPathBody( filename,dir )
inFname := body + ".amb"
outFname := body + ".amber"
_,err = os.Stat( inFname )
if err != nil { return outFname,fmt.Errorf( "%v(%s)",err.Error(),inFname ) }
// log.Println( "amb file=",inFname )
fin, err = os.Open( inFname )
if err != nil { return outFname,err }
defer fin.Close()
log.Println( "amber file=",outFname )
fout, err = os.Create( outFname )
if err != nil { return outFname,err }
defer fout.Close()
// i := 1
err = nil
scanner := bufio.NewScanner( fin )
for scanner.Scan() {
text := scanner.Text()
if k := ss.Index( text,"///" ) ; k == 0 {
continue // 行頭 "///" は行の完全削除
}
if k := ss.Index( text,"//-" ) ; k > 0 {
text = text[:k] // "//-" 行末まで削除
}
text = ss.TrimRight( text," \t\n\r" )
_ = chgImptExtd( text,dir )
text,err = outEachConst( text,scanner,fout )
if err != nil { break }
text = chgTagColon( text )
// incl,err := outIfInclude( text,dir,fout )
// if incl && err == nil { continue }
fout.WriteString( text )
// fmt.Printf( "%4d:%s",i,text )
// i++ ;
}
if err != nil { return outFname,err }
return outFname,scanner.Err()
}
//------------------------------------------------------------------
// import/extends文の場合、そのambファイルについてamberファイルを作成
//・ambファイルがない場合、amberファイルの中は見に行かない
// (その系列のamberファイル作成は打ち切られる)
//---------------------------------------------------------------------
func chgImptExtd( text,dir string ) error {
strs := ss.Fields( text )
if len(strs) != 2 { return nil }
if strs[0] != "import" && strs[0] != "extends" { return nil }
_,err := MakeAmberFile( strs[1],dir )
if err != nil { log.Println( err ) }
return err
}
//------------------------------------------------------------------
// amb形式をamber形式に変換
// ":: " --> "[" / ",, " --> "][" / ", " --> "]" or "["
// 必要なら最後に "]" を付加
//---------------------------------------------------------------------
func chgTagColon( text string ) string {
var open bool
if !ss.Contains( text,":: " ) { return text + "\n" }
text = ss.TrimRight( text," \t\n\r" )
text = ss.Replace( text,":: ","[",1 )
open = true
for n:= 0 ; n < 50 ; n++ {
kk := ss.Index( text,",, " )
k := ss.Index( text,", " )
if kk < 0 && k < 0 { break }
if k < 0 || (0 < kk && kk < k) {
text = ss.Replace( text,",, ","][",1 ) ; open = true
} else {
if open { text = ss.Replace( text,", ","]",1 ) ; open = false
} else { text = ss.Replace( text,", ","[",1 ) ; open = true }
}
}
if open { return text + "]\n" }
return text + "\n"
}
//------------------------------------------------------------------
// fname.amb/fname.amber/fname ---> fname
//---------------------------------------------------------------------
func getPathBody( fname,dir string ) string {
ext := filepath.Ext( fname )
if ext == ".amb" || ext == ".amber" {
fname = fname[:ss.LastIndex( fname,ext )]
}
if !filepath.IsAbs( fname ) {
fdir := filepath.Dir( fname )
if fdir == "." || fdir == "" {
fname = filepath.Clean( filepath.Join( dir,fname ))
}
}
// log.Println( "getPathBody out fname=",fname )
return fname
}
//------------------------------------------------------------------
// 定数回繰り返しファイル出力
// each$$ 10 ・・・ブロック中の文字列 $$ を 0~9に置き換えて計10回反復
// ブロック 空行・import・extends・include等不可
// インデントの tab,blank 混在不可
// each$$ 3,8 ・・・$$=3,4,5,6,7 計5回反復。 ","の前後にblank入ってはならない
//---------------------------------------------------------------------
func outEachConst( text string, scanner *bufio.Scanner, fout *os.File ) (string,error) {
strs := ss.Fields( text )
if len(strs) != 2 { return text,nil }
if strs[0] != "each$$" { return text,nil }
strs = ss.Split( strs[1],"," )
var begin,end int
switch len(strs) {
case 1:
begin = 0
end,_ = sv.Atoi( strs[0] )
case 2:
begin,_ = sv.Atoi( strs[0] )
end,_ = sv.Atoi( strs[1] )
default:
log.Println( "Error: amb each$$ format",text )
return text,fmt.Errorf( "Error: amb each$$ format %s",text )
}
// each$$ ブロック読み込み
indent0 := indentCnt( text )
indent1 := -1
txts := make( []string,512 )
for scanner.Scan() {
text = scanner.Text()
if k := ss.Index( text,"///" ) ; k == 0 {
continue // 行頭 "///" は行の完全削除
}
if k := ss.Index( text,"//-" ) ; k > 0 {
text = text[:k] // "//-" 行末まで削除
}
text = ss.TrimRight( text," \t\n\r" )
indent := indentCnt( text )
if indent <= indent0 { break } // 全部tab or blank,空行不可
if indent1 < 0 { indent1 = indent }
if indent1 > indent {
log.Println( "Error amb each$$ indent",text )
return text,fmt.Errorf( "Error amb each$$ indent",text )
}
text = text[:indent0] + text[indent1:]
text = chgTagColon( text )
txts = append( txts,text )
text = ""
}
if err := scanner.Err() ; err != nil {
log.Println( "Error scan amb file %s",err.Error() )
return text,err
}
// each$$ ブロック反復・書き込み
for begin < end {
idxStr := sv.Itoa( begin )
for _,txt := range txts {
txt = ss.Replace( txt,"$$",idxStr,-1 )
fout.WriteString( txt )
}
begin += 1
}
return text,nil
}
//------------------------------------------------------------------
func indentCnt( s string ) int {
slen := len(s)
for i := 0 ; i < slen ; i++ {
if s[i] != '\t' && s[i] != ' ' { return i }
}
return slen
}
//------------------------------------------------------------------
// "include filename"をその場に展開(簡易,問題あり)
//
//・importと異なり、<script>や<style>ブロック内でも展開
//・"/* ・・・ */" ---> ""(くっつける,改行も削除, ネスト・文字列無視)
//・インデントとの絡みで、うまくいくとは限らない
// (もともと、あらゆる場合に対処することはできない)
//・"\n\r" や "\r\n" は想定外(未対処)
//・パスの処理はいい加減
//・ネスト不可
//*有効にするには MakeAmberFile()中のコメントアウト解除のこと
//------------------------------------------------------------------
func outIfInclude( text,dir string,fout *os.File ) (bool,error) {
var err error
var fin *os.File
strs := ss.Fields( text )
if len(strs) != 2 { return false,nil }
if strs[0] != "include" { return false,nil }
fname := strs[1]
pref := []byte(text[:ss.Index(text,"include")])
if !filepath.IsAbs( fname ) {
fdir := filepath.Dir( fname )
if fdir == "." || fdir == "" {
fname = filepath.Clean( filepath.Join( dir,fname ))
}
}
fin, err = os.Open( fname )
if err != nil {
log.Println( "include file open failed:",fname )
return false,err
}
log.Println( "include file opened:",fname )
defer fin.Close()
skip := func( fin,fout *os.File,outFlag bool,delm string ) (err error) {
ch := make( []byte,1,1 )
ch2 := make( []byte,2,2 )
for {
if _,err = fin.Read( ch ) ; err != nil { break }
if ch[0] != delm[0] {
if outFlag {
if _,err = fout.Write( ch ) ; err != nil { break }
if ch[0] == '\n' || ch[0] == '\r'{
if _,err = fout.Write( pref ) ; err != nil { break }
}
}
continue
}
ch2[0] = ch[0]
if _,err = fin.Read( ch ); err != nil {
ch[0] = ch2[0]
fout.Write( ch )
break
}
if ch[0] == delm[1] { return nil }
if outFlag {
ch2[1] = ch[0] ;
if _,err = fout.Write( ch2 ); err != nil { break }
if ch[0] == '\n' || ch[0] == '\r'{
if _,err = fout.Write( pref ) ; err != nil { break }
}
}
}
return err
}
if _,err = fout.Write( pref ) ; err != nil {
log.Println( "error outIfInclude write",err.Error() )
return true,err
}
for {
if err = skip( fin,fout,true,"/*" ) ; err != nil { break }
if err = skip( fin,fout,false,"*/" ) ; err != nil { break }
}
if err != io.EOF {
log.Println( "error outIfInclude skip",err.Error() )
return true,err
}
_,err = fout.WriteString( "\n" )
return true,err
}
////---------------------------------------------------------------------
//// ambファイル変換 単発コマンド
//// 上方の package main や import 文の コメントアウトも解除して
//// Goファイルへ保存、コンパイルして使えます。データバインドなし。
////
//// >command ambファイル名 0 ・・・テンプレート変換結果をstdoutへ出力
//// >command ambファイル名 1 ・・・ブラウザ上 localhost:8020 で表示
//// >command ambファイル名 2 ・・・amberファイル出力のみ
////---------------------------------------------------------------------
//func main() {
//
// if len( os.Args ) != 3 {
// fmt.Println( ">command ambFile n" )
// fmt.Println( " n=0:template stdout, 1:browser localhost:8020, 2:make amberFile" )
// return
// }
//
// id,_ := sv.Atoi(os.Args[2])
// switch id {
// case 0 :
// amberFname,err := MakeAmberFile( os.Args[1],"" )
// if err != nil { fmt.Println( err ) ; return }
// cmpl := amber.New()
// err = cmpl.ParseFile( amberFname )
// if err != nil { fmt.Println( err ) ; return }
// tmpl,err := cmpl.Compile()
// if err != nil { fmt.Println( err ) ; return }
// err = tmpl.Execute( os.Stdout, nil )
// if err != nil { fmt.Println( err ) ; return }
// case 1 :
// http.HandleFunc( "/", viewHandler )
// http.ListenAndServe( ":8020",nil )
// return
// case 2 :
// _,err := MakeAmberFile( os.Args[1],"" )
// if err != nil { fmt.Println( err ) ; return }
// default :
// fmt.Println( ">command ambFile n" )
// fmt.Println( " n=0:template stdout, 1:browser localhost:8020, 2:make amberFile" )
// }
//}
//
//func viewHandler(w http.ResponseWriter, r *http.Request) {
// var err error
// var amberFname string
//
// amberFname,err = MakeAmberFile( os.Args[1],"" )
// if err != nil { http.Error( w,err.Error(),http.StatusInternalServerError ) ; return }
// cmpl := amber.New()
// err = cmpl.ParseFile( amberFname )
// if err != nil { http.Error( w,err.Error(),http.StatusInternalServerError ) ; return }
// tmpl,err := cmpl.Compile()
// if err != nil { http.Error( w,err.Error(),http.StatusInternalServerError ) ; return }
// err = tmpl.Execute( w, nil )
// if err != nil { http.Error( w,err.Error(),http.StatusInternalServerError ) ; return }
//}
本稿は、Windows8とCentOS7,Firefoxで確認しました。