タグ嫌いの人のための Goテンプレート Amber メモ



１．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")

![sft0302000.JPG](https://qiita-image-store.s3.amazonaws.com/0/99607/77acaa20-dc13-5fd2-fceb-435fae53533f.jpeg)

    
上記では 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形式に変換するサービスを利用すると少し楽できます。


２．メモ
-----------------------

１. インデントは全て空白か、全てタブかのどちらかに揃えておくのが吉です。たとえ＜script＞や＜style＞ブロック内の記述であっても、ブロックの終了を見るためと思いますが、インデント量をちゃんとしておかないとエラーになります。

２. ＜script＞や＜style＞ブロック内では、javascriptやcss定義を直接記述できます。従ってその中では import や mixinなどは使えません。

３. mixinの２重定義は、後で定義したものが有効のようです。

４. mixin の記述は定義時もコール時も空白に要注意


        mixin macro($p1)            // "mixin"と"macro"の間は１文字の空白
        mixin macro($p1, $p2, $p3)  // パラメータ記述時、および呼び出し時の
        +macro(pr1)                 // 　           空白パターンにも注意
        +macro(pr1, pr2, pr3) 　　


５. 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}

とはできないみたいです。


６. importは GOPATH、GOROOT等は見てないです。

７. importは、ネストの２段目以降はインデントを解釈してないようで、mixinの定義ファイルみたいなのならいいですが、直接展開するためのタグ群のネストは難ありでしょう。

８. import/extendsするファイル名に拡張子がないと ".amber"が付加されます。

９. import/extendsは、参照の循環?なので無限ループするかもです。

１０. "tag text" の形は、tagによっては textが無視されることがあります。その場合は、

                tag
                | text

とします。

１１. イテレーションのブロックでは、スコープがその要素に限定されるようです。たとえば、

```go:goファイル

        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>



３．おまけ (ambファイル)
-------------------------
この手のものは見た目こそが重要と私は思いますが、amber形式は少し見づらい気がしたので、amberファイルならぬ、ambファイルというのを書いて、そこから amberファイルを生成することにしました。いろいろな制限や欠陥もあり全くもって個人的用途なのですが、それでも同じような事をしたい方の参考になればと思います。

１. タグ記法追加(一行以内のこと)

    (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)で、 **最後の "," のあとにブランク２つ** あることに注意


２. 行コメント記法追加

        行頭の "///" は行の完全削除
        行途中の "//-" は行末まで削除


３. 定数回繰り返し記法追加

        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>


４. 使用例
「６. ソースリスト」の後方もご参照ください。

```go:goファイル
    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) 
    }
```


５. 注意

- UTF-8のみ可。パスに日本語やブランクは不可
- インデントは全タブor全ブランク
- 記述するファイルの拡張子は".amb"、amberファイルの拡張子は".amber"であること
- ambファイル、.amberファイルの行末のブランクは勝手に削除されることあり
- import/extendsでは、ファイル名の拡張子を除いて指定のこと
- import/extendsでは、そのambファイルがあればamberファイルを作成するが、
ambファイルがない場合、amberファイルの中は見に行かないので、その系列のamberファイル生成は打ち切られる。ちゃんとやりたいならmakeなんかで処理したほうがよい。
- パスの処理はいい加減


６. ソースリスト
標準記法から少し外れてます。

```go:
//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で確認しました。