顛末:
実際に自分で記述してみればわかると思うんだけど、「{{template “…” .}}」だけでもう「なげーよ」と。はっきりいって「楽したくてマクロを定義したつもりなのに、余計に記述量が多い」となりかねず、そして「データを一つしか渡せない」がために「
{{template "s_prosody" (.Eval `{"rate":"-10%"}`)}}
」のようなことをしなければならない、のに、ここまで読んでくれた方はわかるかと思うが「構造の定義をテンプレート内で行うことは出来ない」のだ、素の Go text/template では。ワタシが「json_loads や .Eval を追加したから初めて出来るようになったこと」なのである。そしてまたしても「なげーよ、なげーよーー」。もうね、「こんなんだったら本物の XML を直接書いたほうが楽だっ」てことになりかねん、てことで、事実今そうなりつつある。そういうわけで、ここまで頑張って実用になる使い方を探ってきたけれど、これはもう限界。これで出来ることもそれなりにはあるけれど、どんなにお世辞を重ねようが「使いにくいという事実は揺るがない」。ので、うん、ワタシとしてはもう標準ライブラリの text/template には未練はないかな。ほかのエンジンを探そうっと…。
標準ライブラリであるということが理由で、検証してみたことそのものは価値があることである。やりたかったことみたいにテンプレートを Go 言語の外で使いたい場合は結論の通りだけれど、Go でプログラミングする際に、プログラム内部で使う程度の用途にはかなり耐えると思うしね。だから後悔はしていない。疲れたけどよ。
そういうわけで、ほかのサードパーティ巡り開始である。おーさんごーによる列挙からいくつか候補が見つかる:
- fasttemplate – Simple and fast template engine. Substitutes template placeholders up to 10x faster than text/template.
- hero – Hero is a handy, fast and powerful go template engine.
- jet – Jet template engine.
- pongo2 – Django-like template-engine for Go.
- quicktemplate – Fast, powerful, yet easy to use template engine. Converts templates into Go code and then compiles it.
やたらめったら速かぞ、と主張するエンジンがいくつかあるのだが、ひとまず「Converts templates into Go code and then compiles it.」に警戒しておきたい。quicktemplate のこの説明に近いことをほかのものでも見た気がしてる。そしてこのセンテンスを字句通りに理解するならば、ワタシに必要な要件「ランタイム依存しない」にフィットしない。ので、それっぽい(速さをうたうやつ)のを除外すると、まずは jet と pongo2 だけなのかな、って気がした。うん、ひとまずそいつらからよねと。
pongo2 は、python プロジェクトである Django の「Syntax- and feature-set-compatible with Django 1.7」といっていて、まぁ魅力的ろうとは思うんだけれど、その「Syntax- and feature-set-compatible with Django 1.7」部分のドキュメントが完全に Django ドキュメントに丸投げになっててちょっと心象悪くなった。いいんだけどさ、pongo2 は django そのものではないんだから、ドキュメントはちゃんと書いてほしかったかなぁ。(こういうことされると pongo2 にインライン python 記述が出来ると思いかねないが、きっとそれは出来なかろう、みたいなことよ、たとえば。)
pongo2 の印象が少し悪かったから、というだけではなくて、jet の「テンプレートの仕様についてのドキュメント」はちゃんとしてて、それだけで標準ライブラリ text/template へ抱いた不満はなさそうに感じている。どうかな?
jet についての最初の問題は、テンプレートの仕様についてのドキュメントがしっかりわかりやすく書かれているのに、なぜか Go API の説明は(標準ライブラリと同罪の非常に理解しにくい)リファレンスのみだと言うこと。これはちょっと苦労するかもしれん、という予感がする。
text/template は「汎用のテンプレート処理コマンドラインツール」が欲しいなら自分で書く必要があったわけだが、これはそれをする必要がない、とかってことはないのかな、と思ったりもした。ぱっとみではそういう出来合いのはなさそうなので、まぁ text/template でやったのと同じノリでやってみますか、と。ただ、「text/template でやったのと同じノリで」不足機能を補う、ということが必要になりそうなのかが今時点であんまり見えてなくて、ほんとにほとんど何もしないものになるかも。つまり「template のロード、n Go!」だけの数行で終わるやつね。さすがに変数の取り込みなどがあるので、ロード・実行、だけで終わるてことにはならんはずだけど、ワタシのブログのネタとしておもろいんかなそれ、みたいなね、そうならんかちょっと心配。
うん、まぁ四の五の言わずに。はじめてみよう。
まず。Go API のわかりやすい説明はないに等しいが、かろうじて Examples から飛ばされて jettest/test.go と loaders/multi_test.go を参考にすれば、基礎的な初版が作れそうだ、から始まる〇〇生活。
うーん、ちょいと悶絶しながら出来てみた初版:
1 package main
2
3 import (
4 "os"
5 "github.com/CloudyKit/jet/v6"
6 )
7
8 func main() {
9 l := jet.NewOSFileSystemLoader(".")
10 tmplfn := os.Args[1]
11 _, err := l.Open(tmplfn)
12 if err != nil {
13 panic(err)
14 }
15 set := jet.NewSet(l)
16 tmpl, err := set.GetTemplate(tmplfn)
17 if err != nil {
18 panic(err)
19 }
20 tmpl.Execute(os.Stdout, /*variable*/nil, /*context*/nil)
21 }
「Set」という名前から「いっぱいのてんぷれーとかき集め野郎」なのかと思ったら違った。あどばんすどな追加機能ではなくて、これは「玄関」。わかりにくい…。ソースコードのコメントに書いてある:
32 // Set is responsible to load,invoke parse and cache templates and relations
33 // every jet template is associated with one set.
34 // create a set with jet.NewSet(escapeeFn) returns a pointer to the Set
35 type Set struct {
36 loader Loader
37 templates map[string]*Template // parsed templates
38 escapee SafeWriter // escapee to use at runtime
39 globals VarMap // global scope for this template set
40 tmx *sync.RWMutex // template parsing mutex
41 gmx *sync.RWMutex // global variables map mutex
42 defaultExtensions []string
43 developmentMode bool
44 }
ので、ほかのエンジンに慣れていると不思議なこの Set を経るセットアップは、どうやら不可避というか必須っぽく思える。まぁ慣れだ慣れ、とは思うけれど…、だからこそドキュメントをちゃんと書いて欲しいんだよなぁ。だって、ここに至ってもまだ「たぶん」とか「思える」としか言えないんだもん…。
データを何も与えられない例なのでなんのウマみも感じられないけれど一応動かせばこう:
1 Hi, {{.}}!
1 [me@host: tmpljetexpr01]$ ./tmpljetexpr01.exe tmpljet01.jet
2 Hi, !
ここから先の最初の流れは標準ライブラリの text/template でやったのと同じね。まずはデータをテンプレートに流し込めねーとそりゃテンプレートエンジンの使用としては両手両足を失っているようなもの、出来ることは随分限られてしまう、てこと。ここはかなり標準ライブラリの text/template でやったことをそのまま活用出来るね:
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "io/ioutil"
7 "encoding/json"
8 "github.com/ogier/pflag"
9 mxj "github.com/clbanning/mxj/v2"
10 "github.com/CloudyKit/jet/v6"
11 )
12
13 func json_loads(jsoncont string) interface{} {
14 var i interface{}
15 json.Unmarshal(([]byte)(jsoncont), &i)
16 return i
17 }
18
19 func json_load(fn string) interface{} {
20 jsoncont, err := ioutil.ReadFile(fn)
21 if err != nil { panic(err) }
22 return json_loads(string(jsoncont))
23 }
24
25 func xml_loads(xmlcont string) interface{} {
26 i, err := mxj.NewMapXml([]byte(xmlcont))
27 if err != nil { panic(err) }
28 return i
29 }
30
31 func xml_load(fn string) interface{} {
32 xmlcont, err := ioutil.ReadFile(fn)
33 if err != nil { panic(err) }
34 return xml_loads(string(xmlcont))
35 }
36
37 func main() {
38 ousage := pflag.Usage
39 pflag.Usage = func() {
40 ousage()
41 os.Exit(1)
42 }
43 pflag.Parse()
44
45 l := jet.NewOSFileSystemLoader(".")
46 tmplfn := pflag.Arg(0)
47 _, err := l.Open(tmplfn)
48 if err != nil {
49 panic(err)
50 }
51 set := jet.NewSet(l)
52 tmpl, err := set.GetTemplate(tmplfn)
53 if err != nil {
54 panic(err)
55 }
56 var context interface{}
57 if len(pflag.Args()) > 1 {
58 if filepath.Ext(pflag.Arg(1)) == ".xml" {
59 context = xml_load(pflag.Arg(1))
60 } else {
61 context = json_load(pflag.Arg(1))
62 }
63 }
64 tmpl.Execute(os.Stdout, /*variable*/nil, context/*nil*/)
65 }
コマンドラインオプションはまだ何も考えてないがひとまずは「コピペ」の都合で pflag には予め依存しとく。どうせやりたくなるろうし…、てことよりも、「variableを追加する」んだと思ってたら、text/template でやってたのに直接対応するのは context なのかもしんない、そうなのか、てのはちょっとまだ悶々としてる。つまりこれ「jet.VarMap」の御し方がわからず「interface{}」なのでてことでやってみたら意図したものだった、てこと。うん…、variable と context の「両方について考える」のはひとまず保留にして、これで動かすと:
1 Hi, {{.}}!
1 [1, 2, 4]
1 <data>
2 <li>1</li>
3 <li>3</li>
4 <li>5</li>
5 </data>
1 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01.jet data4tmpljet01.json
2 Hi, [1 2 4]!
3 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01.jet data4tmpljet01.xml
4 Hi, map[data:map[li:[1 3 5]]]!
うん、ひとまずは。一応これで「context は駆使するテンプレート記述そのもの」で遊び始められる。たとえば:
1 {{.Greeting}}, {{.Name}}!
1 {
2 "Greeting": "こんばちは",
3 "Name": "新世界(大阪)"
4 }
1 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01-2.jet data4tmpljet01-2.json
2 こんばちは, 新世界(大阪)!
うん、いいね。
次は「variable と context の「両方について考える」」か、あるいはテンプレートの仕様についての学習に突入すべきか、どっちにすっかな…? うーん、最低でもこの2つのコンセプトについての理解はしてから進めたいよなぁ…、そうするか?
variable を使っている実例は eval_test.go の中に見つけた(たとえばここ)。うん、何をすればいいかはわかる、けれど、context との使い分けはかえってわからなくなった。つまり、上の例はそのままこうも出来るということなの:
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "io/ioutil"
7 "encoding/json"
8 "github.com/ogier/pflag"
9 mxj "github.com/clbanning/mxj/v2"
10 "github.com/CloudyKit/jet/v6"
11 )
12
13 func json_loads(jsoncont string) interface{} {
14 var i interface{}
15 json.Unmarshal(([]byte)(jsoncont), &i)
16 return i
17 }
18
19 func json_load(fn string) interface{} {
20 jsoncont, err := ioutil.ReadFile(fn)
21 if err != nil { panic(err) }
22 return json_loads(string(jsoncont))
23 }
24
25 func xml_loads(xmlcont string) interface{} {
26 i, err := mxj.NewMapXml([]byte(xmlcont))
27 if err != nil { panic(err) }
28 return i
29 }
30
31 func xml_load(fn string) interface{} {
32 xmlcont, err := ioutil.ReadFile(fn)
33 if err != nil { panic(err) }
34 return xml_loads(string(xmlcont))
35 }
36
37 func main() {
38 ousage := pflag.Usage
39 pflag.Usage = func() {
40 ousage()
41 os.Exit(1)
42 }
43 pflag.Parse()
44
45 l := jet.NewOSFileSystemLoader(".")
46 tmplfn := pflag.Arg(0)
47 _, err := l.Open(tmplfn)
48 if err != nil {
49 panic(err)
50 }
51 set := jet.NewSet(l)
52 tmpl, err := set.GetTemplate(tmplfn)
53 if err != nil {
54 panic(err)
55 }
56 var variable = make(jet.VarMap)
57 if len(pflag.Args()) > 1 {
58 if filepath.Ext(pflag.Arg(1)) == ".xml" {
59 variable.Set("data", xml_load(pflag.Arg(1)))
60 } else {
61 variable.Set("data", json_load(pflag.Arg(1)))
62 }
63 }
64 tmpl.Execute(os.Stdout, variable/*nil*/, /*context*/nil)
65 }
階層が一つ下るのでテンプレートでの使用は少し変わる、当然:
1 {{data.Greeting}}, {{data.Name}}!
実行例は同じなので省略。
この VarMap に関数とかもぶちこめる、てのは text/template でやったのと同じノリ。ゆえ、「text/template でやったのと同じノリのものとしての完成品」としてはこちらの VarMap だけ使って実現出来る、てこと。だったら context はどう使ってやろうか、と。むむむ…、使い分けは自由に考えなはれ、ちぅことなのだろうか、それとも何か前提となるユースケースがあるのだろうか? それとも単に Map での階層化を伴うかグローバルか、てだけかなぁ? あまり真剣に考えたくないなら素直に「どっちもコマンドライン指定でぶっ込める」がいいかしらね?:
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "io/ioutil"
7 "encoding/json"
8 "github.com/ogier/pflag"
9 mxj "github.com/clbanning/mxj/v2"
10 "github.com/CloudyKit/jet/v6"
11 )
12
13 func json_loads(jsoncont string) interface{} {
14 var i interface{}
15 json.Unmarshal(([]byte)(jsoncont), &i)
16 return i
17 }
18
19 func json_load(fn string) interface{} {
20 jsoncont, err := ioutil.ReadFile(fn)
21 if err != nil { panic(err) }
22 return json_loads(string(jsoncont))
23 }
24
25 func xml_loads(xmlcont string) interface{} {
26 i, err := mxj.NewMapXml([]byte(xmlcont))
27 if err != nil { panic(err) }
28 return i
29 }
30
31 func xml_load(fn string) interface{} {
32 xmlcont, err := ioutil.ReadFile(fn)
33 if err != nil { panic(err) }
34 return xml_loads(string(xmlcont))
35 }
36
37 func main() {
38 var (
39 extravars = pflag.StringP(
40 "extravars", "v", "", "specify json or xml for the variables")
41 )
42 ousage := pflag.Usage
43 pflag.Usage = func() {
44 ousage()
45 os.Exit(1)
46 }
47 pflag.Parse()
48
49 l := jet.NewOSFileSystemLoader(".")
50 tmplfn := pflag.Arg(0)
51 _, err := l.Open(tmplfn)
52 if err != nil {
53 panic(err)
54 }
55 set := jet.NewSet(l)
56 tmpl, err := set.GetTemplate(tmplfn)
57 if err != nil {
58 panic(err)
59 }
60 from_fn := func(fn string) interface{} {
61 if filepath.Ext(fn) == ".xml" {
62 return xml_load(fn)
63 } else {
64 return json_load(fn)
65 }
66 }
67 var context interface{}
68 if len(pflag.Args()) > 1 {
69 context = from_fn(pflag.Arg(1))
70 }
71 var variable = make(jet.VarMap)
72 if *extravars != "" {
73 outer := from_fn(*extravars)
74 for k, v := range outer.(map[string]interface{}) {
75 variable.Set(k, v)
76 }
77 }
78 tmpl.Execute(os.Stdout, variable, context)
79 }
ここでの発想としては、context は json, xml からロードしたトップレベルを使い、variables は「Map であることを前提に」階層をひとつ展開する、てことね。伝わるかしら。「data」という名前を固定するのがイヤだ、ていう言い方でも伝わる? ともあれ:
1 {
2 "data": {
3 "Greeting": "こんばちは",
4 "Name": "新世界(大阪)"
5 }
6 }
1 [me@host: tmpljetexpr03]$ # context のほうに突っ込む例
2 [me@host: tmpljetexpr03]$ ./tmpljetexpr03.exe tmpljet01-2.jet data4tmpljet01-2.json
3 こんばちは, 新世界(大阪)!
4 [me@host: tmpljetexpr03]$ # variable のほうに突っ込む例
5 [me@host: tmpljetexpr03]$ ./tmpljetexpr03.exe tmpljet01-2-2.jet --extravars=data4tmpljet01-3.json
6 こんばちは, 新世界(大阪)!
まぁこれならあんまし難しいこと考えずに済むわな。
とりあえず必要に思うかどうかはまだわからんけど、出来る「ので」てことで json_load[s]/xml_load[s] をテンプレート側に公開しておく:
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "io/ioutil"
7 "encoding/json"
8 "github.com/ogier/pflag"
9 mxj "github.com/clbanning/mxj/v2"
10 "github.com/CloudyKit/jet/v6"
11 )
12
13 func json_loads(jsoncont string) interface{} {
14 var i interface{}
15 json.Unmarshal(([]byte)(jsoncont), &i)
16 return i
17 }
18
19 func json_load(fn string) interface{} {
20 jsoncont, err := ioutil.ReadFile(fn)
21 if err != nil { panic(err) }
22 return json_loads(string(jsoncont))
23 }
24
25 func xml_loads(xmlcont string) interface{} {
26 i, err := mxj.NewMapXml([]byte(xmlcont))
27 if err != nil { panic(err) }
28 return i
29 }
30
31 func xml_load(fn string) interface{} {
32 xmlcont, err := ioutil.ReadFile(fn)
33 if err != nil { panic(err) }
34 return xml_loads(string(xmlcont))
35 }
36
37 func main() {
38 var (
39 extravars = pflag.StringP(
40 "extravars", "v", "", "specify json or xml for the variables")
41 )
42 ousage := pflag.Usage
43 pflag.Usage = func() {
44 ousage()
45 os.Exit(1)
46 }
47 pflag.Parse()
48
49 l := jet.NewOSFileSystemLoader(".")
50 tmplfn := pflag.Arg(0)
51 _, err := l.Open(tmplfn)
52 if err != nil {
53 panic(err)
54 }
55 set := jet.NewSet(l)
56 tmpl, err := set.GetTemplate(tmplfn)
57 if err != nil {
58 panic(err)
59 }
60 from_fn := func(fn string) interface{} {
61 if filepath.Ext(fn) == ".xml" {
62 return xml_load(fn)
63 } else {
64 return json_load(fn)
65 }
66 }
67 var context interface{}
68 if len(pflag.Args()) > 1 {
69 context = from_fn(pflag.Arg(1))
70 }
71 var variable = make(jet.VarMap)
72 variable.Set("json_loads", json_loads)
73 variable.Set("json_load", json_load)
74 variable.Set("xml_loads", xml_loads)
75 variable.Set("xml_load", xml_load)
76 if *extravars != "" {
77 outer := from_fn(*extravars)
78 for k, v := range outer.(map[string]interface{}) {
79 variable.Set(k, v)
80 }
81 }
82 tmpl.Execute(os.Stdout, variable, context)
83 }
1 {{- d := json_load("data4tmpljet01-3.json") -}}
2 {{d.data.Name}} - {{d.data.Greeting}}
1 [me@host: tmpljetexpr04]$ ./tmpljetexpr04.exe tmpljet02.jet
2 新世界(大阪) - こんばちは
まぁ text/template でやったのと同じ話なので、とりたてて興奮するもんでもなくて、ゼロカロリーである。
Go プログラムは少しこのままにしておいて、テンプレートの仕様について学んでおく。といっても text/template の時と違って、ドキュメントはカッチリきっちり書かれてるし、(pongo2と違って)ベース部分はどうやら text/template に似せてあるようで、スムーズに理解出来る。まぁ既にさらっと例示しちゃってるんでお察しかもしれんけどね。
設定で「{}」を使うか「[]」を使うか選択可能だ、と言っているけれど、その切り替えについては後回しにして、まずは前者固定として。
空白除去は text/template と同じだね。コメントは text/template よりスッキリしてる:
1 {* this is a comment *}
2 {*
3 none of this will be executed:
4 {{ asd }}
5 {{ include "./foo.jet" }}
6 *}
Variables、Expressions はね、もう以下だけで text/template で不満だったのがきっとないんだろうと即座にわかるろ?:
1 {{ s := "helloworld" }}
2 {{ s[1] }}
1 {{ s := slice("foo", "bar", "asd") }}
2 {{ s[0] }}
3 {{ i := 2 }}
4 {{ s[i] }}
1 {{ m := map("foo", 123, "bar", 456) }}
2 {{ m["foo"] }}
3 {{ bar := "bar" }}
4 {{ m[bar] }}
つまり「普通だ」てことだよ。至極直感的に記述出来る。text/template の「惜しい」がやりきってある、てこと。もうなんかこれだけでも乗り換えた甲斐があった、とすら思うよね。
あと、きっともう気付いてると思うけど、text/template だけでやろうとしてたやつでは goval で実現しようとした「計算とか」は機能として組み込まれてる:
1 {{ (1 + 2) * 3 - 4.1 }}
2 {{ "HELLO" + " " + "WORLD!" }}
万事が万事、text/template で「こうだったら良かったのに…」が「そう」なってる、つまり、用語やコンセプト名から想像出来る通りの「書き方で」書けばそれが受け入れられる、と思えばほとんど合ってると思う。text/template 用語の「pipelines」は「どこがパイプラインやねん」だったのが「ちゃんとパイプライン」だし、Control structures や Expressions も「高級言語のソレ」のノリそのまんまで考えればそれがきっと正しい:
1 {{ item == true || !item2 && item3 != "test" }}
ので、そうした「text/template コンセプトをちゃんと完遂しただけ」と思われる機能については、「ドキュメント読めばわかーる」で良いであろう。
ここからは、text/template では「惜しい」ですらなかったダメ機能が jet でどうなってるか、だ。つまりメインエベントだわな。
全然ダメだった、のひとつ目は既に書いた。「テンプレート内で変数定義する際、構造を作れない(参照は出来るくせに)」については、「map」の形で書けるので、json_loads とかを使わなくていい、てこと。これだけでも存分に乗り換えてよかったね、て思うよね、と言った。
そして、「ほか」といっても Templates と Blocks だけなのだから、これも「読めばわかーる」と言ってしまえばそうなんだけれど、text/template に見切りをつけた決定打となったのこそがこれなので、「text/template で書こうとしてちっとも楽できないと感じた実例」をば:
1 {*- most outer container -*}
2 {{- block speak(lang="ja-JP") -}}
3 <speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{{lang}}'>
4 {{- yield content -}}
5 </speak>
6 {{- end -}}
7
8 {*- voice element -*}
9 {{- block voice(voice="ja-JP, KeitaNeural") -}}
10 <voice name='Microsoft Server Speech Text to Speech Voice ({{voice}})'>
11 {{- yield content -}}
12 </voice>
13 {{- end -}}
14
15 {*- prosody element -*}
16 {{- block prosody(pitch="+0Hz", rate="+0%") -}}
17 <prosody pitch="{{pitch}}" rate="{{rate}}">
18 {{- yield content -}}
19 </prosody>
20 {{- end -}}
21
22 {*- break element -*}
23 {{- block break(time="1s") -}}
24 <break time="{{time}}"/>
25 {{- end -}}
1 {{ import "./base.jet" }}
2 {{ yield speak() content }}
3 {{ yield voice() content }}
4 <p>
5 <s>こんばんは。またお会いできましたね。{{yield break(time="4s")}}</s>
6 <s>{{ yield prosody(rate="-20%", pitch="+110Hz") content }}どうしました?{{ end }}</s>
7 </p>
8 {{ end }}
9 {{ end }}
1 <speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='ja-JP'>
2 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
3 <p>
4 <s>こんばんは。またお会いできましたね。<break time="4s"/></s>
5 <s><prosody pitch="+110Hz" rate="-20%">どうしました?</prosody></s>
6 </p>
7 </voice>
8 </speak>
うん、めっさ普通。この「普通」がいかに大事で大変なことなのか、てことよな。
微妙に「記述が長い」が解決してないけれど、少なくとも「記述が長過ぎる」ではなくなったし、不自然な考え方もしなくて良くなってマクロ(という言い方はしてないみたいだけど)を臆せずガンガン作れるようになってる、てなわけで、何億倍かは気分が良い。だいたいにして「めっさ読みやすい、わかりやすい」よね。
ちなみにこの SSML を Microsoft Edge TTS サーバに投げて音声に出来る:
1 [me@host: ~]$ py -3 -m edge_tts -f 上で作ったSSML.xml -z > part0.mp3
あとはこの CLI ツールの進化をどうしていくか、なのだが、最低でも「テンプレートパス」の扱いと複数ファイルの処理について綺麗に整理すべきなんだろう。が、ひとまずはここまでのものでもかなり実用になるので、その方面のは後回しでいいか、と。
正直 jet で満足しそうな予感がしていたんだけれど、ちょっと冗長性がやはり気になるのと、あとね、ひとつどうしてもやりたいのに出来ていないことがある。こういうことがしたかったの:
1 {{- block speak(lang="ja-JP") -}}
2 <speak version='1.0'
3 xmlns='http://www.w3.org/2001/10/synthesis'
4 xmlns:mstts='https://www.w3.org/2001/mstts'
5 xml:lang='{{lang}}'>
6 {{- yield content | nl_remove -}}
7 </speak>
8 {{- end -}}
9
10 {* 呼び出す側でやろうとやっぱりダメなものはダメ *}
11 {{ yield speak() content | nl_remove }}
12 {{- end -}}
これは Microsoft Edge’s TTS Server 相手の SSML 記述。何をしたいかというと。要素として「パラグラフ」「センテンス」について、SSML 記述でその区切りを指定出来るわけだけれど、「ワード」の区切りはこれは「英語とかなら自明、日本語だと不明瞭不可解奇っ怪奇々怪々」でしょ? わかるよね。だから、「パラグラフ」「センテンス」の内側に入る「テキスト」に改行が入るか入らないかは、これは英語ならまず支障ないけれど、日本語だと読み方が変わりうるわけ。でもだからといって「SSML 記述の元になるテキスト」を無改行で書き連ねるのは読むのも書くのもツラい、だからテンプレートの機能で改行取っ払いが欲しいぞ、と、まぁそういうわけ。だけど上の書き方は(たとえ「nl_remove」が実在していたとしても)出来ない。
思うに「yield」と「content」のスペシャル扱い過ぎが原因なんじゃなかろうかと思う。構文のパースがこの仕様ではシンプルになるのは理解は出来るんだよなぁ、気持ちはわかるけれども、て感じ。
ひとまず次善策として「ポストプロセス」的な思想の、というよりは「乱暴な変換」だけでも組み込んでみた:
1 package main
2
3 import (
4 "os"
5 "bytes"
6 "regexp"
7 "path/filepath"
8 "io/ioutil"
9 "encoding/json"
10 "github.com/ogier/pflag"
11 mxj "github.com/clbanning/mxj/v2"
12 "github.com/CloudyKit/jet/v6"
13 )
14
15 func json_loads(jsoncont string) interface{} {
16 var i interface{}
17 json.Unmarshal(([]byte)(jsoncont), &i)
18 return i
19 }
20
21 func json_load(fn string) interface{} {
22 jsoncont, err := ioutil.ReadFile(fn)
23 if err != nil { panic(err) }
24 return json_loads(string(jsoncont))
25 }
26
27 func xml_loads(xmlcont string) interface{} {
28 i, err := mxj.NewMapXml([]byte(xmlcont))
29 if err != nil { panic(err) }
30 return i
31 }
32
33 func xml_load(fn string) interface{} {
34 xmlcont, err := ioutil.ReadFile(fn)
35 if err != nil { panic(err) }
36 return xml_loads(string(xmlcont))
37 }
38
39 func main() {
40 var (
41 extravars = pflag.StringP(
42 "extravars", "v", "", "specify json or xml for the variables")
43 subnl = pflag.StringP(
44 "replace_rendered_newline", "n", "\n", "replace newline to this")
45 )
46 ousage := pflag.Usage
47 pflag.Usage = func() {
48 ousage()
49 os.Exit(1)
50 }
51 pflag.Parse()
52
53 l := jet.NewOSFileSystemLoader(".")
54 tmplfn := pflag.Arg(0)
55 _, err := l.Open(tmplfn)
56 if err != nil {
57 panic(err)
58 }
59 set := jet.NewSet(l)
60 tmpl, err := set.GetTemplate(tmplfn)
61 if err != nil {
62 panic(err)
63 }
64 from_fn := func(fn string) interface{} {
65 if filepath.Ext(fn) == ".xml" {
66 return xml_load(fn)
67 } else {
68 return json_load(fn)
69 }
70 }
71 var context interface{}
72 if len(pflag.Args()) > 1 {
73 context = from_fn(pflag.Arg(1))
74 }
75 var variable = make(jet.VarMap)
76 variable.Set("json_loads", json_loads)
77 variable.Set("json_load", json_load)
78 variable.Set("xml_loads", xml_loads)
79 variable.Set("xml_load", xml_load)
80 if *extravars != "" {
81 outer := from_fn(*extravars)
82 for k, v := range outer.(map[string]interface{}) {
83 variable.Set(k, v)
84 }
85 }
86 nl_remove := func (cont string) string {
87 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
88 }
89 writer := bytes.NewBufferString("")
90 tmpl.Execute(writer, variable, context)
91 os.Stdout.Write([]byte(nl_remove(writer.String())))
92 }
問答無用で改行を置き換える、というものだが、これは「汎用」を考えるならダメなんだけれど、相手が xml (や html など SGML 系)なら問題ないケースが結構多いだろうなと思う。CDATA を駆使してるとダメだけれど、そうでないなら。ただ、これはあとで少しはちゃんとしたいかなぁ。「改行を置き換える」という決め打ちでないのがいいよね、同じ乱暴なんだとしてもね。(xml の場合は例えば「オリジナルの改行を取り除きつつ 「>」 の前に改行を入れる」という(バッド)ノウハウがあるでしょ、そういうの。)
2022-02-11追記:
話としてはふたつ。
ひとつめは「やってて気付いてなくて、あとになってちょっと悶絶した」話。「block」なんだけどね、これ、Go 標準の text/template と同じなんだよね、以下:
1 {{block copyright()}}
2 <div>© HOGEME, Inc. 2032</div>
3 {{end}}
4 <footer>
5 {{yield copyright()}}
6 </footer>
これを実行すれば、「素直に」こうなる:
1 <div>© HOGEME, Inc. 2032</div>
2
3 <footer>
4
5 <div>© HOGEME, Inc. 2032</div>
6
7 </footer>
ちゃんと説明されてる:
Defining a block in a template that’s being executed will also invoke it immediately. To avoid this, use
import
orextends
.
なんでずっと気付かなかったかって、もちろん import 元でしか block を書いてなかったから。そしてなんで今さら気付いたかって、続くふたつ目の話のための「実験」で簡単に試そうとして block と yield を同じテンプレートファイルに書いちゃったため。なんで二回出るんだぁ、と、10分以上頭掻きむしってたわよ。
ふたつめの話は、ワタシの節穴に関係してて、最後に話が覆るのだが、まぁノンビリ読み進めてくれ、そして笑うがいい。
Python を例にするとキーワードの関係でちょっとややこしいところもあるんだけれど、要はこれ:
1 for i in range(5):
2 print(i)
Go の「range」が Python などで言うところの「for」なのでめっちゃややこしいのだが、欲しいのはこの「Python で言うところの range」に相当する機能である。numpy の arange や linspace のようなもの、と言ってもいい。つまり、回数指定等でシーケンスを生成する機能が欲しい。けれども、どうにもこういう機能が見当たらない。うーん、ないのか?
これで実現したかったのはやはり SSML に関係している。Microsoft Edge TTS Server のエンジンが「break」(読み上げの休止機能)の実装でやらかしちゃってるんだよね。説明は「This value should be set less than 5,000 ms」だ、だから5秒以上を指定したお前が悪い、ということはわかる。けれども「黙って切り捨てる」は一番やっちゃいけない。ライターの指示を勝手に無視して間違ったまま「正しい」フリをして動き続けてしまうと、あとになって気付いた際には影響が大き過ぎて簡単には直せなくなる、なんてことが起こる。そりゃそうであろう。テストしながら書いているのであれば、その間違った結果を一度以上は受け入れてしまっていたのだから。そのテストを全てやり直すハメになる、てことだ。ともあれ、そういうことなので、たとえば「30秒の break」を実現したければ、5秒指定の break を6回呼び出す必要があるわけだ、なので、テンプレートの block として「30秒を引数に与えると5秒指定の break を6回呼び出す」ようなものを書きたい、と。
そして「「Python で言うところの range」に相当する機能がないので」書けない、のか? てハナシ。slice と repeat で誤魔化せないかなとも思ったが、どうもうまくいかなかった。のでもうこれは、追加するしかないかなと:
1 package main
2
3 import (
4 "os"
5 "bytes"
6 "regexp"
7 "path/filepath"
8 "io/ioutil"
9 "encoding/json"
10 "github.com/ogier/pflag"
11 mxj "github.com/clbanning/mxj/v2"
12 "github.com/CloudyKit/jet/v6"
13 )
14
15 func json_loads(jsoncont string) interface{} {
16 var i interface{}
17 json.Unmarshal(([]byte)(jsoncont), &i)
18 return i
19 }
20
21 func json_load(fn string) interface{} {
22 jsoncont, err := ioutil.ReadFile(fn)
23 if err != nil { panic(err) }
24 return json_loads(string(jsoncont))
25 }
26
27 func xml_loads(xmlcont string) interface{} {
28 i, err := mxj.NewMapXml([]byte(xmlcont))
29 if err != nil { panic(err) }
30 return i
31 }
32
33 func xml_load(fn string) interface{} {
34 xmlcont, err := ioutil.ReadFile(fn)
35 if err != nil { panic(err) }
36 return xml_loads(string(xmlcont))
37 }
38
39 func make_seq(sss ...float64) []float64 {
40 sta := 0.0
41 ste := 1.0
42 sto := 1.0
43 if len(sss) == 1 {
44 sto = sss[0]
45 } else if len(sss) == 2 {
46 sta = sss[0]
47 sto = sss[1]
48 } else if len(sss) == 3 {
49 sta = sss[0]
50 sto = sss[1]
51 ste = sss[2]
52 if ste == 0.0 {
53 panic("`step` must not be zero")
54 }
55 } else {
56 panic("require arguments, at least one for `stop`")
57 }
58 var res []float64
59 for {
60 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
61 break
62 }
63 res = append(res, sta)
64 sta += ste
65 }
66 return res
67 }
68
69 func main() {
70 var (
71 extravars = pflag.StringP(
72 "extravars", "v", "", "specify json or xml for the variables")
73 subnl = pflag.StringP(
74 "replace_rendered_newline", "n", "\n", "replace newline to this")
75 )
76 ousage := pflag.Usage
77 pflag.Usage = func() {
78 ousage()
79 os.Exit(1)
80 }
81 pflag.Parse()
82
83 l := jet.NewOSFileSystemLoader(".")
84 tmplfn := pflag.Arg(0)
85 _, err := l.Open(tmplfn)
86 if err != nil {
87 panic(err)
88 }
89 set := jet.NewSet(l)
90 tmpl, err := set.GetTemplate(tmplfn)
91 if err != nil {
92 panic(err)
93 }
94 from_fn := func(fn string) interface{} {
95 if filepath.Ext(fn) == ".xml" {
96 return xml_load(fn)
97 } else {
98 return json_load(fn)
99 }
100 }
101 var context interface{}
102 if len(pflag.Args()) > 1 {
103 context = from_fn(pflag.Arg(1))
104 }
105 var variable = make(jet.VarMap)
106 variable.Set("json_loads", json_loads)
107 variable.Set("json_load", json_load)
108 variable.Set("xml_loads", xml_loads)
109 variable.Set("xml_load", xml_load)
110 variable.Set("make_seq", make_seq)
111 if *extravars != "" {
112 outer := from_fn(*extravars)
113 for k, v := range outer.(map[string]interface{}) {
114 variable.Set(k, v)
115 }
116 }
117 nl_remove := func (cont string) string {
118 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
119 }
120 writer := bytes.NewBufferString("")
121 err = tmpl.Execute(writer, variable, context)
122 if err != nil {
123 panic(err)
124 }
125 os.Stdout.Write([]byte(nl_remove(writer.String())))
126 }
これによりめでたく:
1 {{ block break_s(sec=5) }}
2 {{- range make_seq(sec / 5) }}
3 {{- nxt := ((. + 1) * 5) }}
4 {{- if nxt > sec }}{{ nxt = sec }}{{ end -}}
5 {{- t := nxt - (. * 5) -}}
6 <break time="{{t}}s"/>
7 {{ end -}}
8 {{ end }}
1 {{- import "base.jet" -}}
2 {{ yield break_s(sec=6) }}
3
4 {{ yield break_s(sec=13) }}
1 <break time="5s"/>
2 <break time="1s"/>
3
4
5 <break time="5s"/>
6 <break time="5s"/>
7 <break time="3s"/>
うん、よいね。
にしてもこれってかなり基礎的で絶対に必要なもんのような気がするんだけど…。ほかのエンジンってどうなってたかなぁと思い返してみるんだけど、多くのものが実装言語の機能をそのまま使えるものが多かったので、おそらくそれらはその中で吸収出来ているんであろうね、だってこんなん気になったことないもん、ほかのエンジン使ってて。
と、あったら恥ずかしいことになるので改めてリファレンスを熟読………ありやがりまして、辱められておめでとうございます。うわぁ、なんか見つけにくい命名…。致し方ない、のかしらねぇ…?
ワタシのは整数であることを前提としてないので同じものではなくて、そしてワタシのでないとワタシのは素直には書けない。ので、「追加し損」ではない。けれど、ints だって十分な場合は十分なわけだ:
1 {{ block break5rep(times=5) }}
2 {{- range ints(0, times) }}
3 <break time="5s"/>
4 {{- end -}}
5 {{ end }}
1 {{- import "base.jet" -}}
2 {{ yield break5rep(times=3) }}
1 <break time="5s"/>
2 <break time="5s"/>
3 <break time="5s"/>
頭まわらんので出来てないが、頑張れば ints でもやりたいことは出来る、かも。けど整数のシーケンスしか作れない、は、今回のようなのでなくてもいずれは不自由に感じることだろう、無駄ではなかった、と思っておこう…。
ところで「追記:」として上の方にも入れまくった「Execute のエラー処理」。やべーよこういうミス。Go ならではというか。Go って、かなり「原点回帰」なのよ、あえて C 言語に先祖返りしてる仕様がたくさんあって、「機構としての「例外」のような「巻き戻し」はない」のね、だから、必要なときにはいつでも都度 func 等からの戻りをチェックしなければならない。わかってたハズなのに、やっぱほかのモダンな言語に染まりすぎちゃってたわ、「エラー処理をサボるとその報いはデカい」てことをすっかり忘れていた。今のワタシの場合 Microsoft Edge TTS Server が相手なんだけど、だとするならば「テンプレート処理結果で誤りが起こった」なら Microsoft Edge TTS Server との通信は端折っていいし、端折るべきである。壊れた XML を投げて通信時間を損する合理的な理由なぞなかろう。サボって良かった、なんてことにはならない。その小さな時間節約が、何分・何十分・何百分の損失に繋がる。みたいなこと。main エントリの終了コードって、バッチ処理にはとても重大なことなのだよね。
2022-02-12追記:
思ったよりも早く必要になった。「テンプレートパスの追加」。
実際ワタシはこの道具を「自分の個人タスクのために」大活躍させているわけなのだけれど、となれば当然「基底お便利ベース」を「共通置き場」に放り込んでおきたい、という欲が出てくるわけだ。こんなのやだろ、と:
1 [me@host: ~]$ find . -maxdepth 2 -name 'base*.jet' -ls
2 1023080 1 -rw-r--r-- 1 hhsprings Administrators 35 Feb 02:43 ./wk1/base.jet
3 1022877 2 -rw-r--r-- 11 hhsprings Administrators 3648 Feb 21:13 ./wk1/base_common.jet
4 498716 1 -rw-r--r-- 1 hhsprings Administrators 1587 Feb 01:10 ./wk2/base.jet
5 1022877 2 -rw-r--r-- 11 hhsprings Administrators 3648 Feb 21:13 ./wk2/base_common.jet
6 6789960 2 -rw-r--r-- 1 hhsprings Administrators 2301 Feb 09:35 ./wk3/base.jet
7 1022877 2 -rw-r--r-- 11 hhsprings Administrators 3648 Feb 21:13 ./wk3/base_common.jet
8 498592 1 -rw-r--r-- 1 hhsprings Administrators 1576 Feb 23:38 ./wk4/base.jet
9 1022877 2 -rw-r--r-- 11 hhsprings Administrators 3648 Feb 21:13 ./wk4/base_common.jet
10 498521 1 -rw-r--r-- 1 hhsprings Administrators 1895 Feb 02:10 ./wk5/base.jet
11 1022877 2 -rw-r--r-- 11 hhsprings Administrators 3648 Feb 21:13 ./wk5/base_common.jet
rwx フラグ表示の次の数字カラムはハードリンク数で、つまり base_common.jet はハードリンクしているわけね、けどこのハードリンクを作るのも維持するのもメンドイさそりゃ、なので例えば common みたいなフォルダに置いて、その場所をローダが見つけられるようにしたいわけだ。
相変わらずドキュメントのわかりにくさでかなり迷走したけれど、出来た:
1 package main
2
3 import (
4 "strings"
5 "os"
6 "bytes"
7 "regexp"
8 "path/filepath"
9 "io/ioutil"
10 "encoding/json"
11 "github.com/ogier/pflag"
12 mxj "github.com/clbanning/mxj/v2"
13 "github.com/CloudyKit/jet/v6"
14 "github.com/CloudyKit/jet/v6/loaders/multi"
15 )
16
17 func json_loads(jsoncont string) interface{} {
18 var i interface{}
19 json.Unmarshal(([]byte)(jsoncont), &i)
20 return i
21 }
22
23 func json_load(fn string) interface{} {
24 jsoncont, err := ioutil.ReadFile(fn)
25 if err != nil { panic(err) }
26 return json_loads(string(jsoncont))
27 }
28
29 func xml_loads(xmlcont string) interface{} {
30 i, err := mxj.NewMapXml([]byte(xmlcont))
31 if err != nil { panic(err) }
32 return i
33 }
34
35 func xml_load(fn string) interface{} {
36 xmlcont, err := ioutil.ReadFile(fn)
37 if err != nil { panic(err) }
38 return xml_loads(string(xmlcont))
39 }
40
41 func make_seq(sss ...float64) []float64 {
42 sta := 0.0
43 ste := 1.0
44 sto := 1.0
45 if len(sss) == 1 {
46 sto = sss[0]
47 } else if len(sss) == 2 {
48 sta = sss[0]
49 sto = sss[1]
50 } else if len(sss) == 3 {
51 sta = sss[0]
52 sto = sss[1]
53 ste = sss[2]
54 if ste == 0.0 {
55 panic("`step` must not be zero")
56 }
57 } else {
58 panic("require arguments, at least one for `stop`")
59 }
60 var res []float64
61 for {
62 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
63 break
64 }
65 res = append(res, sta)
66 sta += ste
67 }
68 return res
69 }
70
71 func main() {
72 var (
73 extravars = pflag.StringP(
74 "extravars", "v", "", "specify json or xml for the variables")
75 subnl = pflag.StringP(
76 "replace_rendered_newline", "n", "\n", "replace newline to this")
77 addpath = pflag.StringP(
78 "addpath", "p", "",
79 "additional template paths (semicolon separated list)")
80 )
81 ousage := pflag.Usage
82 pflag.Usage = func() {
83 ousage()
84 os.Exit(1)
85 }
86 pflag.Parse()
87
88 var loaders []jet.Loader
89 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
90 for _, p := range strings.Split(*addpath, ";") {
91 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
92 }
93 l := multi.NewLoader(loaders...)
94 tmplfn := pflag.Arg(0)
95 set := jet.NewSet(l)
96 tmpl, err := set.GetTemplate(tmplfn)
97 if err != nil {
98 panic(err)
99 }
100 from_fn := func(fn string) interface{} {
101 if filepath.Ext(fn) == ".xml" {
102 return xml_load(fn)
103 } else {
104 return json_load(fn)
105 }
106 }
107 var context interface{}
108 if len(pflag.Args()) > 1 {
109 context = from_fn(pflag.Arg(1))
110 }
111 var variable = make(jet.VarMap)
112 variable.Set("json_loads", json_loads)
113 variable.Set("json_load", json_load)
114 variable.Set("xml_loads", xml_loads)
115 variable.Set("xml_load", xml_load)
116 variable.Set("make_seq", make_seq)
117 if *extravars != "" {
118 outer := from_fn(*extravars)
119 for k, v := range outer.(map[string]interface{}) {
120 variable.Set(k, v)
121 }
122 }
123 nl_remove := func (cont string) string {
124 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
125 }
126 writer := bytes.NewBufferString("")
127 err = tmpl.Execute(writer, variable, context)
128 if err != nil {
129 panic(err)
130 }
131 os.Stdout.Write([]byte(nl_remove(writer.String())))
132 }
バージョン違いのソースコードを読んでしまって混乱したのも時間がかかった要因の一つだけれど、でもね、やっぱりドキュメントの問題よ、これは。
ちなみに蛇足と言うか。この一つ前までの版、「l」(Loader)の Open がいらなかった。なんで気付かなかったんだろ?
2022-02-13追記:
今したい話は2つあるのだが、2つ目はちょっとややこしそうなので後日として、1つ目だけ。
昨日の、「テンプレートの共通置き場を考えたい」なのだが、よく考えたら json_load、xml_load もこれに追従出来ないと困るんではないかと。ベースとなるテンプレートで json_load をあてにしてたとすると、それっておそらくテンプレートと json が同じ場所にあることを前提にしちゃうハズと思うんだよね、事実ワタシはそうしてた。ちょっと、コードの整理も伴ってやりたかったことよりも大きな改造をしちゃったがこんな感じ:
1 package main
2
3 import (
4 "strings"
5 "os"
6 "bytes"
7 "regexp"
8 "path/filepath"
9 "io"
10 "encoding/json"
11 "github.com/ogier/pflag"
12 mxj "github.com/clbanning/mxj/v2"
13 "github.com/CloudyKit/jet/v6"
14 "github.com/CloudyKit/jet/v6/loaders/multi"
15 )
16
17 type MyOpt struct {
18 extravars *string
19 subnl *string
20 addpath *string
21 }
22 var myOpt MyOpt
23
24 func init() {
25 ousage := pflag.Usage
26 pflag.Usage = func() {
27 ousage()
28 os.Exit(1)
29 }
30 myOpt.extravars = pflag.StringP(
31 "extravars", "v", "", "specify json or xml for the variables")
32 myOpt.subnl = pflag.StringP(
33 "replace_rendered_newline", "n", "\n", "replace newline to this")
34 myOpt.addpath = pflag.StringP(
35 "addpath", "p", "",
36 "additional template paths (semicolon separated list)")
37 }
38
39 func _Read(path string) ([]byte, error) {
40 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
41 for _, p := range sp {
42 if f, err := os.Open(filepath.Join(p, path)); err == nil {
43 return io.ReadAll(f)
44 }
45 }
46 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
47 }
48
49 func json_loads(jsoncont string) interface{} {
50 var i interface{}
51 json.Unmarshal(([]byte)(jsoncont), &i)
52 return i
53 }
54
55 func json_load(fn string) interface{} {
56 jsoncont, err := _Read(fn)
57 if err != nil { panic(err) }
58 return json_loads(string(jsoncont))
59 }
60
61 func xml_loads(xmlcont string) interface{} {
62 i, err := mxj.NewMapXml([]byte(xmlcont))
63 if err != nil { panic(err) }
64 return i
65 }
66
67 func xml_load(fn string) interface{} {
68 xmlcont, err := _Read(fn)
69 if err != nil { panic(err) }
70 return xml_loads(string(xmlcont))
71 }
72
73 func make_seq(sss ...float64) []float64 {
74 sta := 0.0
75 ste := 1.0
76 sto := 1.0
77 if len(sss) == 1 {
78 sto = sss[0]
79 } else if len(sss) == 2 {
80 sta = sss[0]
81 sto = sss[1]
82 } else if len(sss) == 3 {
83 sta = sss[0]
84 sto = sss[1]
85 ste = sss[2]
86 if ste == 0.0 {
87 panic("`step` must not be zero")
88 }
89 } else {
90 panic("require arguments, at least one for `stop`")
91 }
92 var res []float64
93 for {
94 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
95 break
96 }
97 res = append(res, sta)
98 sta += ste
99 }
100 return res
101 }
102
103 func main() {
104 pflag.Parse()
105
106 var loaders []jet.Loader
107 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
108 for _, p := range strings.Split(*myOpt.addpath, ";") {
109 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
110 }
111 l := multi.NewLoader(loaders...)
112 tmplfn := pflag.Arg(0)
113 set := jet.NewSet(l)
114 tmpl, err := set.GetTemplate(tmplfn)
115 if err != nil {
116 panic(err)
117 }
118 from_fn := func(fn string) interface{} {
119 if filepath.Ext(fn) == ".xml" {
120 return xml_load(fn)
121 } else {
122 return json_load(fn)
123 }
124 }
125 var context interface{}
126 if len(pflag.Args()) > 1 {
127 context = from_fn(pflag.Arg(1))
128 }
129 var variable = make(jet.VarMap)
130 variable.Set("json_loads", json_loads)
131 variable.Set("json_load", json_load)
132 variable.Set("xml_loads", xml_loads)
133 variable.Set("xml_load", xml_load)
134 variable.Set("make_seq", make_seq)
135 if *myOpt.extravars != "" {
136 outer := from_fn(*myOpt.extravars)
137 for k, v := range outer.(map[string]interface{}) {
138 variable.Set(k, v)
139 }
140 }
141 nl_remove := func (cont string) string {
142 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
143 }
144 writer := bytes.NewBufferString("")
145 err = tmpl.Execute(writer, variable, context)
146 if err != nil {
147 panic(err)
148 }
149 os.Stdout.Write([]byte(nl_remove(writer.String())))
150 }
テンプレートの場所と必ず同じなのか、てのは本当は考える余地ありなのかもしれんけど、当座はあんまし凝ったもんはいらんからね、同じ、とした。
「2つ目」なのだが、予告だけしとくと「intへのキャストとか min/max とかあるいは math」の話。ちょっと前の追記の「make_seq vs ints」に関係してる。が、まだ何も話が見えてない、ので、後日、と。
2022-02-14追記:
昨日保留にした話なのだが、なんというかちょっと微妙な気分になってる。
話としては結局のところ「何が欠けているか? 答えは、色々欠けている」ということなんだけれど、だったらどうするのが正解か、てのもなかなか言いにくい。というのも、これから書く内容はハッキリいってしまえば「jet 自身に組み込んだほうがよくね?」てものたちばかりになるし、であれば、ブログのネタとしてじゃなく PR 挙げちまった方が世のため人のためになると思う。ただ、…、そう、話は「built-ins」の話になるので、これって、まぁ技術者間の好みの問題も含め、論争の種にもなりうるような、そういう繊細な話にもなりうる。言い方を変えるなら「こうしてご自分で自由に追加出来る余地は提供されてるんだから、それでええやん」てことやね。
ほかの言語で実現されているテンプレートエンジンの多くは、実装言語機能を「直接」使えることが多くて、そうである場合は組み込み機能の提供量で不満を感じる機会は少ない。けれども jet はそうではないので、不足機能があればここまでやってきたやり方で追加していくことになる。それをどの程度予めやってあるか、つまり built-ins が不十分であることが不満の元となる可能性がある、という話だが、事実ワタシのやりたいたった一つの実例で既にそれが露呈しつつある、ということ。
make_seq と ints の例で、もう答えは出ているようなもんなんだけど、「当り前のことがしたい場合に足りていない機能が既に結構多い」のだわ。難しいことがしたい場合に欠けている、ではないんだよ、基礎的なことを行う際の部品から既に足りない。上でやった例がわかりやすい:
1 {{ block break_s(sec=5) }}
2 {{- range make_seq(sec / 5) }}
3 {{- nxt := ((. + 1) * 5) }}
4 {{- if nxt > sec }}{{ nxt = sec }}{{ end -}}
5 {{- t := nxt - (. * 5) -}}
6 <break time="{{t}}s"/>
7 {{ end -}}
8 {{ end }}
ひとつには make_seq のように整数を前提としないシーケンス生成が必要だったということを意味しているが、そうではなくて例えばここで「sec / 5」を「整数にしたい」と思ったとして、既にその機能がない。つまり「int へのキャスト」のようなことが出来ない、ここでの例で make_seq 自身がそう空気を読もうとしない限りは。さらに、たとえば「{{t}}」部分で数値書式化、たとえば float で「%.1f」で出力したいと思ったとしても、またしてもその機能がない。
どちらも「追加しちゃえる」:
1 package main
2
3 import (
4 "fmt"
5 "strings"
6 "strconv"
7 "os"
8 "bytes"
9 "regexp"
10 "reflect"
11 "path/filepath"
12 "io"
13 "encoding/json"
14 "github.com/ogier/pflag"
15 mxj "github.com/clbanning/mxj/v2"
16 "github.com/CloudyKit/jet/v6"
17 "github.com/CloudyKit/jet/v6/loaders/multi"
18 )
19
20 type MyOpt struct {
21 extravars *string
22 subnl *string
23 addpath *string
24 }
25 var myOpt MyOpt
26
27 func init() {
28 ousage := pflag.Usage
29 pflag.Usage = func() {
30 ousage()
31 os.Exit(1)
32 }
33 myOpt.extravars = pflag.StringP(
34 "extravars", "v", "", "specify json or xml for the variables")
35 myOpt.subnl = pflag.StringP(
36 "replace_rendered_newline", "n", "\n", "replace newline to this")
37 myOpt.addpath = pflag.StringP(
38 "addpath", "p", "",
39 "additional template paths (semicolon separated list)")
40 }
41
42 func to_num(iv interface{}) interface{} {
43 v := reflect.ValueOf(iv)
44 if !v.IsValid() {
45 panic(fmt.Errorf("invalid value can't be converted to float64"))
46 }
47 kind := v.Kind()
48 if kind == reflect.Float32 || kind == reflect.Float64 {
49 return v.Float()
50 } else if kind >= reflect.Int && kind <= reflect.Int64 {
51 return v.Int()
52 } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
53 return v.Uint()
54 } else if kind == reflect.String {
55 n, e := strconv.ParseFloat(v.String(), 0)
56 if e != nil {
57 panic(e)
58 }
59 return n
60 } else if kind == reflect.Bool {
61 if v.Bool() {
62 return 0
63 }
64 return 1
65 }
66 panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
67 }
68 func to_int(iv interface{}, base... int) interface{} {
69 v := reflect.ValueOf(iv)
70 if !v.IsValid() {
71 panic(fmt.Errorf("invalid value can't be converted to float64"))
72 }
73 kind := v.Kind()
74 if kind == reflect.String {
75 var b int
76 if len(base) > 0 {
77 b = base[0]
78 } else {
79 b = 10
80 }
81 s := v.String()
82 if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
83 s = s[2:]
84 }
85 n, e := strconv.ParseInt(s, b, 0)
86 if e != nil {
87 panic(e)
88 }
89 return n
90 }
91 return int(to_num(iv).(float64))
92 }
93
94 func _Read(path string) ([]byte, error) {
95 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
96 for _, p := range sp {
97 if f, err := os.Open(filepath.Join(p, path)); err == nil {
98 return io.ReadAll(f)
99 }
100 }
101 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
102 }
103
104 func json_loads(jsoncont string) interface{} {
105 var i interface{}
106 json.Unmarshal(([]byte)(jsoncont), &i)
107 return i
108 }
109
110 func json_load(fn string) interface{} {
111 jsoncont, err := _Read(fn)
112 if err != nil { panic(err) }
113 return json_loads(string(jsoncont))
114 }
115
116 func xml_loads(xmlcont string) interface{} {
117 i, err := mxj.NewMapXml([]byte(xmlcont))
118 if err != nil { panic(err) }
119 return i
120 }
121
122 func xml_load(fn string) interface{} {
123 xmlcont, err := _Read(fn)
124 if err != nil { panic(err) }
125 return xml_loads(string(xmlcont))
126 }
127
128 func make_seq(sss ...float64) []float64 {
129 sta := 0.0
130 ste := 1.0
131 sto := 1.0
132 if len(sss) == 1 {
133 sto = sss[0]
134 } else if len(sss) == 2 {
135 sta = sss[0]
136 sto = sss[1]
137 } else if len(sss) == 3 {
138 sta = sss[0]
139 sto = sss[1]
140 ste = sss[2]
141 if ste == 0.0 {
142 panic("`step` must not be zero")
143 }
144 } else {
145 panic("require arguments, at least one for `stop`")
146 }
147 var res []float64
148 for {
149 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
150 break
151 }
152 res = append(res, sta)
153 sta += ste
154 }
155 return res
156 }
157
158 func main() {
159 pflag.Parse()
160
161 var loaders []jet.Loader
162 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
163 for _, p := range strings.Split(*myOpt.addpath, ";") {
164 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
165 }
166 l := multi.NewLoader(loaders...)
167 tmplfn := pflag.Arg(0)
168 set := jet.NewSet(l)
169 tmpl, err := set.GetTemplate(tmplfn)
170 if err != nil {
171 panic(err)
172 }
173 from_fn := func(fn string) interface{} {
174 if filepath.Ext(fn) == ".xml" {
175 return xml_load(fn)
176 } else {
177 return json_load(fn)
178 }
179 }
180 var context interface{}
181 if len(pflag.Args()) > 1 {
182 context = from_fn(pflag.Arg(1))
183 }
184 var variable = make(jet.VarMap)
185 variable.Set("json_loads", json_loads)
186 variable.Set("json_load", json_load)
187 variable.Set("xml_loads", xml_loads)
188 variable.Set("xml_load", xml_load)
189 variable.Set("make_seq", make_seq)
190 variable.Set("float", to_num)
191 variable.Set("int", to_int)
192 variable.Set("sprintf", fmt.Sprintf)
193 if *myOpt.extravars != "" {
194 outer := from_fn(*myOpt.extravars)
195 for k, v := range outer.(map[string]interface{}) {
196 variable.Set(k, v)
197 }
198 }
199 nl_remove := func (cont string) string {
200 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
201 }
202 writer := bytes.NewBufferString("")
203 err = tmpl.Execute(writer, variable, context)
204 if err != nil {
205 panic(err)
206 }
207 os.Stdout.Write([]byte(nl_remove(writer.String())))
208 }
1 {{ int("30") }}
2 {{ int(5.1 / 2) }}
3 {{ int(3) }}
4 {{ int("01", 8) }}
5 {{ int("07", 8) }}
6 {{ int("10", 8) }}
7 {{ int("11", 8) }}
8 {{ int("F", 16) }}
9 {{ int(0xE) }}
10 {{ int("0xE", 16) }}
11 {{- s := "0xE" }}
12 {{ int(s[2:], 16) }}
13 {{ sprintf("%.9f", float("29.5e-3")) }}
そりゃぁ別になんにも難しいことはないわいよ。to_num、to_int の実装はかなり github.com/CloudyKit/jet/v6/eval.go 内の実装を真似れたしさ。けど「さすがにこれ、built-ins にないのって、あまりにもあんまりだ」と思わんか? 特に sprintf よな。
そして話はさらに「min/max」や、math 関数に及ぶわけだ、当然それらもない。少なくとも min, max があれば、上の break の実現は少しスマートになる。そして「どこまでやれば満足か?」という境界の問題になってくる。そもそも「int化」だけでなく、丸め機能のワンセット(floor, ceil, round)が必要なことも多いしさ。うーん…。ワタシとしてはまずは min/max と math のフルセットがあると嬉しい、と思うんだけどね、strconv フルセット、みたいなのもチラついちゃうんよね、ので、ちょっともうちっと落ち着いて考えたいわ。
ので、今回はひとまずここまで。
2022-02-15追記:
半日寝かせたくらいじゃ、取り立てて考えが整理出来るわけでもないのだよね。たとえば math について「全部だ」と決断しようとする場合ですら「Constants も含めるか」というバリエーションがあってだな。
ひとまず「ループのコントロールにて使いがちなのでとにかく非常に日常的に頻出」という意味で、符号操作・丸め・刈り取り系だけは、と考えるならば、以下の品揃えになるだろうと思う:
mod, remainder は直接使わずとも「%」演算子があるからいい、という考え方もあるけれど、まぁあってもいいだろうとは思う。で、これらをミニマルと考えたとしての「じゃぁその次に最小限は?」の答えは…、まぁあんましないね。どれもいらんとも言えるし、どれもいるとも言えるし。どれも割と基礎的な級数展開の元になるものたちだからね、必要になる時は必要になる。Go 実装の変更はオモロいもんではない:
1 package main
2
3 import (
4 "fmt"
5 "strings"
6 "strconv"
7 "os"
8 "bytes"
9 "regexp"
10 "reflect"
11 "path/filepath"
12 "io"
13 "math"
14 "encoding/json"
15 "github.com/ogier/pflag"
16 mxj "github.com/clbanning/mxj/v2"
17 "github.com/CloudyKit/jet/v6"
18 "github.com/CloudyKit/jet/v6/loaders/multi"
19 )
20
21 type MyOpt struct {
22 extravars *string
23 subnl *string
24 addpath *string
25 }
26 var myOpt MyOpt
27
28 func init() {
29 ousage := pflag.Usage
30 pflag.Usage = func() {
31 ousage()
32 os.Exit(1)
33 }
34 myOpt.extravars = pflag.StringP(
35 "extravars", "v", "", "specify json or xml for the variables")
36 myOpt.subnl = pflag.StringP(
37 "replace_rendered_newline", "n", "\n", "replace newline to this")
38 myOpt.addpath = pflag.StringP(
39 "addpath", "p", "",
40 "additional template paths (semicolon separated list)")
41 }
42
43 func to_num(iv interface{}) interface{} {
44 v := reflect.ValueOf(iv)
45 if !v.IsValid() {
46 panic(fmt.Errorf("invalid value can't be converted to float64"))
47 }
48 kind := v.Kind()
49 if kind == reflect.Float32 || kind == reflect.Float64 {
50 return v.Float()
51 } else if kind >= reflect.Int && kind <= reflect.Int64 {
52 return v.Int()
53 } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
54 return v.Uint()
55 } else if kind == reflect.String {
56 n, e := strconv.ParseFloat(v.String(), 0)
57 if e != nil {
58 panic(e)
59 }
60 return n
61 } else if kind == reflect.Bool {
62 if v.Bool() {
63 return 0
64 }
65 return 1
66 }
67 panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
68 }
69 func to_int(iv interface{}, base... int) interface{} {
70 v := reflect.ValueOf(iv)
71 if !v.IsValid() {
72 panic(fmt.Errorf("invalid value can't be converted to float64"))
73 }
74 kind := v.Kind()
75 if kind == reflect.String {
76 var b int
77 if len(base) > 0 {
78 b = base[0]
79 } else {
80 b = 10
81 }
82 s := v.String()
83 if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
84 s = s[2:]
85 }
86 n, e := strconv.ParseInt(s, b, 0)
87 if e != nil {
88 panic(e)
89 }
90 return n
91 }
92 return int(to_num(iv).(float64))
93 }
94
95 func _Read(path string) ([]byte, error) {
96 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
97 for _, p := range sp {
98 if f, err := os.Open(filepath.Join(p, path)); err == nil {
99 return io.ReadAll(f)
100 }
101 }
102 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
103 }
104
105 func json_loads(jsoncont string) interface{} {
106 var i interface{}
107 json.Unmarshal(([]byte)(jsoncont), &i)
108 return i
109 }
110
111 func json_load(fn string) interface{} {
112 jsoncont, err := _Read(fn)
113 if err != nil { panic(err) }
114 return json_loads(string(jsoncont))
115 }
116
117 func xml_loads(xmlcont string) interface{} {
118 i, err := mxj.NewMapXml([]byte(xmlcont))
119 if err != nil { panic(err) }
120 return i
121 }
122
123 func xml_load(fn string) interface{} {
124 xmlcont, err := _Read(fn)
125 if err != nil { panic(err) }
126 return xml_loads(string(xmlcont))
127 }
128
129 func make_seq(sss ...float64) []float64 {
130 sta := 0.0
131 ste := 1.0
132 sto := 1.0
133 if len(sss) == 1 {
134 sto = sss[0]
135 } else if len(sss) == 2 {
136 sta = sss[0]
137 sto = sss[1]
138 } else if len(sss) == 3 {
139 sta = sss[0]
140 sto = sss[1]
141 ste = sss[2]
142 if ste == 0.0 {
143 panic("`step` must not be zero")
144 }
145 } else {
146 panic("require arguments, at least one for `stop`")
147 }
148 var res []float64
149 for {
150 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
151 break
152 }
153 res = append(res, sta)
154 sta += ste
155 }
156 return res
157 }
158
159 func main() {
160 pflag.Parse()
161
162 var loaders []jet.Loader
163 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
164 for _, p := range strings.Split(*myOpt.addpath, ";") {
165 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
166 }
167 l := multi.NewLoader(loaders...)
168 tmplfn := pflag.Arg(0)
169 set := jet.NewSet(l)
170 tmpl, err := set.GetTemplate(tmplfn)
171 if err != nil {
172 panic(err)
173 }
174 from_fn := func(fn string) interface{} {
175 if filepath.Ext(fn) == ".xml" {
176 return xml_load(fn)
177 } else {
178 return json_load(fn)
179 }
180 }
181 var context interface{}
182 if len(pflag.Args()) > 1 {
183 context = from_fn(pflag.Arg(1))
184 }
185 var variable = make(jet.VarMap)
186 variable.Set("json_loads", json_loads)
187 variable.Set("json_load", json_load)
188 variable.Set("xml_loads", xml_loads)
189 variable.Set("xml_load", xml_load)
190 variable.Set("make_seq", make_seq)
191 variable.Set("float", to_num)
192 variable.Set("int", to_int)
193 variable.Set("sprintf", fmt.Sprintf)
194 variable.Set("abs", math.Abs)
195 variable.Set("ceil", math.Ceil)
196 variable.Set("copysign", math.Copysign)
197 variable.Set("dim", math.Dim)
198 variable.Set("floor", math.Floor)
199 variable.Set("max", math.Max)
200 variable.Set("min", math.Min)
201 variable.Set("round", math.Round)
202 variable.Set("roundtoeven", math.RoundToEven)
203 variable.Set("trunc", math.Trunc)
204 variable.Set("mod", math.Mod)
205 variable.Set("modf", math.Modf)
206 variable.Set("remainder", math.Remainder)
207 if *myOpt.extravars != "" {
208 outer := from_fn(*myOpt.extravars)
209 for k, v := range outer.(map[string]interface{}) {
210 variable.Set(k, v)
211 }
212 }
213 nl_remove := func (cont string) string {
214 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
215 }
216 writer := bytes.NewBufferString("")
217 err = tmpl.Execute(writer, variable, context)
218 if err != nil {
219 panic(err)
220 }
221 os.Stdout.Write([]byte(nl_remove(writer.String())))
222 }
まぁ自分のためだけ、てことならこれで存分に十分なんだけどね。ベッセル関数関連なんかこの後一生使わん可能性すらあるですもの。
2022-02-19追記:
「Go 言語のおべんきょ」としての「だとしても少しは実用になるネタ」なんてことを考えてた流れで、ほかでやってたネタとこのネタが結び付いてくれた。putclip はよく使うが getclip はあんまし用途ないよなぁなんて言ったけれど、ふと、テンプレートエンジンの機能としてあったら便利ぞな、と気付いた、今更だけど。
てわけで、「Goのおべんきょ」の足しには全然ならんけれど、道具の実用性はぐんと増すてやーつ:
1 package main
2
3 import (
4 "fmt"
5 "strings"
6 "strconv"
7 "os"
8 "bytes"
9 "regexp"
10 "reflect"
11 "path/filepath"
12 "io"
13 "math"
14 "encoding/json"
15 "github.com/ogier/pflag"
16 mxj "github.com/clbanning/mxj/v2"
17 "github.com/CloudyKit/jet/v6"
18 "github.com/CloudyKit/jet/v6/loaders/multi"
19 "golang.design/x/clipboard" // github.com/golang-design/clipboard
20 )
21
22 type MyOpt struct {
23 extravars *string
24 subnl *string
25 addpath *string
26 }
27 var myOpt MyOpt
28
29 func init() {
30 ousage := pflag.Usage
31 pflag.Usage = func() {
32 ousage()
33 os.Exit(1)
34 }
35 myOpt.extravars = pflag.StringP(
36 "extravars", "v", "", "specify json or xml for the variables")
37 myOpt.subnl = pflag.StringP(
38 "replace_rendered_newline", "n", "\n", "replace newline to this")
39 myOpt.addpath = pflag.StringP(
40 "addpath", "p", "",
41 "additional template paths (semicolon separated list)")
42
43 err := clipboard.Init()
44 if err != nil {
45 panic(err)
46 }
47 }
48
49 func to_num(iv interface{}) interface{} {
50 v := reflect.ValueOf(iv)
51 if !v.IsValid() {
52 panic(fmt.Errorf("invalid value can't be converted to float64"))
53 }
54 kind := v.Kind()
55 if kind == reflect.Float32 || kind == reflect.Float64 {
56 return v.Float()
57 } else if kind >= reflect.Int && kind <= reflect.Int64 {
58 return v.Int()
59 } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
60 return v.Uint()
61 } else if kind == reflect.String {
62 n, e := strconv.ParseFloat(v.String(), 0)
63 if e != nil {
64 panic(e)
65 }
66 return n
67 } else if kind == reflect.Bool {
68 if v.Bool() {
69 return 0
70 }
71 return 1
72 }
73 panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
74 }
75 func to_int(iv interface{}, base... int) interface{} {
76 v := reflect.ValueOf(iv)
77 if !v.IsValid() {
78 panic(fmt.Errorf("invalid value can't be converted to float64"))
79 }
80 kind := v.Kind()
81 if kind == reflect.String {
82 var b int
83 if len(base) > 0 {
84 b = base[0]
85 } else {
86 b = 10
87 }
88 s := v.String()
89 if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
90 s = s[2:]
91 }
92 n, e := strconv.ParseInt(s, b, 0)
93 if e != nil {
94 panic(e)
95 }
96 return n
97 }
98 return int(to_num(iv).(float64))
99 }
100
101 func _Read(path string) ([]byte, error) {
102 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
103 for _, p := range sp {
104 if f, err := os.Open(filepath.Join(p, path)); err == nil {
105 return io.ReadAll(f)
106 }
107 }
108 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
109 }
110
111 func json_loads(jsoncont string) interface{} {
112 var i interface{}
113 json.Unmarshal(([]byte)(jsoncont), &i)
114 return i
115 }
116
117 func json_load(fn string) interface{} {
118 jsoncont, err := _Read(fn)
119 if err != nil { panic(err) }
120 return json_loads(string(jsoncont))
121 }
122
123 func xml_loads(xmlcont string) interface{} {
124 i, err := mxj.NewMapXml([]byte(xmlcont))
125 if err != nil { panic(err) }
126 return i
127 }
128
129 func xml_load(fn string) interface{} {
130 xmlcont, err := _Read(fn)
131 if err != nil { panic(err) }
132 return xml_loads(string(xmlcont))
133 }
134
135 func make_seq(sss ...float64) []float64 {
136 sta := 0.0
137 ste := 1.0
138 sto := 1.0
139 if len(sss) == 1 {
140 sto = sss[0]
141 } else if len(sss) == 2 {
142 sta = sss[0]
143 sto = sss[1]
144 } else if len(sss) == 3 {
145 sta = sss[0]
146 sto = sss[1]
147 ste = sss[2]
148 if ste == 0.0 {
149 panic("`step` must not be zero")
150 }
151 } else {
152 panic("require arguments, at least one for `stop`")
153 }
154 var res []float64
155 for {
156 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
157 break
158 }
159 res = append(res, sta)
160 sta += ste
161 }
162 return res
163 }
164
165 func getclip(fmtimg bool) ([]byte) {
166 if !fmtimg {
167 return clipboard.Read(clipboard.FmtText)
168 } else {
169 return clipboard.Read(clipboard.FmtImage)
170 }
171 }
172
173 func main() {
174 pflag.Parse()
175
176 var loaders []jet.Loader
177 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
178 for _, p := range strings.Split(*myOpt.addpath, ";") {
179 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
180 }
181 l := multi.NewLoader(loaders...)
182 tmplfn := pflag.Arg(0)
183 set := jet.NewSet(l)
184 tmpl, err := set.GetTemplate(tmplfn)
185 if err != nil {
186 panic(err)
187 }
188 from_fn := func(fn string) interface{} {
189 if filepath.Ext(fn) == ".xml" {
190 return xml_load(fn)
191 } else {
192 return json_load(fn)
193 }
194 }
195 var context interface{}
196 if len(pflag.Args()) > 1 {
197 context = from_fn(pflag.Arg(1))
198 }
199 var variable = make(jet.VarMap)
200 variable.Set("json_loads", json_loads)
201 variable.Set("json_load", json_load)
202 variable.Set("xml_loads", xml_loads)
203 variable.Set("xml_load", xml_load)
204 variable.Set("make_seq", make_seq)
205 variable.Set("float", to_num)
206 variable.Set("int", to_int)
207 variable.Set("sprintf", fmt.Sprintf)
208 variable.Set("abs", math.Abs)
209 variable.Set("ceil", math.Ceil)
210 variable.Set("copysign", math.Copysign)
211 variable.Set("dim", math.Dim)
212 variable.Set("floor", math.Floor)
213 variable.Set("max", math.Max)
214 variable.Set("min", math.Min)
215 variable.Set("round", math.Round)
216 variable.Set("roundtoeven", math.RoundToEven)
217 variable.Set("trunc", math.Trunc)
218 variable.Set("mod", math.Mod)
219 variable.Set("modf", math.Modf)
220 variable.Set("remainder", math.Remainder)
221 variable.Set("getclip", getclip)
222 if *myOpt.extravars != "" {
223 outer := from_fn(*myOpt.extravars)
224 for k, v := range outer.(map[string]interface{}) {
225 variable.Set(k, v)
226 }
227 }
228 nl_remove := func (cont string) string {
229 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
230 }
231 writer := bytes.NewBufferString("")
232 err = tmpl.Execute(writer, variable, context)
233 if err != nil {
234 panic(err)
235 }
236 os.Stdout.Write([]byte(nl_remove(writer.String())))
237 }
使用例:
1 {{ getclip(false) | upper }}
2 {* | html エスケープはデフォルトの振る舞いなので例としては相応しくない、
3 ので、クソおもしろくない upper を例にしてる。 *}
これさ、まさに今あなたが目にしてるこのページのね、スニペット部分があるじゃんか。これを「ワタシがあなたに見せるために執筆」してるときには、これってただの html のテキストボックスに書き込んでるわけですよ、ゆえに、html に記述する際にはエスケープしなければならない文字たち(大なり小なりとかよ)は、手作業でエスケープしてるわけ。でも今やったようなのを使えば、まぁちょっと楽出来るんだわ。getclip と組み合わせればなおのこと。うん、我ながらいいこと思いついたわい、とも思うし、今頃気付くなよ、とも思う。もっと早く気付いとけば…とね。
フィルタとして使えるのが今は html くらいしかないけれど、png イメージのために base64 エンコーダとかも組み込むとなお良いよね、これはあとでやろっかなと。やったとすれば使用例はこうなるはず:
1 {{ getclip(true) | base64encode | raw }}
2022-02-19追記(2):
「png イメージのために base64 エンコーダとかも組み込むとなお良い」:
1 package main
2
3 import (
4 "fmt"
5 "strings"
6 "strconv"
7 "os"
8 "bytes"
9 "regexp"
10 "reflect"
11 "path/filepath"
12 "io"
13 "math"
14 "encoding/json"
15 "encoding/base64"
16 "github.com/ogier/pflag"
17 mxj "github.com/clbanning/mxj/v2"
18 "github.com/CloudyKit/jet/v6"
19 "github.com/CloudyKit/jet/v6/loaders/multi"
20 "golang.design/x/clipboard" // github.com/golang-design/clipboard
21 )
22
23 type MyOpt struct {
24 extravars *string
25 subnl *string
26 addpath *string
27 }
28 var myOpt MyOpt
29
30 func init() {
31 ousage := pflag.Usage
32 pflag.Usage = func() {
33 ousage()
34 os.Exit(1)
35 }
36 myOpt.extravars = pflag.StringP(
37 "extravars", "v", "", "specify json or xml for the variables")
38 myOpt.subnl = pflag.StringP(
39 "replace_rendered_newline", "n", "\n", "replace newline to this")
40 myOpt.addpath = pflag.StringP(
41 "addpath", "p", "",
42 "additional template paths (semicolon separated list)")
43
44 err := clipboard.Init()
45 if err != nil {
46 panic(err)
47 }
48 }
49
50 func to_num(iv interface{}) interface{} {
51 v := reflect.ValueOf(iv)
52 if !v.IsValid() {
53 panic(fmt.Errorf("invalid value can't be converted to float64"))
54 }
55 kind := v.Kind()
56 if kind == reflect.Float32 || kind == reflect.Float64 {
57 return v.Float()
58 } else if kind >= reflect.Int && kind <= reflect.Int64 {
59 return v.Int()
60 } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
61 return v.Uint()
62 } else if kind == reflect.String {
63 n, e := strconv.ParseFloat(v.String(), 0)
64 if e != nil {
65 panic(e)
66 }
67 return n
68 } else if kind == reflect.Bool {
69 if v.Bool() {
70 return 0
71 }
72 return 1
73 }
74 panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
75 }
76 func to_int(iv interface{}, base... int) interface{} {
77 v := reflect.ValueOf(iv)
78 if !v.IsValid() {
79 panic(fmt.Errorf("invalid value can't be converted to float64"))
80 }
81 kind := v.Kind()
82 if kind == reflect.String {
83 var b int
84 if len(base) > 0 {
85 b = base[0]
86 } else {
87 b = 10
88 }
89 s := v.String()
90 if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
91 s = s[2:]
92 }
93 n, e := strconv.ParseInt(s, b, 0)
94 if e != nil {
95 panic(e)
96 }
97 return n
98 }
99 return int(to_num(iv).(float64))
100 }
101
102 func _Read(path string) ([]byte, error) {
103 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
104 for _, p := range sp {
105 if f, err := os.Open(filepath.Join(p, path)); err == nil {
106 return io.ReadAll(f)
107 }
108 }
109 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
110 }
111
112 func json_loads(jsoncont string) interface{} {
113 var i interface{}
114 json.Unmarshal(([]byte)(jsoncont), &i)
115 return i
116 }
117
118 func json_load(fn string) interface{} {
119 jsoncont, err := _Read(fn)
120 if err != nil { panic(err) }
121 return json_loads(string(jsoncont))
122 }
123
124 func xml_loads(xmlcont string) interface{} {
125 i, err := mxj.NewMapXml([]byte(xmlcont))
126 if err != nil { panic(err) }
127 return i
128 }
129
130 func xml_load(fn string) interface{} {
131 xmlcont, err := _Read(fn)
132 if err != nil { panic(err) }
133 return xml_loads(string(xmlcont))
134 }
135
136 func make_seq(sss ...float64) []float64 {
137 sta := 0.0
138 ste := 1.0
139 sto := 1.0
140 if len(sss) == 1 {
141 sto = sss[0]
142 } else if len(sss) == 2 {
143 sta = sss[0]
144 sto = sss[1]
145 } else if len(sss) == 3 {
146 sta = sss[0]
147 sto = sss[1]
148 ste = sss[2]
149 if ste == 0.0 {
150 panic("`step` must not be zero")
151 }
152 } else {
153 panic("require arguments, at least one for `stop`")
154 }
155 var res []float64
156 for {
157 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
158 break
159 }
160 res = append(res, sta)
161 sta += ste
162 }
163 return res
164 }
165
166 func getclip(fmtimg bool) ([]byte) {
167 if !fmtimg {
168 return clipboard.Read(clipboard.FmtText)
169 } else {
170 return clipboard.Read(clipboard.FmtImage)
171 }
172 }
173
174 func main() {
175 pflag.Parse()
176
177 var loaders []jet.Loader
178 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
179 for _, p := range strings.Split(*myOpt.addpath, ";") {
180 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
181 }
182 l := multi.NewLoader(loaders...)
183 tmplfn := pflag.Arg(0)
184 set := jet.NewSet(l)
185 tmpl, err := set.GetTemplate(tmplfn)
186 if err != nil {
187 panic(err)
188 }
189 from_fn := func(fn string) interface{} {
190 if filepath.Ext(fn) == ".xml" {
191 return xml_load(fn)
192 } else {
193 return json_load(fn)
194 }
195 }
196 var context interface{}
197 if len(pflag.Args()) > 1 {
198 context = from_fn(pflag.Arg(1))
199 }
200 var variable = make(jet.VarMap)
201 variable.Set("json_loads", json_loads)
202 variable.Set("json_load", json_load)
203 variable.Set("xml_loads", xml_loads)
204 variable.Set("xml_load", xml_load)
205 variable.Set("make_seq", make_seq)
206 variable.Set("float", to_num)
207 variable.Set("int", to_int)
208 variable.Set("sprintf", fmt.Sprintf)
209 variable.Set("abs", math.Abs)
210 variable.Set("ceil", math.Ceil)
211 variable.Set("copysign", math.Copysign)
212 variable.Set("dim", math.Dim)
213 variable.Set("floor", math.Floor)
214 variable.Set("max", math.Max)
215 variable.Set("min", math.Min)
216 variable.Set("round", math.Round)
217 variable.Set("roundtoeven", math.RoundToEven)
218 variable.Set("trunc", math.Trunc)
219 variable.Set("mod", math.Mod)
220 variable.Set("modf", math.Modf)
221 variable.Set("remainder", math.Remainder)
222 variable.Set("getclip", getclip)
223 variable.Set("base64encode", base64.StdEncoding.EncodeToString)
224 variable.Set("base64decode", base64.StdEncoding.DecodeString)
225 if *myOpt.extravars != "" {
226 outer := from_fn(*myOpt.extravars)
227 for k, v := range outer.(map[string]interface{}) {
228 variable.Set(k, v)
229 }
230 }
231 nl_remove := func (cont string) string {
232 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
233 }
234 writer := bytes.NewBufferString("")
235 err = tmpl.Execute(writer, variable, context)
236 if err != nil {
237 panic(err)
238 }
239 os.Stdout.Write([]byte(nl_remove(writer.String())))
240 }
まぁなんてことはないよね。なお、これまで何気にやってたけど「base64.StdEncoding.DecodeString」の戻りには error を含むのだが、そこはうまくやってくれる。
ところで上で追記コメント入れたけど、この回でデリミタの変更をトライしたんだけれど、#187 のせいで一旦断念した。やめて欲しいよなこういうの。
2022-03-13追記:
本題とちょっと離れるんだけど、WSL2 の導入で本物の linux を自由に使えるようになったんで、そっちの環境でビルドしてみようと試みたんだけど、ちょっと問題が。
まず、本日時点での「ubuntu on WSL2 マイクロソフト公式」の apt 管理の Go はめっさ古くて 1.13.8、なので、1.17.8 をわざわざ入れた。これはワタシが今ネイティブ Windows 上で使ってる 1.16 より新しい。次に「ubuntu on WSL2 マイクロソフト公式、on Windows 10」ということは必然的に「X Window System、ナニソレタベレンノ」なの。
そういうわけで、まずは「X Window System、ナニソレタベレンノ」が clipboard 部分のビルドで問題を起こす。のだけど、それを回避するためにと C で言うところの「#ifdef」みたいなものはないだろうか、と探してみたんだけれど、…うん、あるにはあるんだ、だけどそれ、Go ではめっちゃ限定的で、コンパイル単位、つまり(// +build tag というディレクティブで)「a.go」というファイル一個単位の制御しか出来ないのよ、「問題を起こす部分だけ取り除く」ことが出来ない。うげぇ。いろいろ考えたが、「オレオレプリプロセス」が一番シンプルよな、と、sed で処理出来る単純なコメント(//ENABLE_CLIP BEGIN と //ENABLE_CLIP END)を突っ込んでおくことにした。で、ビルド用のスクリプトを書いておく:
1 #! /bin/sh
2 if test -z "${COMSPEC}" ; then
3 ext=""
4 else
5 ext=".exe"
6 fi
7 if test "$1"="--disable-clip" ; then
8 sed '/ENABLE_CLIP BEGIN/,/ENABLE_CLIP END/'d main.go.in > main.go
9 else
10 cat main.go.in > main.go
11 fi
12
13 go build && (
14 rm -f go_jettemplate_example${ext}
15 ln tmpljetexpr${ext} go_jettemplate_example${ext}
16 install -c go_jettemplate_example${ext} /usr/local/bin
17 )
go のソースは「main.go.in」にしとく、てことだよ。昔ながらの方法ね。
もう一つは「ReadAll」問題。ワタシが読んだドキュメントは「1.16 になって io が整理されたので、もはや io/ioutil はいらんのぢゃ」という趣旨のことを言っていたと理解していた。けど、1.17 で「io.ReadAll」が消えとる…。これは ioutil に戻すしかないね。
てわけで、こんな感じ:
1 package main
2
3 import (
4 "fmt"
5 "strings"
6 "strconv"
7 "os"
8 "bytes"
9 "regexp"
10 "reflect"
11 "path/filepath"
12 "io/ioutil"
13 "math"
14 "encoding/json"
15 "encoding/base64"
16 "github.com/ogier/pflag"
17 mxj "github.com/clbanning/mxj/v2"
18 "github.com/CloudyKit/jet/v6"
19 "github.com/CloudyKit/jet/v6/loaders/multi"
20 //ENABLE_CLIP BEGIN
21 "golang.design/x/clipboard" // github.com/golang-design/clipboard
22 //ENABLE_CLIP END
23 )
24
25 type MyOpt struct {
26 extravars *string
27 subnl *string
28 addpath *string
29 }
30 var myOpt MyOpt
31
32 func init() {
33 ousage := pflag.Usage
34 pflag.Usage = func() {
35 ousage()
36 os.Exit(1)
37 }
38 myOpt.extravars = pflag.StringP(
39 "extravars", "v", "", "specify json or xml for the variables")
40 myOpt.subnl = pflag.StringP(
41 "replace_rendered_newline", "n", "\n", "replace newline to this")
42 myOpt.addpath = pflag.StringP(
43 "addpath", "p", "",
44 "additional template paths (semicolon separated list)")
45
46 //ENABLE_CLIP BEGIN
47 err := clipboard.Init()
48 if err != nil {
49 panic(err)
50 }
51 //ENABLE_CLIP END
52 }
53
54 func to_num(iv interface{}) interface{} {
55 v := reflect.ValueOf(iv)
56 if !v.IsValid() {
57 panic(fmt.Errorf("invalid value can't be converted to float64"))
58 }
59 kind := v.Kind()
60 if kind == reflect.Float32 || kind == reflect.Float64 {
61 return v.Float()
62 } else if kind >= reflect.Int && kind <= reflect.Int64 {
63 return v.Int()
64 } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
65 return v.Uint()
66 } else if kind == reflect.String {
67 n, e := strconv.ParseFloat(v.String(), 0)
68 if e != nil {
69 panic(e)
70 }
71 return n
72 } else if kind == reflect.Bool {
73 if v.Bool() {
74 return 0
75 }
76 return 1
77 }
78 panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
79 }
80 func to_int(iv interface{}, base... int) interface{} {
81 v := reflect.ValueOf(iv)
82 if !v.IsValid() {
83 panic(fmt.Errorf("invalid value can't be converted to float64"))
84 }
85 kind := v.Kind()
86 if kind == reflect.String {
87 var b int
88 if len(base) > 0 {
89 b = base[0]
90 } else {
91 b = 10
92 }
93 s := v.String()
94 if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
95 s = s[2:]
96 }
97 n, e := strconv.ParseInt(s, b, 0)
98 if e != nil {
99 panic(e)
100 }
101 return n
102 }
103 return int(to_num(iv).(float64))
104 }
105
106 func _Read(path string) ([]byte, error) {
107 sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
108 for _, p := range sp {
109 if f, err := os.Open(filepath.Join(p, path)); err == nil {
110 return ioutil.ReadAll(f)
111 }
112 }
113 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
114 }
115
116 func json_loads(jsoncont string) interface{} {
117 var i interface{}
118 json.Unmarshal(([]byte)(jsoncont), &i)
119 return i
120 }
121
122 func json_load(fn string) interface{} {
123 jsoncont, err := _Read(fn)
124 if err != nil { panic(err) }
125 return json_loads(string(jsoncont))
126 }
127
128 func xml_loads(xmlcont string) interface{} {
129 i, err := mxj.NewMapXml([]byte(xmlcont))
130 if err != nil { panic(err) }
131 return i
132 }
133
134 func xml_load(fn string) interface{} {
135 xmlcont, err := _Read(fn)
136 if err != nil { panic(err) }
137 return xml_loads(string(xmlcont))
138 }
139
140 func make_seq(sss ...float64) []float64 {
141 sta := 0.0
142 ste := 1.0
143 sto := 1.0
144 if len(sss) == 1 {
145 sto = sss[0]
146 } else if len(sss) == 2 {
147 sta = sss[0]
148 sto = sss[1]
149 } else if len(sss) == 3 {
150 sta = sss[0]
151 sto = sss[1]
152 ste = sss[2]
153 if ste == 0.0 {
154 panic("`step` must not be zero")
155 }
156 } else {
157 panic("require arguments, at least one for `stop`")
158 }
159 var res []float64
160 for {
161 if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
162 break
163 }
164 res = append(res, sta)
165 sta += ste
166 }
167 return res
168 }
169
170 //ENABLE_CLIP BEGIN
171 func getclip(fmtimg bool) ([]byte) {
172 if !fmtimg {
173 return clipboard.Read(clipboard.FmtText)
174 } else {
175 return clipboard.Read(clipboard.FmtImage)
176 }
177 }
178 //ENABLE_CLIP END
179
180 func main() {
181 pflag.Parse()
182
183 var loaders []jet.Loader
184 loaders = append(loaders, jet.NewOSFileSystemLoader("."))
185 for _, p := range strings.Split(*myOpt.addpath, ";") {
186 loaders = append(loaders, jet.NewOSFileSystemLoader(p))
187 }
188 l := multi.NewLoader(loaders...)
189 tmplfn := pflag.Arg(0)
190 set := jet.NewSet(l)
191 tmpl, err := set.GetTemplate(tmplfn)
192 if err != nil {
193 panic(err)
194 }
195 from_fn := func(fn string) interface{} {
196 if filepath.Ext(fn) == ".xml" {
197 return xml_load(fn)
198 } else {
199 return json_load(fn)
200 }
201 }
202 var context interface{}
203 if len(pflag.Args()) > 1 {
204 context = from_fn(pflag.Arg(1))
205 }
206 var variable = make(jet.VarMap)
207 variable.Set("json_loads", json_loads)
208 variable.Set("json_load", json_load)
209 variable.Set("xml_loads", xml_loads)
210 variable.Set("xml_load", xml_load)
211 variable.Set("make_seq", make_seq)
212 variable.Set("float", to_num)
213 variable.Set("int", to_int)
214 variable.Set("sprintf", fmt.Sprintf)
215 variable.Set("abs", math.Abs)
216 variable.Set("ceil", math.Ceil)
217 variable.Set("copysign", math.Copysign)
218 variable.Set("dim", math.Dim)
219 variable.Set("floor", math.Floor)
220 variable.Set("max", math.Max)
221 variable.Set("min", math.Min)
222 variable.Set("round", math.Round)
223 variable.Set("roundtoeven", math.RoundToEven)
224 variable.Set("trunc", math.Trunc)
225 variable.Set("mod", math.Mod)
226 variable.Set("modf", math.Modf)
227 variable.Set("remainder", math.Remainder)
228 //ENABLE_CLIP BEGIN
229 variable.Set("getclip", getclip)
230 //ENABLE_CLIP END
231 variable.Set("base64encode", base64.StdEncoding.EncodeToString)
232 variable.Set("base64decode", base64.StdEncoding.DecodeString)
233 if *myOpt.extravars != "" {
234 outer := from_fn(*myOpt.extravars)
235 for k, v := range outer.(map[string]interface{}) {
236 variable.Set(k, v)
237 }
238 }
239 nl_remove := func (cont string) string {
240 return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
241 }
242 writer := bytes.NewBufferString("")
243 err = tmpl.Execute(writer, variable, context)
244 if err != nil {
245 panic(err)
246 }
247 os.Stdout.Write([]byte(nl_remove(writer.String())))
248 }
にしても ifdef 相当の細かい制御が出来ないのは痛いな…。昔ながらのプログラマならワタシみたいに手札はいっぱい持ってるけど、若いエンジニアがこの答えに辿り着くのって案外難しい気がするよ。なんとかならんもんかね…。(Go が先祖返り的に意図的に必要以上に単純化しているのは、多くは納得がいくんだけど、これはあまり支持出来ない。)