2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-15

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

sft0302000.JPG

上記では 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. イテレーションのブロックでは、スコープがその要素に限定されるようです。たとえば、

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>

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. ソースリスト」の後方もご参照ください。

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) 
    }

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で確認しました。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?